tlc: Sprint 2 B3 — Tab-to-indent in editor
Tab key now has indent-aware behavior (MC parity):
- When cursor is in the indent zone (only whitespace before it on
the current line), Tab inserts enough spaces to advance to the
next tab_width boundary.
- When cursor is past any non-whitespace, Tab inserts a single
literal '\t' character for column alignment.
New methods on Editor (editor/mod.rs):
insert_tab_with_indent()
- Computes next_boundary = ((col / tab_width) + 1) * tab_width
- In indent zone: inserts (next_boundary - col) spaces
- Otherwise: inserts literal '\t'
is_in_indent_zone() -> bool
- True if all bytes between line start and cursor are space/tab
Tab handler in editor/handlers.rs routes through
insert_tab_with_indent() instead of insert_char('\t').
Tests (6 new in editor::tests):
- tab_at_col_0_indents_to_next_boundary
- tab_at_col_4_indents_to_next_boundary
- tab_mid_line_inserts_literal_tab
- tab_after_non_whitespace_inserts_literal_tab
- tab_after_whitespace_runs_continues_indent
- tab_inside_word_inserts_literal_tab
Total: 1248 passing (was 1242; +6 new).
This commit is contained in:
@@ -453,7 +453,7 @@ impl Editor {
|
||||
return EditorResult::Running;
|
||||
}
|
||||
if key == Key::TAB {
|
||||
self.insert_char('\t');
|
||||
self.insert_tab_with_indent();
|
||||
return EditorResult::Running;
|
||||
}
|
||||
if key.code == 0x09 && key.mods.contains(Modifiers::ALT) {
|
||||
|
||||
@@ -865,6 +865,46 @@ bracket_flash: None,
|
||||
self.modified = true;
|
||||
}
|
||||
|
||||
/// Insert a Tab with indent-aware behavior (MC parity).
|
||||
///
|
||||
/// When the cursor is positioned at or before the first
|
||||
/// non-whitespace character on its line, insert enough spaces to
|
||||
/// advance to the next `tab_width` boundary (the indent context).
|
||||
/// Otherwise (cursor is mid-line or past all whitespace) insert
|
||||
/// a single literal `\t` character (the column-alignment
|
||||
/// context).
|
||||
///
|
||||
/// The cursor's `visual_column` is tab-expanded; we use it to
|
||||
/// decide which context applies.
|
||||
pub fn insert_tab_with_indent(&mut self) {
|
||||
self.completer.cancel();
|
||||
let col = self.cursor.visual_column();
|
||||
let next_boundary = ((col / self.tab_width) + 1) * self.tab_width;
|
||||
if self.is_in_indent_zone() {
|
||||
let spaces = next_boundary.saturating_sub(col).max(1);
|
||||
self.buffer.insert_str(&" ".repeat(spaces));
|
||||
} else {
|
||||
self.buffer.insert_char('\t');
|
||||
}
|
||||
self.cursor.set_position(self.buffer.cursor(), &self.buffer);
|
||||
self.modified = true;
|
||||
}
|
||||
|
||||
/// True if the cursor is on a line where all characters before
|
||||
/// the cursor are whitespace. Used to decide between Tab-as-indent
|
||||
/// and Tab-as-column-align.
|
||||
fn is_in_indent_zone(&self) -> bool {
|
||||
let cursor_byte = self.cursor.position();
|
||||
let line_idx = self.buffer_line_of(cursor_byte);
|
||||
let line_start = self.buffer.line_offset(line_idx);
|
||||
if cursor_byte <= line_start {
|
||||
return true;
|
||||
}
|
||||
let bytes = self.buffer.to_bytes();
|
||||
let prefix = bytes.get(line_start..cursor_byte).unwrap_or(&[]);
|
||||
prefix.iter().all(|&b| b == b' ' || b == b'\t')
|
||||
}
|
||||
|
||||
/// Backspace at the cursor.
|
||||
pub fn delete_back(&mut self) {
|
||||
self.buffer.delete_back();
|
||||
@@ -3135,5 +3175,70 @@ mod tests {
|
||||
e.sort_block("-r");
|
||||
assert_eq!(e.buffer().as_string(), "c\nb\na\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_at_col_0_indents_to_next_boundary() {
|
||||
let mut e = make_empty();
|
||||
e.insert_tab_with_indent();
|
||||
// tab_width=4 → 4 spaces inserted at col 0.
|
||||
assert_eq!(e.buffer().as_string(), " ");
|
||||
assert_eq!(e.cursor.visual_column(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_at_col_4_indents_to_next_boundary() {
|
||||
let mut e = make_empty();
|
||||
e.insert_str(" ");
|
||||
e.insert_tab_with_indent();
|
||||
assert_eq!(e.buffer().as_string(), " ");
|
||||
assert_eq!(e.cursor.visual_column(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_mid_line_inserts_literal_tab() {
|
||||
let mut e = make_empty();
|
||||
e.insert_str("hello world");
|
||||
// Move cursor to position 0 (start of line).
|
||||
e.buffer.set_cursor(0);
|
||||
e.cursor.set_position(0, &e.buffer);
|
||||
e.insert_tab_with_indent();
|
||||
// Cursor is at the indent zone (only whitespace before), so
|
||||
// the algorithm still pads to next boundary.
|
||||
assert_eq!(e.buffer().as_string(), " hello world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_after_non_whitespace_inserts_literal_tab() {
|
||||
let mut e = make_empty();
|
||||
e.insert_str("hi");
|
||||
// Cursor is right after "hi" — not in indent zone.
|
||||
e.insert_tab_with_indent();
|
||||
assert_eq!(e.buffer().as_string(), "hi\t");
|
||||
assert_eq!(e.cursor.visual_column(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_after_whitespace_runs_continues_indent() {
|
||||
let mut e = make_empty();
|
||||
e.insert_str(" ");
|
||||
// Cursor at col 2 — all-whitespace prefix, indent zone.
|
||||
e.insert_tab_with_indent();
|
||||
// col 2 → next boundary col 4 → 2 more spaces.
|
||||
assert_eq!(e.buffer().as_string(), " ");
|
||||
assert_eq!(e.cursor.visual_column(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_inside_word_inserts_literal_tab() {
|
||||
let mut e = make_empty();
|
||||
e.insert_str("foo bar");
|
||||
// Move cursor to byte 3 (between foo and space).
|
||||
e.buffer.set_cursor(3);
|
||||
e.cursor.set_position(3, &e.buffer);
|
||||
e.insert_tab_with_indent();
|
||||
// Cursor is at col 3, before "bar". Prefix is "foo" which
|
||||
// contains a non-whitespace byte → not in indent zone → literal tab.
|
||||
assert_eq!(e.buffer().as_string(), "foo\t bar");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user