From 18b9bebd696d92e2ee4350ed280c3ab57eb1b7f4 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Tue, 24 Jun 2025 11:05:31 +0530 Subject: [PATCH 01/19] switch to regex style indent/outdent --- crates/languages/src/python/config.toml | 3 +- crates/languages/src/python/indents.scm | 71 +------------------------ 2 files changed, 2 insertions(+), 72 deletions(-) diff --git a/crates/languages/src/python/config.toml b/crates/languages/src/python/config.toml index f878cb39665492..a2fdeeb746876f 100644 --- a/crates/languages/src/python/config.toml +++ b/crates/languages/src/python/config.toml @@ -28,6 +28,5 @@ brackets = [ auto_indent_using_last_non_empty_line = false debuggers = ["Debugpy"] -significant_indentation = true -increase_indent_pattern = "^\\s*(try)\\b.*:" +increase_indent_pattern = "^[^#].*:\\s*$" decrease_indent_pattern = "^\\s*(else|elif|except|finally)\\b.*:" diff --git a/crates/languages/src/python/indents.scm b/crates/languages/src/python/indents.scm index f306d814350091..112b414aa45f27 100644 --- a/crates/languages/src/python/indents.scm +++ b/crates/languages/src/python/indents.scm @@ -1,72 +1,3 @@ -(_ "(" ")" @end) @indent (_ "[" "]" @end) @indent (_ "{" "}" @end) @indent - -(function_definition - ":" @start - body: (block) @indent -) - -(if_statement - ":" @start - consequence: (block) @indent - alternative: (_)? @outdent -) - -(else_clause - ":" @start - body: (block) @indent -) - -(elif_clause - ":" @start - consequence: (block) @indent -) - -(for_statement - ":" @start - body: (block) @indent -) - -(with_statement - ":" @start - body: (block) @indent -) - -(while_statement - ":" @start - body: (block) @indent -) - -(match_statement - ":" @start - body: (block) @indent -) - -(class_definition - ":" @start - body: (block) @indent -) - -(case_clause - ":" @start - consequence: (block) @indent -) - -(try_statement - ":" @start - body: (block) @indent - (except_clause)? @outdent - (else_clause)? @outdent - (finally_clause)? @outdent -) - -(except_clause - ":" @start - (block) @indent -) - -(finally_clause - ":" @start - (block) @indent -) +(_ "(" ")" @end) @indent From 4acaa9c096d1c8a9d078d66aada7c049177ce1ed Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Tue, 24 Jun 2025 11:51:59 +0530 Subject: [PATCH 02/19] add editor tests --- crates/editor/src/editor_tests.rs | 136 +++++++++++++++--------------- 1 file changed, 68 insertions(+), 68 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index b138c6690d4b45..f8094b79d2cc18 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -21624,74 +21624,74 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) { finally:ˇ "}); - // TODO: test `except` auto outdents when typed inside `try` block right after for block - // cx.set_state(indoc! {" - // def main(): - // try: - // for i in range(n): - // pass - // ˇ - // "}); - // cx.update_editor(|editor, window, cx| { - // editor.handle_input("except:", window, cx); - // }); - // cx.assert_editor_state(indoc! {" - // def main(): - // try: - // for i in range(n): - // pass - // except:ˇ - // "}); - - // TODO: test `else` auto outdents when typed inside `except` block right after for block - // cx.set_state(indoc! {" - // def main(): - // try: - // i = 2 - // except: - // for i in range(n): - // pass - // ˇ - // "}); - // cx.update_editor(|editor, window, cx| { - // editor.handle_input("else:", window, cx); - // }); - // cx.assert_editor_state(indoc! {" - // def main(): - // try: - // i = 2 - // except: - // for i in range(n): - // pass - // else:ˇ - // "}); - - // TODO: test `finally` auto outdents when typed inside `else` block right after for block - // cx.set_state(indoc! {" - // def main(): - // try: - // i = 2 - // except: - // j = 2 - // else: - // for i in range(n): - // pass - // ˇ - // "}); - // cx.update_editor(|editor, window, cx| { - // editor.handle_input("finally:", window, cx); - // }); - // cx.assert_editor_state(indoc! {" - // def main(): - // try: - // i = 2 - // except: - // j = 2 - // else: - // for i in range(n): - // pass - // finally:ˇ - // "}); + // test `except` auto outdents when typed inside `try` block right after for block + cx.set_state(indoc! {" + def main(): + try: + for i in range(n): + pass + ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.handle_input("except:", window, cx); + }); + cx.assert_editor_state(indoc! {" + def main(): + try: + for i in range(n): + pass + except:ˇ + "}); + + // test `else` does not outdents when typed inside `except` block right after for block + cx.set_state(indoc! {" + def main(): + try: + i = 2 + except: + for i in range(n): + pass + ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.handle_input("else:", window, cx); + }); + cx.assert_editor_state(indoc! {" + def main(): + try: + i = 2 + except: + for i in range(n): + pass + else:ˇ + "}); + + // test `finally` auto outdents when typed inside `else` block right after for block + cx.set_state(indoc! {" + def main(): + try: + i = 2 + except: + j = 2 + else: + for i in range(n): + pass + ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.handle_input("finally:", window, cx); + }); + cx.assert_editor_state(indoc! {" + def main(): + try: + i = 2 + except: + j = 2 + else: + for i in range(n): + pass + finally:ˇ + "}); // test `else` stays at correct indent when typed after `for` block cx.set_state(indoc! {" From 5afa1e62e6379c6e7c62d5239923b0952b902b30 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Tue, 24 Jun 2025 14:41:31 +0530 Subject: [PATCH 03/19] feat. outdent rules --- crates/editor/src/editor_tests.rs | 4 +- crates/language/src/buffer.rs | 61 +++++++++++++++++++++---- crates/language/src/language.rs | 17 +++++++ crates/languages/src/python/config.toml | 11 ++++- crates/languages/src/python/indents.scm | 14 ++++++ 5 files changed, 95 insertions(+), 12 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index f8094b79d2cc18..f2eb213fd96553 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -21494,9 +21494,9 @@ async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestApp cx.set_state(indoc! {" def main(): ˇ try: - ˇ fetch() + ˇ fetch() ˇ except ValueError: - ˇ handle_error() + ˇ handle_error() ˇ else: ˇ match value: ˇ case _: diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index b362a2a982a097..9d6c31083fa936 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2897,6 +2897,12 @@ impl BufferSnapshot { let prev_non_blank_row = self.prev_non_blank_row(row_range.start); let significant_indentation = config.significant_indentation; + #[derive(Debug, Clone)] + struct IndentBlock { + start_point: Point, + block_type: SharedString, + } + // Find the suggested indentation ranges based on the syntax tree. let start = Point::new(prev_non_blank_row.unwrap_or(row_range.start), 0); let end = Point::new(row_range.end, 0); @@ -2904,20 +2910,18 @@ impl BufferSnapshot { let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| { Some(&grammar.indents_config.as_ref()?.query) }); - let indent_configs = matches - .grammars() - .iter() - .map(|grammar| grammar.indents_config.as_ref().unwrap()) - .collect::>(); + let grammars = matches.grammars().iter().cloned().collect::>(); let mut indent_ranges = Vec::>::new(); + let mut typed_indent_blocks = Vec::::new(); let mut outdent_positions = Vec::::new(); while let Some(mat) = matches.peek() { let mut start: Option = None; let mut end: Option = None; let mut outdent: Option = None; - let config = &indent_configs[mat.grammar_index]; + let grammar = &grammars[mat.grammar_index]; + let config = grammar.indents_config.as_ref().unwrap(); for capture in mat.captures { if capture.index == config.indent_capture_ix { start.get_or_insert(Point::from_ts_point(capture.node.start_position())); @@ -2930,6 +2934,16 @@ impl BufferSnapshot { let point = Point::from_ts_point(capture.node.start_position()); outdent.get_or_insert(point); outdent_positions.push(point); + } else { + let capture_name = &config.query.capture_names()[capture.index as usize]; + if let Some(block_type) = capture_name.strip_prefix("indent.") { + if !block_type.is_empty() { + typed_indent_blocks.push(IndentBlock { + start_point: Point::from_ts_point(capture.node.start_position()), + block_type: block_type.to_string().into(), + }); + } + } } } @@ -2955,6 +2969,8 @@ impl BufferSnapshot { } } + typed_indent_blocks.sort_by_key(|b| b.start_point); + let mut error_ranges = Vec::>::new(); let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| { grammar.error_query.as_ref() @@ -3029,6 +3045,28 @@ impl BufferSnapshot { Some(row_range.map(move |row| { let row_start = Point::new(row, self.indent_size_for_line(row).len); + let mut from_new_logic = false; + let new_logic_basis_row = if !config.outdent_rules.is_empty() { + let mut basis_row = None; + let line_range = Point::new(row, 0)..Point::new(row, self.line_len(row)); + let line = self.text_for_range(line_range).collect::(); + for rule in &config.outdent_rules { + if rule.regex.as_ref().map_or(false, |r| r.is_match(&line)) { + if let Some(parent_block) = typed_indent_blocks.iter().rfind(|block| { + block.start_point.row < row + && rule.parents.iter().any(|p| p == block.block_type.as_ref()) + }) { + basis_row = Some(parent_block.start_point.row); + from_new_logic = true; + } + break; + } + } + basis_row + } else { + None + }; + let mut indent_from_prev_row = false; let mut outdent_from_prev_row = false; let mut outdent_to_row = u32::MAX; @@ -3077,8 +3115,15 @@ impl BufferSnapshot { .iter() .any(|e| e.start.row < row && e.end > row_start); - let suggestion = if outdent_to_row == prev_row - || (outdent_from_prev_row && indent_from_prev_row) + let from_regex = from_regex || from_new_logic; + + let suggestion = if let Some(basis_row) = new_logic_basis_row { + Some(IndentSuggestion { + basis_row, + delta: Ordering::Equal, + within_error: within_error && !from_regex, + }) + } else if outdent_to_row == prev_row || (outdent_from_prev_row && indent_from_prev_row) { Some(IndentSuggestion { basis_row: prev_row, diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 8b8c411366f027..dd0ab419a7b2c1 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -717,6 +717,9 @@ pub struct LanguageConfig { #[serde(default, deserialize_with = "deserialize_regex")] #[schemars(schema_with = "regex_json_schema")] pub decrease_indent_pattern: Option, + /// TODO: comment here + #[serde(default)] + pub outdent_rules: Vec, /// A list of characters that trigger the automatic insertion of a closing /// bracket when they immediately precede the point where an opening /// bracket is inserted. @@ -776,6 +779,19 @@ pub struct LanguageConfig { pub documentation: Option, } +#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)] +pub struct OutdentRule { + #[serde( + default, + serialize_with = "serialize_regex", + deserialize_with = "deserialize_regex" + )] + #[schemars(schema_with = "regex_json_schema")] + pub regex: Option, + #[serde(default)] + pub parents: Vec, +} + #[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)] pub struct LanguageMatcher { /// Given a list of `LanguageConfig`'s, the language of a file can be determined based on the path extension matching any of the `path_suffixes`. @@ -899,6 +915,7 @@ impl Default for LanguageConfig { auto_indent_on_paste: None, increase_indent_pattern: Default::default(), decrease_indent_pattern: Default::default(), + outdent_rules: Default::default(), autoclose_before: Default::default(), line_comments: Default::default(), block_comment: Default::default(), diff --git a/crates/languages/src/python/config.toml b/crates/languages/src/python/config.toml index a2fdeeb746876f..7712f67285a499 100644 --- a/crates/languages/src/python/config.toml +++ b/crates/languages/src/python/config.toml @@ -28,5 +28,12 @@ brackets = [ auto_indent_using_last_non_empty_line = false debuggers = ["Debugpy"] -increase_indent_pattern = "^[^#].*:\\s*$" -decrease_indent_pattern = "^\\s*(else|elif|except|finally)\\b.*:" +# increase_indent_pattern = "^[^#].*:\\s*$" +# decrease_indent_pattern = "^\\s*(else|elif|except|finally)\\b.*:" +outdent_rules = [ + { regex = "^\\s*elif\\b", parents = ["if", "elif"] }, + { regex = "^\\s*else\\b", parents = ["if", "elif", "for", "while", "try"] }, + { regex = "^\\s*except\\b", parents = ["try", "except"] }, + { regex = "^\\s*finally\\b",parents = ["try", "except"] }, + { regex = "^\\s*case\\b", parents = ["case"] }, +] diff --git a/crates/languages/src/python/indents.scm b/crates/languages/src/python/indents.scm index 112b414aa45f27..9c876b1d052262 100644 --- a/crates/languages/src/python/indents.scm +++ b/crates/languages/src/python/indents.scm @@ -1,3 +1,17 @@ (_ "[" "]" @end) @indent (_ "{" "}" @end) @indent (_ "(" ")" @end) @indent + +(function_definition) @indent.def +(class_definition) @indent.class +(if_statement) @indent.if +(for_statement) @indent.for +(while_statement) @indent.while +(with_statement) @indent.with +(match_statement) @indent.match +(try_statement) @indent.try +(elif_clause) @indent.elif +(else_clause) @indent.else +(except_clause) @indent.except +(finally_clause) @indent.finally +(case_pattern) @indent.case From 27a72e80e235c4ddb7ec3c2a2b5726af48d54b9a Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Tue, 24 Jun 2025 18:27:45 +0530 Subject: [PATCH 04/19] test --- crates/language/src/buffer.rs | 33 ++++++++++++++++++------- crates/languages/src/python/config.toml | 10 ++++---- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 9d6c31083fa936..8083236e429fa1 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2893,6 +2893,8 @@ impl BufferSnapshot { &self, row_range: Range, ) -> Option> + '_> { + dbg!(&row_range); + let config = &self.language.as_ref()?.config; let prev_non_blank_row = self.prev_non_blank_row(row_range.start); let significant_indentation = config.significant_indentation; @@ -2922,7 +2924,9 @@ impl BufferSnapshot { let grammar = &grammars[mat.grammar_index]; let config = grammar.indents_config.as_ref().unwrap(); + dbg!(mat.captures.len()); for capture in mat.captures { + dbg!(&capture.index); if capture.index == config.indent_capture_ix { start.get_or_insert(Point::from_ts_point(capture.node.start_position())); end.get_or_insert(Point::from_ts_point(capture.node.end_position())); @@ -2934,15 +2938,18 @@ impl BufferSnapshot { let point = Point::from_ts_point(capture.node.start_position()); outdent.get_or_insert(point); outdent_positions.push(point); - } else { - let capture_name = &config.query.capture_names()[capture.index as usize]; - if let Some(block_type) = capture_name.strip_prefix("indent.") { - if !block_type.is_empty() { - typed_indent_blocks.push(IndentBlock { - start_point: Point::from_ts_point(capture.node.start_position()), - block_type: block_type.to_string().into(), - }); - } + } + + dbg!(&config.query.capture_names()); + + let capture_name = &config.query.capture_names()[capture.index as usize]; + dbg!(&capture_name); + if let Some(block_type) = capture_name.strip_prefix("indent.") { + if !block_type.is_empty() { + typed_indent_blocks.push(IndentBlock { + start_point: Point::from_ts_point(capture.node.start_position()), + block_type: block_type.to_string().into(), + }); } } } @@ -3041,6 +3048,9 @@ impl BufferSnapshot { } else { row_range.start.saturating_sub(1) }; + + dbg!(&typed_indent_blocks); + let mut prev_row_start = Point::new(prev_row, self.indent_size_for_line(prev_row).len); Some(row_range.map(move |row| { let row_start = Point::new(row, self.indent_size_for_line(row).len); @@ -3052,7 +3062,11 @@ impl BufferSnapshot { let line = self.text_for_range(line_range).collect::(); for rule in &config.outdent_rules { if rule.regex.as_ref().map_or(false, |r| r.is_match(&line)) { + println!("matched"); + dbg!(&rule.regex, &line); if let Some(parent_block) = typed_indent_blocks.iter().rfind(|block| { + dbg!(&block); + block.start_point.row < row && rule.parents.iter().any(|p| p == block.block_type.as_ref()) }) { @@ -3118,6 +3132,7 @@ impl BufferSnapshot { let from_regex = from_regex || from_new_logic; let suggestion = if let Some(basis_row) = new_logic_basis_row { + dbg!(&row, &basis_row); Some(IndentSuggestion { basis_row, delta: Ordering::Equal, diff --git a/crates/languages/src/python/config.toml b/crates/languages/src/python/config.toml index 7712f67285a499..deca6d9546c188 100644 --- a/crates/languages/src/python/config.toml +++ b/crates/languages/src/python/config.toml @@ -31,9 +31,9 @@ debuggers = ["Debugpy"] # increase_indent_pattern = "^[^#].*:\\s*$" # decrease_indent_pattern = "^\\s*(else|elif|except|finally)\\b.*:" outdent_rules = [ - { regex = "^\\s*elif\\b", parents = ["if", "elif"] }, - { regex = "^\\s*else\\b", parents = ["if", "elif", "for", "while", "try"] }, - { regex = "^\\s*except\\b", parents = ["try", "except"] }, - { regex = "^\\s*finally\\b",parents = ["try", "except"] }, - { regex = "^\\s*case\\b", parents = ["case"] }, + { regex = "^\\s*elif\\b.*:", parents = ["if", "elif"] }, + { regex = "^\\s*else\\b.*:", parents = ["if", "elif", "for", "while", "try"] }, + { regex = "^\\s*except\\b.*:", parents = ["try", "except"] }, + { regex = "^\\s*finally\\b.*:",parents = ["try", "except", "else"] }, + { regex = "^\\s*case\\b.*:", parents = ["match", "case"] } ] From a06d3bb314e9e0785f2a33ed1153fb813722bb9a Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Tue, 24 Jun 2025 19:02:24 +0530 Subject: [PATCH 05/19] choose right outdent --- crates/language/src/buffer.rs | 7 ++++--- crates/languages/src/python/indents.scm | 27 +++++++++++++------------ 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 8083236e429fa1..7ad977e5db75d0 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2976,6 +2976,8 @@ impl BufferSnapshot { } } + dbg!(&indent_ranges); + typed_indent_blocks.sort_by_key(|b| b.start_point); let mut error_ranges = Vec::>::new(); @@ -3062,12 +3064,11 @@ impl BufferSnapshot { let line = self.text_for_range(line_range).collect::(); for rule in &config.outdent_rules { if rule.regex.as_ref().map_or(false, |r| r.is_match(&line)) { - println!("matched"); dbg!(&rule.regex, &line); + let current_line_indent = self.indent_size_for_line(row).len; if let Some(parent_block) = typed_indent_blocks.iter().rfind(|block| { - dbg!(&block); - block.start_point.row < row + && block.start_point.column <= current_line_indent && rule.parents.iter().any(|p| p == block.block_type.as_ref()) }) { basis_row = Some(parent_block.start_point.row); diff --git a/crates/languages/src/python/indents.scm b/crates/languages/src/python/indents.scm index 9c876b1d052262..e78515e53f31ce 100644 --- a/crates/languages/src/python/indents.scm +++ b/crates/languages/src/python/indents.scm @@ -2,16 +2,17 @@ (_ "{" "}" @end) @indent (_ "(" ")" @end) @indent -(function_definition) @indent.def -(class_definition) @indent.class -(if_statement) @indent.if -(for_statement) @indent.for -(while_statement) @indent.while -(with_statement) @indent.with -(match_statement) @indent.match -(try_statement) @indent.try -(elif_clause) @indent.elif -(else_clause) @indent.else -(except_clause) @indent.except -(finally_clause) @indent.finally -(case_pattern) @indent.case +(function_definition) @indent @indent.def +(class_definition) @indent @indent.class +(if_statement) @indent @indent.if +(for_statement) @indent @indent.for +(while_statement) @indent @indent.while +(with_statement) @indent @indent.with +(match_statement) @indent @indent.match +(try_statement) @indent @indent.try + +(elif_clause) @indent @indent.elif +(else_clause) @indent @indent.else +(except_clause) @indent @indent.except +(finally_clause) @indent @indent.finally +(case_pattern) @indent @indent.case From b02e723b32189707a5ecec84811d8e32af1fb9b5 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Tue, 24 Jun 2025 19:35:08 +0530 Subject: [PATCH 06/19] clean up --- crates/language/src/buffer.rs | 13 ++++++++----- crates/language/src/language.rs | 18 +++++++----------- crates/languages/src/python/config.toml | 15 +++++++-------- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 7ad977e5db75d0..b3cd4fd9733c1e 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -3058,18 +3058,21 @@ impl BufferSnapshot { let row_start = Point::new(row, self.indent_size_for_line(row).len); let mut from_new_logic = false; - let new_logic_basis_row = if !config.outdent_rules.is_empty() { + let new_logic_basis_row = if !config.decrease_indent_patterns.is_empty() { let mut basis_row = None; let line_range = Point::new(row, 0)..Point::new(row, self.line_len(row)); let line = self.text_for_range(line_range).collect::(); - for rule in &config.outdent_rules { - if rule.regex.as_ref().map_or(false, |r| r.is_match(&line)) { - dbg!(&rule.regex, &line); + for rule in &config.decrease_indent_patterns { + if rule.pattern.as_ref().map_or(false, |r| r.is_match(&line)) { + dbg!(&rule.pattern, &line); let current_line_indent = self.indent_size_for_line(row).len; if let Some(parent_block) = typed_indent_blocks.iter().rfind(|block| { block.start_point.row < row && block.start_point.column <= current_line_indent - && rule.parents.iter().any(|p| p == block.block_type.as_ref()) + && rule + .valid_after + .iter() + .any(|p| p == block.block_type.as_ref()) }) { basis_row = Some(parent_block.start_point.row); from_new_logic = true; diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index dd0ab419a7b2c1..acec80ae682068 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -719,7 +719,7 @@ pub struct LanguageConfig { pub decrease_indent_pattern: Option, /// TODO: comment here #[serde(default)] - pub outdent_rules: Vec, + pub decrease_indent_patterns: Vec, /// A list of characters that trigger the automatic insertion of a closing /// bracket when they immediately precede the point where an opening /// bracket is inserted. @@ -779,17 +779,13 @@ pub struct LanguageConfig { pub documentation: Option, } -#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)] -pub struct OutdentRule { - #[serde( - default, - serialize_with = "serialize_regex", - deserialize_with = "deserialize_regex" - )] +#[derive(Clone, Debug, Deserialize, Default, JsonSchema)] +pub struct DecreaseIndentConfig { + #[serde(default, deserialize_with = "deserialize_regex")] #[schemars(schema_with = "regex_json_schema")] - pub regex: Option, + pub pattern: Option, #[serde(default)] - pub parents: Vec, + pub valid_after: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)] @@ -915,7 +911,7 @@ impl Default for LanguageConfig { auto_indent_on_paste: None, increase_indent_pattern: Default::default(), decrease_indent_pattern: Default::default(), - outdent_rules: Default::default(), + decrease_indent_patterns: Default::default(), autoclose_before: Default::default(), line_comments: Default::default(), block_comment: Default::default(), diff --git a/crates/languages/src/python/config.toml b/crates/languages/src/python/config.toml index deca6d9546c188..8bfff7a93a401d 100644 --- a/crates/languages/src/python/config.toml +++ b/crates/languages/src/python/config.toml @@ -28,12 +28,11 @@ brackets = [ auto_indent_using_last_non_empty_line = false debuggers = ["Debugpy"] -# increase_indent_pattern = "^[^#].*:\\s*$" -# decrease_indent_pattern = "^\\s*(else|elif|except|finally)\\b.*:" -outdent_rules = [ - { regex = "^\\s*elif\\b.*:", parents = ["if", "elif"] }, - { regex = "^\\s*else\\b.*:", parents = ["if", "elif", "for", "while", "try"] }, - { regex = "^\\s*except\\b.*:", parents = ["try", "except"] }, - { regex = "^\\s*finally\\b.*:",parents = ["try", "except", "else"] }, - { regex = "^\\s*case\\b.*:", parents = ["match", "case"] } +increase_indent_pattern = "^[^#].*:\\s*$" +decrease_indent_patterns = [ + { pattern = "^\\s*elif\\b.*:", valid_after = ["if", "elif"] }, + { pattern = "^\\s*else\\b.*:", valid_after = ["if", "elif", "for", "while", "except"] }, + { pattern = "^\\s*except\\b.*:", valid_after = ["try"] }, + { pattern = "^\\s*finally\\b.*:", valid_after = ["try", "except", "else"] }, + { pattern = "^\\s*case\\b.*:", valid_after = ["case"] } ] From 045476f80f4356a25dfdc3cd53a782ecf7431980 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Tue, 24 Jun 2025 19:50:10 +0530 Subject: [PATCH 07/19] remove significant indentation --- crates/language/src/buffer.rs | 58 ++++++++------------------------- crates/language/src/language.rs | 5 --- 2 files changed, 14 insertions(+), 49 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index b3cd4fd9733c1e..88296bf3ccf72f 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2893,11 +2893,8 @@ impl BufferSnapshot { &self, row_range: Range, ) -> Option> + '_> { - dbg!(&row_range); - let config = &self.language.as_ref()?.config; let prev_non_blank_row = self.prev_non_blank_row(row_range.start); - let significant_indentation = config.significant_indentation; #[derive(Debug, Clone)] struct IndentBlock { @@ -2920,13 +2917,10 @@ impl BufferSnapshot { while let Some(mat) = matches.peek() { let mut start: Option = None; let mut end: Option = None; - let mut outdent: Option = None; let grammar = &grammars[mat.grammar_index]; let config = grammar.indents_config.as_ref().unwrap(); - dbg!(mat.captures.len()); for capture in mat.captures { - dbg!(&capture.index); if capture.index == config.indent_capture_ix { start.get_or_insert(Point::from_ts_point(capture.node.start_position())); end.get_or_insert(Point::from_ts_point(capture.node.end_position())); @@ -2935,15 +2929,9 @@ impl BufferSnapshot { } else if Some(capture.index) == config.end_capture_ix { end = Some(Point::from_ts_point(capture.node.start_position())); } else if Some(capture.index) == config.outdent_capture_ix { - let point = Point::from_ts_point(capture.node.start_position()); - outdent.get_or_insert(point); - outdent_positions.push(point); + outdent_positions.push(Point::from_ts_point(capture.node.start_position()); } - - dbg!(&config.query.capture_names()); - let capture_name = &config.query.capture_names()[capture.index as usize]; - dbg!(&capture_name); if let Some(block_type) = capture_name.strip_prefix("indent.") { if !block_type.is_empty() { typed_indent_blocks.push(IndentBlock { @@ -2955,14 +2943,8 @@ impl BufferSnapshot { } matches.advance(); - // in case of significant indentation expand end to outdent position - let end = if significant_indentation { - outdent.or(end) - } else { - end - }; if let Some((start, end)) = start.zip(end) { - if start.row == end.row && (!significant_indentation || start.column < end.column) { + if start.row == end.row { continue; } let range = start..end; @@ -2976,8 +2958,6 @@ impl BufferSnapshot { } } - dbg!(&indent_ranges); - typed_indent_blocks.sort_by_key(|b| b.start_point); let mut error_ranges = Vec::>::new(); @@ -3004,20 +2984,16 @@ impl BufferSnapshot { matches.advance(); } - // we don't use outdent positions to truncate in case of significant indentation - // rather we use them to expand (handled above) - if !significant_indentation { - outdent_positions.sort(); - for outdent_position in outdent_positions { - // find the innermost indent range containing this outdent_position - // set its end to the outdent position - if let Some(range_to_truncate) = indent_ranges - .iter_mut() - .filter(|indent_range| indent_range.contains(&outdent_position)) - .next_back() - { - range_to_truncate.end = outdent_position; - } + outdent_positions.sort(); + for outdent_position in outdent_positions { + // find the innermost indent range containing this outdent_position + // set its end to the outdent position + if let Some(range_to_truncate) = indent_ranges + .iter_mut() + .filter(|indent_range| indent_range.contains(&outdent_position)) + .next_back() + { + range_to_truncate.end = outdent_position; } } @@ -3118,14 +3094,8 @@ impl BufferSnapshot { if range.start.row == prev_row && range.end > row_start { indent_from_prev_row = true; } - if significant_indentation && self.is_line_blank(row) && range.start.row == prev_row - { - indent_from_prev_row = true; - } - if !significant_indentation || !self.is_line_blank(row) { - if range.end > prev_row_start && range.end <= row_start { - outdent_to_row = outdent_to_row.min(range.start.row); - } + if range.end > prev_row_start && range.end <= row_start { + outdent_to_row = outdent_to_row.min(range.start.row); } } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index acec80ae682068..082c8cf0355573 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -696,10 +696,6 @@ pub struct LanguageConfig { #[serde(default)] #[schemars(schema_with = "bracket_pair_config_json_schema")] pub brackets: BracketPairConfig, - /// If set to true, indicates the language uses significant whitespace/indentation - /// for syntax structure (like Python) rather than brackets/braces for code blocks. - #[serde(default)] - pub significant_indentation: bool, /// If set to true, auto indentation uses last non empty line to determine /// the indentation level for a new line. #[serde(default = "auto_indent_using_last_non_empty_line_default")] @@ -927,7 +923,6 @@ impl Default for LanguageConfig { jsx_tag_auto_close: None, completion_query_characters: Default::default(), debuggers: Default::default(), - significant_indentation: Default::default(), documentation: None, } } From b94020ce851ddc1835e7f2545050a3cbdcd30cfb Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Tue, 24 Jun 2025 19:51:27 +0530 Subject: [PATCH 08/19] typo --- crates/language/src/buffer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 88296bf3ccf72f..ed99bed0e73c04 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2929,7 +2929,7 @@ impl BufferSnapshot { } else if Some(capture.index) == config.end_capture_ix { end = Some(Point::from_ts_point(capture.node.start_position())); } else if Some(capture.index) == config.outdent_capture_ix { - outdent_positions.push(Point::from_ts_point(capture.node.start_position()); + outdent_positions.push(Point::from_ts_point(capture.node.start_position())); } let capture_name = &config.query.capture_names()[capture.index as usize]; if let Some(block_type) = capture_name.strip_prefix("indent.") { From 2017fedaca564bf9ada53fc16efbe15620bfc6d3 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 25 Jun 2025 11:33:46 +0530 Subject: [PATCH 09/19] clean up --- crates/language/src/buffer.rs | 41 +++++++++---------------- crates/language/src/language.rs | 10 ++++++ crates/languages/src/python/indents.scm | 27 ++++++++-------- 3 files changed, 38 insertions(+), 40 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index ed99bed0e73c04..6cd1ec7c51ea0c 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2897,9 +2897,9 @@ impl BufferSnapshot { let prev_non_blank_row = self.prev_non_blank_row(row_range.start); #[derive(Debug, Clone)] - struct IndentBlock { - start_point: Point, - block_type: SharedString, + struct StartPosition { + start: Point, + suffix: SharedString, } // Find the suggested indentation ranges based on the syntax tree. @@ -2912,7 +2912,7 @@ impl BufferSnapshot { let grammars = matches.grammars().iter().cloned().collect::>(); let mut indent_ranges = Vec::>::new(); - let mut typed_indent_blocks = Vec::::new(); + let mut start_positions = Vec::::new(); let mut outdent_positions = Vec::::new(); while let Some(mat) = matches.peek() { let mut start: Option = None; @@ -2930,15 +2930,11 @@ impl BufferSnapshot { end = Some(Point::from_ts_point(capture.node.start_position())); } else if Some(capture.index) == config.outdent_capture_ix { outdent_positions.push(Point::from_ts_point(capture.node.start_position())); - } - let capture_name = &config.query.capture_names()[capture.index as usize]; - if let Some(block_type) = capture_name.strip_prefix("indent.") { - if !block_type.is_empty() { - typed_indent_blocks.push(IndentBlock { - start_point: Point::from_ts_point(capture.node.start_position()), - block_type: block_type.to_string().into(), - }); - } + } else if let Some(suffix) = config.suffixed_start_captures.get(&capture.index) { + start_positions.push(StartPosition { + start: Point::from_ts_point(capture.node.start_position()), + suffix: suffix.clone(), + }); } } @@ -2958,8 +2954,6 @@ impl BufferSnapshot { } } - typed_indent_blocks.sort_by_key(|b| b.start_point); - let mut error_ranges = Vec::>::new(); let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| { grammar.error_query.as_ref() @@ -3027,7 +3021,7 @@ impl BufferSnapshot { row_range.start.saturating_sub(1) }; - dbg!(&typed_indent_blocks); + start_positions.sort_by_key(|b| b.start); let mut prev_row_start = Point::new(prev_row, self.indent_size_for_line(prev_row).len); Some(row_range.map(move |row| { @@ -3040,17 +3034,13 @@ impl BufferSnapshot { let line = self.text_for_range(line_range).collect::(); for rule in &config.decrease_indent_patterns { if rule.pattern.as_ref().map_or(false, |r| r.is_match(&line)) { - dbg!(&rule.pattern, &line); let current_line_indent = self.indent_size_for_line(row).len; - if let Some(parent_block) = typed_indent_blocks.iter().rfind(|block| { - block.start_point.row < row - && block.start_point.column <= current_line_indent - && rule - .valid_after - .iter() - .any(|p| p == block.block_type.as_ref()) + if let Some(parent_block) = start_positions.iter().rfind(|block| { + block.start.row < row + && block.start.column <= current_line_indent + && rule.valid_after.iter().any(|p| p == block.suffix.as_ref()) }) { - basis_row = Some(parent_block.start_point.row); + basis_row = Some(parent_block.start.row); from_new_logic = true; } break; @@ -3106,7 +3096,6 @@ impl BufferSnapshot { let from_regex = from_regex || from_new_logic; let suggestion = if let Some(basis_row) = new_logic_basis_row { - dbg!(&row, &basis_row); Some(IndentSuggestion { basis_row, delta: Ordering::Equal, diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 082c8cf0355573..8388f75bca0e50 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -1099,6 +1099,7 @@ struct IndentConfig { start_capture_ix: Option, end_capture_ix: Option, outdent_capture_ix: Option, + suffixed_start_captures: HashMap, } pub struct OutlineConfig { @@ -1484,6 +1485,14 @@ impl Language { ("outdent", &mut outdent_capture_ix), ], ); + + let mut suffixed_start_captures = HashMap::default(); + for (ix, name) in query.capture_names().iter().enumerate() { + if let Some(suffix) = name.strip_prefix("start.") { + suffixed_start_captures.insert(ix as u32, suffix.to_owned().into()); + } + } + if let Some(indent_capture_ix) = indent_capture_ix { grammar.indents_config = Some(IndentConfig { query, @@ -1491,6 +1500,7 @@ impl Language { start_capture_ix, end_capture_ix, outdent_capture_ix, + suffixed_start_captures, }); } Ok(self) diff --git a/crates/languages/src/python/indents.scm b/crates/languages/src/python/indents.scm index e78515e53f31ce..617aa706d3177c 100644 --- a/crates/languages/src/python/indents.scm +++ b/crates/languages/src/python/indents.scm @@ -2,17 +2,16 @@ (_ "{" "}" @end) @indent (_ "(" ")" @end) @indent -(function_definition) @indent @indent.def -(class_definition) @indent @indent.class -(if_statement) @indent @indent.if -(for_statement) @indent @indent.for -(while_statement) @indent @indent.while -(with_statement) @indent @indent.with -(match_statement) @indent @indent.match -(try_statement) @indent @indent.try - -(elif_clause) @indent @indent.elif -(else_clause) @indent @indent.else -(except_clause) @indent @indent.except -(finally_clause) @indent @indent.finally -(case_pattern) @indent @indent.case +(function_definition) @start.def +(class_definition) @start.class +(if_statement) @start.if +(for_statement) @start.for +(while_statement) @start.while +(with_statement) @start.with +(match_statement) @start.match +(try_statement) @start.try +(elif_clause) @start.elif +(else_clause) @start.else +(except_clause) @start.except +(finally_clause) @start.finally +(case_pattern) @start.case From 5d5ee9cc01f4b625fa1cf8006629197cec1a729d Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 25 Jun 2025 12:05:43 +0530 Subject: [PATCH 10/19] handle tests --- crates/editor/src/editor_tests.rs | 96 ++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 34 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index f2eb213fd96553..d6c1dda1808c87 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -21561,22 +21561,23 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) { else:ˇ "}); + // TODO: blocked on https://github.com/tree-sitter/tree-sitter-python/pull/304 // test `except` auto outdents when typed inside `try` block - cx.set_state(indoc! {" - def main(): - try: - i = 2 - ˇ - "}); - cx.update_editor(|editor, window, cx| { - editor.handle_input("except:", window, cx); - }); - cx.assert_editor_state(indoc! {" - def main(): - try: - i = 2 - except:ˇ - "}); + // cx.set_state(indoc! {" + // def main(): + // try: + // i = 2 + // ˇ + // "}); + // cx.update_editor(|editor, window, cx| { + // editor.handle_input("except:", window, cx); + // }); + // cx.assert_editor_state(indoc! {" + // def main(): + // try: + // i = 2 + // except:ˇ + // "}); // test `else` auto outdents when typed inside `except` block cx.set_state(indoc! {" @@ -21624,25 +21625,6 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) { finally:ˇ "}); - // test `except` auto outdents when typed inside `try` block right after for block - cx.set_state(indoc! {" - def main(): - try: - for i in range(n): - pass - ˇ - "}); - cx.update_editor(|editor, window, cx| { - editor.handle_input("except:", window, cx); - }); - cx.assert_editor_state(indoc! {" - def main(): - try: - for i in range(n): - pass - except:ˇ - "}); - // test `else` does not outdents when typed inside `except` block right after for block cx.set_state(indoc! {" def main(): @@ -21693,6 +21675,52 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) { finally:ˇ "}); + // test `except` outdents to inner "try" block + cx.set_state(indoc! {" + def main(): + try: + i = 2 + if i == 2: + try: + i = 3 + ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.handle_input("except:", window, cx); + }); + cx.assert_editor_state(indoc! {" + def main(): + try: + i = 2 + if i == 2: + try: + i = 3 + except:ˇ + "}); + + // test `except` outdents to outer "try" block + cx.set_state(indoc! {" + def main(): + try: + i = 2 + if i == 2: + try: + i = 3 + ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.handle_input("except:", window, cx); + }); + cx.assert_editor_state(indoc! {" + def main(): + try: + i = 2 + if i == 2: + try: + i = 3 + except:ˇ + "}); + // test `else` stays at correct indent when typed after `for` block cx.set_state(indoc! {" def main(): From 7e9c398b2a7f704a65fb6e4335fcc725f3f379b0 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 25 Jun 2025 12:11:19 +0530 Subject: [PATCH 11/19] tests will be correct after rebase --- crates/editor/src/editor_tests.rs | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index d6c1dda1808c87..f92332f6c1f6a5 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -21561,23 +21561,22 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) { else:ˇ "}); - // TODO: blocked on https://github.com/tree-sitter/tree-sitter-python/pull/304 // test `except` auto outdents when typed inside `try` block - // cx.set_state(indoc! {" - // def main(): - // try: - // i = 2 - // ˇ - // "}); - // cx.update_editor(|editor, window, cx| { - // editor.handle_input("except:", window, cx); - // }); - // cx.assert_editor_state(indoc! {" - // def main(): - // try: - // i = 2 - // except:ˇ - // "}); + cx.set_state(indoc! {" + def main(): + try: + i = 2 + ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.handle_input("except:", window, cx); + }); + cx.assert_editor_state(indoc! {" + def main(): + try: + i = 2 + except:ˇ + "}); // test `else` auto outdents when typed inside `except` block cx.set_state(indoc! {" From ceac15b07d549902b1b8d49b3c76a4e3b45f47a1 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 25 Jun 2025 12:34:50 +0530 Subject: [PATCH 12/19] logic clean up --- crates/language/src/buffer.rs | 51 +++++++++++++---------------------- 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 6cd1ec7c51ea0c..179fca6901006e 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -3027,35 +3027,27 @@ impl BufferSnapshot { Some(row_range.map(move |row| { let row_start = Point::new(row, self.indent_size_for_line(row).len); - let mut from_new_logic = false; - let new_logic_basis_row = if !config.decrease_indent_patterns.is_empty() { - let mut basis_row = None; - let line_range = Point::new(row, 0)..Point::new(row, self.line_len(row)); - let line = self.text_for_range(line_range).collect::(); - for rule in &config.decrease_indent_patterns { - if rule.pattern.as_ref().map_or(false, |r| r.is_match(&line)) { - let current_line_indent = self.indent_size_for_line(row).len; - if let Some(parent_block) = start_positions.iter().rfind(|block| { - block.start.row < row - && block.start.column <= current_line_indent - && rule.valid_after.iter().any(|p| p == block.suffix.as_ref()) - }) { - basis_row = Some(parent_block.start.row); - from_new_logic = true; - } - break; - } - } - basis_row - } else { - None - }; - let mut indent_from_prev_row = false; let mut outdent_from_prev_row = false; let mut outdent_to_row = u32::MAX; let mut from_regex = false; + let line_range = Point::new(row, 0)..Point::new(row, self.line_len(row)); + let line = self.text_for_range(line_range).collect::(); + for rule in &config.decrease_indent_patterns { + if rule.pattern.as_ref().map_or(false, |r| r.is_match(&line)) { + if let Some(basis_row) = start_positions.iter().rfind(|pos| { + pos.start.row < row + && pos.start.column <= row_start.column + && rule.valid_after.iter().any(|p| p == pos.suffix.as_ref()) + }) { + outdent_to_row = basis_row.start.row; + from_regex = true; + } + break; + } + } + while let Some((indent_row, delta)) = indent_changes.peek() { match indent_row.cmp(&row) { Ordering::Equal => match delta { @@ -3093,15 +3085,8 @@ impl BufferSnapshot { .iter() .any(|e| e.start.row < row && e.end > row_start); - let from_regex = from_regex || from_new_logic; - - let suggestion = if let Some(basis_row) = new_logic_basis_row { - Some(IndentSuggestion { - basis_row, - delta: Ordering::Equal, - within_error: within_error && !from_regex, - }) - } else if outdent_to_row == prev_row || (outdent_from_prev_row && indent_from_prev_row) + let suggestion = if outdent_to_row == prev_row + || (outdent_from_prev_row && indent_from_prev_row) { Some(IndentSuggestion { basis_row: prev_row, From 483b6b44cf3f3429add2fa35341160a2fdbf92bc Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 25 Jun 2025 14:14:24 +0530 Subject: [PATCH 13/19] update --- crates/language/src/buffer.rs | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 179fca6901006e..6bd9fcd251f519 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -3032,22 +3032,6 @@ impl BufferSnapshot { let mut outdent_to_row = u32::MAX; let mut from_regex = false; - let line_range = Point::new(row, 0)..Point::new(row, self.line_len(row)); - let line = self.text_for_range(line_range).collect::(); - for rule in &config.decrease_indent_patterns { - if rule.pattern.as_ref().map_or(false, |r| r.is_match(&line)) { - if let Some(basis_row) = start_positions.iter().rfind(|pos| { - pos.start.row < row - && pos.start.column <= row_start.column - && rule.valid_after.iter().any(|p| p == pos.suffix.as_ref()) - }) { - outdent_to_row = basis_row.start.row; - from_regex = true; - } - break; - } - } - while let Some((indent_row, delta)) = indent_changes.peek() { match indent_row.cmp(&row) { Ordering::Equal => match delta { @@ -3081,6 +3065,23 @@ impl BufferSnapshot { } } + let line_range = Point::new(row, 0)..Point::new(row, self.line_len(row)); + let line = self.text_for_range(line_range).collect::(); + for rule in &config.decrease_indent_patterns { + if rule.pattern.as_ref().map_or(false, |r| r.is_match(&line)) { + if let Some(basis_row) = start_positions.iter().rfind(|pos| { + pos.start.row < row + && pos.start.column <= row_start.column + && rule.valid_after.iter().any(|p| p == pos.suffix.as_ref()) + }) { + indent_from_prev_row = false; + outdent_to_row = basis_row.start.row; + from_regex = true; + } + break; + } + } + let within_error = error_ranges .iter() .any(|e| e.start.row < row && e.end > row_start); From 947caa2c45c5f1dd08e1c31a100d1718df2a11e7 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 25 Jun 2025 14:52:30 +0530 Subject: [PATCH 14/19] optimize --- crates/language/src/buffer.rs | 50 +++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 6bd9fcd251f519..7dc45d8b7d9cd2 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2991,7 +2991,13 @@ impl BufferSnapshot { } } + start_positions.sort_by_key(|b| b.start); + // Find the suggested indentation increases and decreased based on regexes. + let mut regex_outdent_map = HashMap::default(); + let mut last_seen_suffix = HashMap::default(); + let mut start_positions_iter = start_positions.iter().peekable(); + let mut indent_change_rows = Vec::<(u32, Ordering)>::new(); self.for_each_line( Point::new(prev_non_blank_row.unwrap_or(row_range.start), 0) @@ -3011,6 +3017,29 @@ impl BufferSnapshot { { indent_change_rows.push((row + 1, Ordering::Greater)); } + while let Some(pos) = start_positions_iter.peek() { + if pos.start.row < row { + let pos = start_positions_iter.next().unwrap(); + last_seen_suffix.insert(pos.suffix.to_string(), pos.start); + } else { + break; + } + } + for rule in &config.decrease_indent_patterns { + if rule.pattern.as_ref().map_or(false, |r| r.is_match(line)) { + let row_start_column = self.indent_size_for_line(row).len; + let basis_row = rule + .valid_after + .iter() + .filter_map(|valid_suffix| last_seen_suffix.get(valid_suffix)) + .filter(|start_point| start_point.column <= row_start_column) + .max_by_key(|start_point| start_point.row); + if let Some(outdent_to_row) = basis_row { + regex_outdent_map.insert(row, outdent_to_row.row); + } + break; + } + } }, ); @@ -3021,8 +3050,6 @@ impl BufferSnapshot { row_range.start.saturating_sub(1) }; - start_positions.sort_by_key(|b| b.start); - let mut prev_row_start = Point::new(prev_row, self.indent_size_for_line(prev_row).len); Some(row_range.map(move |row| { let row_start = Point::new(row, self.indent_size_for_line(row).len); @@ -3065,21 +3092,10 @@ impl BufferSnapshot { } } - let line_range = Point::new(row, 0)..Point::new(row, self.line_len(row)); - let line = self.text_for_range(line_range).collect::(); - for rule in &config.decrease_indent_patterns { - if rule.pattern.as_ref().map_or(false, |r| r.is_match(&line)) { - if let Some(basis_row) = start_positions.iter().rfind(|pos| { - pos.start.row < row - && pos.start.column <= row_start.column - && rule.valid_after.iter().any(|p| p == pos.suffix.as_ref()) - }) { - indent_from_prev_row = false; - outdent_to_row = basis_row.start.row; - from_regex = true; - } - break; - } + if let Some(basis_row) = regex_outdent_map.get(&row) { + indent_from_prev_row = false; + outdent_to_row = *basis_row; + from_regex = true; } let within_error = error_ranges From a5ccc244e2083c0f28de13183d1002ea4272a482 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 25 Jun 2025 14:57:32 +0530 Subject: [PATCH 15/19] docs --- crates/language/src/language.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 8388f75bca0e50..cc9558a8c54c18 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -713,7 +713,10 @@ pub struct LanguageConfig { #[serde(default, deserialize_with = "deserialize_regex")] #[schemars(schema_with = "regex_json_schema")] pub decrease_indent_pattern: Option, - /// TODO: comment here + /// A list of rules for decreasing indentation. Each rule pairs a regex with a set of valid + /// "block-starting" tokens. When a line matches a pattern, its indentation is aligned with + /// the most recent line that began with a corresponding token. This enables context-aware + /// outdenting, like aligning an `else` with its `if`. #[serde(default)] pub decrease_indent_patterns: Vec, /// A list of characters that trigger the automatic insertion of a closing From bc00f491468f3510583f8e18de8455f77c97ae29 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 25 Jun 2025 15:02:21 +0530 Subject: [PATCH 16/19] blocked on --- crates/editor/src/editor_tests.rs | 126 +++++++++++++++--------------- 1 file changed, 65 insertions(+), 61 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index f92332f6c1f6a5..fd2f2b20575166 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -21561,22 +21561,24 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) { else:ˇ "}); - // test `except` auto outdents when typed inside `try` block - cx.set_state(indoc! {" - def main(): - try: - i = 2 - ˇ - "}); - cx.update_editor(|editor, window, cx| { - editor.handle_input("except:", window, cx); - }); - cx.assert_editor_state(indoc! {" - def main(): - try: - i = 2 - except:ˇ - "}); + // blocked on: https://github.com/tree-sitter/tree-sitter-python/pull/304 + // + // // test `except` auto outdents when typed inside `try` block + // cx.set_state(indoc! {" + // def main(): + // try: + // i = 2 + // ˇ + // "}); + // cx.update_editor(|editor, window, cx| { + // editor.handle_input("except:", window, cx); + // }); + // cx.assert_editor_state(indoc! {" + // def main(): + // try: + // i = 2 + // except:ˇ + // "}); // test `else` auto outdents when typed inside `except` block cx.set_state(indoc! {" @@ -21674,51 +21676,53 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) { finally:ˇ "}); - // test `except` outdents to inner "try" block - cx.set_state(indoc! {" - def main(): - try: - i = 2 - if i == 2: - try: - i = 3 - ˇ - "}); - cx.update_editor(|editor, window, cx| { - editor.handle_input("except:", window, cx); - }); - cx.assert_editor_state(indoc! {" - def main(): - try: - i = 2 - if i == 2: - try: - i = 3 - except:ˇ - "}); - - // test `except` outdents to outer "try" block - cx.set_state(indoc! {" - def main(): - try: - i = 2 - if i == 2: - try: - i = 3 - ˇ - "}); - cx.update_editor(|editor, window, cx| { - editor.handle_input("except:", window, cx); - }); - cx.assert_editor_state(indoc! {" - def main(): - try: - i = 2 - if i == 2: - try: - i = 3 - except:ˇ - "}); + // blocked on: https://github.com/tree-sitter/tree-sitter-python/pull/304 + // + // // test `except` outdents to inner "try" block + // cx.set_state(indoc! {" + // def main(): + // try: + // i = 2 + // if i == 2: + // try: + // i = 3 + // ˇ + // "}); + // cx.update_editor(|editor, window, cx| { + // editor.handle_input("except:", window, cx); + // }); + // cx.assert_editor_state(indoc! {" + // def main(): + // try: + // i = 2 + // if i == 2: + // try: + // i = 3 + // except:ˇ + // "}); + + // // test `except` outdents to outer "try" block + // cx.set_state(indoc! {" + // def main(): + // try: + // i = 2 + // if i == 2: + // try: + // i = 3 + // ˇ + // "}); + // cx.update_editor(|editor, window, cx| { + // editor.handle_input("except:", window, cx); + // }); + // cx.assert_editor_state(indoc! {" + // def main(): + // try: + // i = 2 + // if i == 2: + // try: + // i = 3 + // except:ˇ + // "}); // test `else` stays at correct indent when typed after `for` block cx.set_state(indoc! {" From bb28576487f962705fc17a708e8c00cce7a3edc7 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 25 Jun 2025 15:33:06 +0530 Subject: [PATCH 17/19] no clone --- crates/language/src/buffer.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 7dc45d8b7d9cd2..0ad7fb41145992 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2909,7 +2909,11 @@ impl BufferSnapshot { let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| { Some(&grammar.indents_config.as_ref()?.query) }); - let grammars = matches.grammars().iter().cloned().collect::>(); + let indent_configs = matches + .grammars() + .iter() + .map(|grammar| grammar.indents_config.as_ref().unwrap()) + .collect::>(); let mut indent_ranges = Vec::>::new(); let mut start_positions = Vec::::new(); @@ -2918,8 +2922,7 @@ impl BufferSnapshot { let mut start: Option = None; let mut end: Option = None; - let grammar = &grammars[mat.grammar_index]; - let config = grammar.indents_config.as_ref().unwrap(); + let config = indent_configs[mat.grammar_index]; for capture in mat.captures { if capture.index == config.indent_capture_ix { start.get_or_insert(Point::from_ts_point(capture.node.start_position())); From edcd9b0230cd2e053ede9e660eb6a906fba7a438 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 25 Jun 2025 16:15:16 +0530 Subject: [PATCH 18/19] fix test --- crates/languages/src/python.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 03b1b749c1b81b..dc6996d3999a0d 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -1395,7 +1395,7 @@ mod tests { // dedent "else" on the line after a closing paren append(&mut buffer, "\n else:\n", cx); - assert_eq!(buffer.text(), "if a:\n b(\n )\nelse:\n"); + assert_eq!(buffer.text(), "if a:\n b(\n )\nelse:\n "); buffer }); From 94ac38bf313905d97dfc5d277b37af998cf54338 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 25 Jun 2025 16:24:59 +0530 Subject: [PATCH 19/19] add missing valid after --- crates/languages/src/python/config.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/languages/src/python/config.toml b/crates/languages/src/python/config.toml index 8bfff7a93a401d..6d83d3f3dec6ba 100644 --- a/crates/languages/src/python/config.toml +++ b/crates/languages/src/python/config.toml @@ -32,7 +32,7 @@ increase_indent_pattern = "^[^#].*:\\s*$" decrease_indent_patterns = [ { pattern = "^\\s*elif\\b.*:", valid_after = ["if", "elif"] }, { pattern = "^\\s*else\\b.*:", valid_after = ["if", "elif", "for", "while", "except"] }, - { pattern = "^\\s*except\\b.*:", valid_after = ["try"] }, + { pattern = "^\\s*except\\b.*:", valid_after = ["try", "except"] }, { pattern = "^\\s*finally\\b.*:", valid_after = ["try", "except", "else"] }, - { pattern = "^\\s*case\\b.*:", valid_after = ["case"] } + { pattern = "^\\s*case\\b.*:", valid_after = ["match", "case"] } ]