From b39097cd3fc1908e05017309f17b1189fab4df00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 12 Oct 2026 16:58:50 -0400 Subject: [PATCH] documents: Shift-Tab takes four columns out of any mix of spaces and tabs --- src/documents/document/lists.rs | 29 ++++++++++++++++++++++----- src/documents/document/tests/tools.rs | 8 ++++++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/documents/document/lists.rs b/src/documents/document/lists.rs index 25e23b1..fd10476 100644 --- a/src/documents/document/lists.rs +++ b/src/documents/document/lists.rs @@ -91,6 +91,22 @@ fn ordered(rest: &str) -> Option<(String, usize)> { Some((format!("{}{close}", number + 1), digits + 1)) } +/// Leading bytes that make one step, tabs to stops of four. +fn outdent(line: &str) -> usize { + let mut columns = 0; + for (bytes, ch) in line.char_indices() { + if columns >= STEP { + return bytes; + } + columns = match ch { + ' ' => columns + 1, + '\t' => (columns / STEP + 1) * STEP, + _ => return bytes, + }; + } + line.len() +} + /// Lines `range` touches, a step in or out. pub(super) fn shift(text: &str, range: Range, deeper: bool) -> (String, Range) { let first = text[..range.start].rfind('\n').map_or(0, |at| at + 1); @@ -102,13 +118,12 @@ pub(super) fn shift(text: &str, range: Range, deeper: bool) -> (String, R let mut at = first; loop { let line = &text[at..text[at..].find('\n').map_or(text.len(), |end| at + end)]; - let spaces = line.len() - line.trim_start_matches(' ').len(); match deeper { true => changes.push((at, STEP as isize)), - // A leading tab is one step out. - false if line.starts_with('\t') => changes.push((at, -1)), - false if spaces > 0 => changes.push((at, -(spaces.min(STEP) as isize))), - false => {} + false => match outdent(line) { + 0 => {} + bytes => changes.push((at, -(bytes as isize))), + }, } match text[at..].find('\n') { Some(end) if at + end < last => at += end + 1, @@ -276,5 +291,9 @@ mod tests { assert_eq!(shift("a\nb", 1..3, true), (" a\n b".into(), 5..11)); assert_eq!(shift(" a", 7..7, false), (" a".into(), 3..3)); assert_eq!(shift(" a\nb", 1..1, false), ("a\nb".into(), 0..0)); + // Spaces and tabs mixed: four columns go. + assert_eq!(shift(" \t- b", 6..6, false), ("- b".into(), 3..3)); + assert_eq!(shift("\t - b", 6..6, false), (" - b".into(), 5..5)); + assert_eq!(shift(" \t\t- b", 6..6, false), ("\t- b".into(), 4..4)); } } diff --git a/src/documents/document/tests/tools.rs b/src/documents/document/tests/tools.rs index 6be0fea..a429a7e 100644 --- a/src/documents/document/tests/tools.rs +++ b/src/documents/document/tests/tools.rs @@ -223,6 +223,14 @@ fn shift_tab_takes_a_tab_indent_out(cx: &mut TestAppContext) { assert_eq!(opened(&document, cx), "- a\n- b"); } +#[gpui::test] +fn shift_tab_takes_spaces_then_a_tab_out(cx: &mut TestAppContext) { + let (document, cx) = open("- a\n \t- b\n", MarkdownMode::Visual, cx); + press(0, Modifiers::none(), cx); + cx.simulate_keystrokes("shift-tab"); + assert_eq!(opened(&document, cx), "- a\n- b"); +} + #[gpui::test] fn a_table_on_an_indented_blank_line_is_a_table(cx: &mut TestAppContext) { let (document, cx) = open(