tlc: Sprint 2 B4 — configurable tab width via Alt-T cycle

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).
This commit is contained in:
kellito
2026-07-05 16:59:18 +03:00
parent 2633c40dfd
commit d4ddc4e2c7
2 changed files with 69 additions and 0 deletions
@@ -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);
@@ -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);
}
}