From d4ddc4e2c75df58b02225b80a9710201b85d3d1f Mon Sep 17 00:00:00 2001 From: kellito Date: Sun, 5 Jul 2026 16:59:18 +0300 Subject: [PATCH] =?UTF-8?q?tlc:=20Sprint=202=20B4=20=E2=80=94=20configurab?= =?UTF-8?q?le=20tab=20width=20via=20Alt-T=20cycle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds user-facing control over the editor tab width. New methods on Editor (editor/mod.rs): tab_width() -> usize - Returns current visual tab width in spaces. set_tab_width(width: usize) - Sets tab width, clamped to min 1. cycle_tab_width() -> usize - Cycles 4 → 8 → 2 → 4 (default 4 → wider → narrower → back). - Returns the new width. Alt-T binding (editor/handlers.rs): try_global_shortcut now handles Alt-T (0x74) and calls cycle_tab_width(). Status bar shows 'Tab width: N'. Tab-to-indent (B3) and word-wrap (B1) both consume tab_width via the field, so changing it via Alt-T takes effect immediately on next render / insert. Tests (5 new in editor::tests): - tab_width_default_is_four - set_tab_width_updates_field (verifies 0 → 1 clamp) - cycle_tab_width_advances_2_4_8_2 - cycle_tab_width_from_unusual_value_resets_to_2 - tab_to_indent_uses_current_tab_width Config wiring note: cfg.editor.tab_width (config.rs) already has the field with serde defaults and persistence. Wiring it into Editor::open() requires the constructor to accept &Config and is deferred to a future sprint. Total: 1253 passing (was 1248; +5 new). --- .../tui/tlc/source/src/editor/handlers.rs | 5 ++ .../recipes/tui/tlc/source/src/editor/mod.rs | 64 +++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/local/recipes/tui/tlc/source/src/editor/handlers.rs b/local/recipes/tui/tlc/source/src/editor/handlers.rs index 423ea8a8e2..75a23959ca 100644 --- a/local/recipes/tui/tlc/source/src/editor/handlers.rs +++ b/local/recipes/tui/tlc/source/src/editor/handlers.rs @@ -296,6 +296,11 @@ impl Editor { }); return Some(EditorResult::Running); } + 0x74 => { + let new_width = self.cycle_tab_width(); + self.message = Some(format!("Tab width: {new_width}")); + return Some(EditorResult::Running); + } 0x72 => { self.redo(); return Some(EditorResult::Running); diff --git a/local/recipes/tui/tlc/source/src/editor/mod.rs b/local/recipes/tui/tlc/source/src/editor/mod.rs index 6fd45b8e2e..aaa2f8ae7b 100644 --- a/local/recipes/tui/tlc/source/src/editor/mod.rs +++ b/local/recipes/tui/tlc/source/src/editor/mod.rs @@ -655,6 +655,30 @@ bracket_flash: None, self.word_wrap } + /// Current visual tab width in spaces. + #[must_use] + pub fn tab_width(&self) -> usize { + self.tab_width + } + + /// Set the visual tab width in spaces. Used by Tab-to-indent + /// and by the word-wrap renderer. + pub fn set_tab_width(&mut self, width: usize) { + self.tab_width = width.max(1); + } + + /// Cycle through the common tab widths (2 → 4 → 8 → 2). Returns + /// the new width. Bound to Alt-T. + pub fn cycle_tab_width(&mut self) -> usize { + let next = match self.tab_width { + w if w <= 2 => 4, + w if w <= 4 => 8, + _ => 2, + }; + self.tab_width = next; + next + } + /// Whether auto-indent is enabled on Enter. #[must_use] pub fn auto_indent(&self) -> bool { @@ -3240,5 +3264,45 @@ mod tests { // contains a non-whitespace byte → not in indent zone → literal tab. assert_eq!(e.buffer().as_string(), "foo\t bar"); } + + #[test] + fn tab_width_default_is_four() { + let e = make_empty(); + assert_eq!(e.tab_width(), 4); + } + + #[test] + fn set_tab_width_updates_field() { + let mut e = make_empty(); + e.set_tab_width(8); + assert_eq!(e.tab_width(), 8); + e.set_tab_width(0); + assert_eq!(e.tab_width(), 1); // clamped to min 1 + } + + #[test] + fn cycle_tab_width_advances_2_4_8_2() { + let mut e = make_empty(); + assert_eq!(e.tab_width(), 4); + assert_eq!(e.cycle_tab_width(), 8); + assert_eq!(e.cycle_tab_width(), 2); + assert_eq!(e.cycle_tab_width(), 4); + } + + #[test] + fn cycle_tab_width_from_unusual_value_resets_to_2() { + let mut e = make_empty(); + e.set_tab_width(16); + assert_eq!(e.cycle_tab_width(), 2); + } + + #[test] + fn tab_to_indent_uses_current_tab_width() { + let mut e = make_empty(); + e.set_tab_width(8); + e.insert_tab_with_indent(); + assert_eq!(e.buffer().as_string(), " "); + assert_eq!(e.cursor.visual_column(), 8); + } }