diff --git a/local/recipes/tui/tlc/source/src/editor/handlers.rs b/local/recipes/tui/tlc/source/src/editor/handlers.rs index e4d84d0280..423ea8a8e2 100644 --- a/local/recipes/tui/tlc/source/src/editor/handlers.rs +++ b/local/recipes/tui/tlc/source/src/editor/handlers.rs @@ -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) { diff --git a/local/recipes/tui/tlc/source/src/editor/mod.rs b/local/recipes/tui/tlc/source/src/editor/mod.rs index 770a3c7af2..6fd45b8e2e 100644 --- a/local/recipes/tui/tlc/source/src/editor/mod.rs +++ b/local/recipes/tui/tlc/source/src/editor/mod.rs @@ -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"); + } }