diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index b138c6690d4b45..fd2f2b20575166 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 _: @@ -21561,34 +21561,59 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) { else:ˇ "}); - // test `except` auto outdents when typed inside `try` block + // 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! {" def main(): try: i = 2 + except: + j = 2 ˇ "}); cx.update_editor(|editor, window, cx| { - editor.handle_input("except:", window, cx); + editor.handle_input("else:", window, cx); }); cx.assert_editor_state(indoc! {" def main(): try: i = 2 - except:ˇ + except: + j = 2 + else:ˇ "}); - // test `else` auto outdents when typed inside `except` block + // test `finally` auto outdents when typed inside `else` block cx.set_state(indoc! {" def main(): try: i = 2 except: j = 2 + else: + k = 2 ˇ "}); cx.update_editor(|editor, window, cx| { - editor.handle_input("else:", window, cx); + editor.handle_input("finally:", window, cx); }); cx.assert_editor_state(indoc! {" def main(): @@ -21596,10 +21621,35 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) { i = 2 except: j = 2 - else:ˇ + else: + k = 2 + finally:ˇ "}); - // test `finally` auto outdents when typed inside `else` block + // 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: @@ -21607,7 +21657,8 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) { except: j = 2 else: - k = 2 + for i in range(n): + pass ˇ "}); cx.update_editor(|editor, window, cx| { @@ -21620,77 +21671,57 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) { except: j = 2 else: - k = 2 + for i in range(n): + pass 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 + // 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 - // except: - // for i in range(n): - // pass - // ˇ + // if i == 2: + // try: + // i = 3 + // ˇ // "}); // cx.update_editor(|editor, window, cx| { - // editor.handle_input("else:", window, cx); + // editor.handle_input("except:", window, cx); // }); // cx.assert_editor_state(indoc! {" // def main(): // try: // i = 2 - // except: - // for i in range(n): - // pass - // else:ˇ + // if i == 2: + // try: + // i = 3 + // except:ˇ // "}); - // TODO: test `finally` auto outdents when typed inside `else` block right after for block + // // test `except` outdents to outer "try" block // cx.set_state(indoc! {" // def main(): // try: // i = 2 - // except: - // j = 2 - // else: - // for i in range(n): - // pass + // if i == 2: + // try: + // i = 3 // ˇ // "}); // cx.update_editor(|editor, window, cx| { - // editor.handle_input("finally:", window, cx); + // editor.handle_input("except:", window, cx); // }); // cx.assert_editor_state(indoc! {" // def main(): // try: // i = 2 - // except: - // j = 2 - // else: - // for i in range(n): - // pass - // finally:ˇ + // if i == 2: + // try: + // i = 3 + // except:ˇ // "}); // test `else` stays at correct indent when typed after `for` block diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index b362a2a982a097..0ad7fb41145992 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2895,7 +2895,12 @@ impl BufferSnapshot { ) -> Option> + '_> { 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 StartPosition { + start: Point, + suffix: 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); @@ -2911,13 +2916,13 @@ impl BufferSnapshot { .collect::>(); let mut indent_ranges = 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; let mut end: Option = None; - let mut outdent: Option = None; - let config = &indent_configs[mat.grammar_index]; + 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())); @@ -2927,21 +2932,18 @@ 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())); + } 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(), + }); } } 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; @@ -2979,24 +2981,26 @@ 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; } } + 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) @@ -3016,6 +3020,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; + } + } }, ); @@ -3025,6 +3052,7 @@ impl BufferSnapshot { } else { row_range.start.saturating_sub(1) }; + 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); @@ -3062,17 +3090,17 @@ 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); } } + 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 .iter() .any(|e| e.start.row < row && e.end > row_start); diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 8b8c411366f027..cc9558a8c54c18 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")] @@ -717,6 +713,12 @@ pub struct LanguageConfig { #[serde(default, deserialize_with = "deserialize_regex")] #[schemars(schema_with = "regex_json_schema")] pub decrease_indent_pattern: Option, + /// 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 /// bracket when they immediately precede the point where an opening /// bracket is inserted. @@ -776,6 +778,15 @@ pub struct LanguageConfig { pub documentation: Option, } +#[derive(Clone, Debug, Deserialize, Default, JsonSchema)] +pub struct DecreaseIndentConfig { + #[serde(default, deserialize_with = "deserialize_regex")] + #[schemars(schema_with = "regex_json_schema")] + pub pattern: Option, + #[serde(default)] + pub valid_after: 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 +910,7 @@ impl Default for LanguageConfig { auto_indent_on_paste: None, increase_indent_pattern: Default::default(), decrease_indent_pattern: Default::default(), + decrease_indent_patterns: Default::default(), autoclose_before: Default::default(), line_comments: Default::default(), block_comment: Default::default(), @@ -914,7 +926,6 @@ impl Default for LanguageConfig { jsx_tag_auto_close: None, completion_query_characters: Default::default(), debuggers: Default::default(), - significant_indentation: Default::default(), documentation: None, } } @@ -1091,6 +1102,7 @@ struct IndentConfig { start_capture_ix: Option, end_capture_ix: Option, outdent_capture_ix: Option, + suffixed_start_captures: HashMap, } pub struct OutlineConfig { @@ -1476,6 +1488,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, @@ -1483,6 +1503,7 @@ impl Language { start_capture_ix, end_capture_ix, outdent_capture_ix, + suffixed_start_captures, }); } Ok(self) 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 }); diff --git a/crates/languages/src/python/config.toml b/crates/languages/src/python/config.toml index f878cb39665492..6d83d3f3dec6ba 100644 --- a/crates/languages/src/python/config.toml +++ b/crates/languages/src/python/config.toml @@ -28,6 +28,11 @@ brackets = [ auto_indent_using_last_non_empty_line = false debuggers = ["Debugpy"] -significant_indentation = true -increase_indent_pattern = "^\\s*(try)\\b.*:" -decrease_indent_pattern = "^\\s*(else|elif|except|finally)\\b.*:" +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", "except"] }, + { pattern = "^\\s*finally\\b.*:", valid_after = ["try", "except", "else"] }, + { pattern = "^\\s*case\\b.*:", valid_after = ["match", "case"] } +] diff --git a/crates/languages/src/python/indents.scm b/crates/languages/src/python/indents.scm index f306d814350091..617aa706d3177c 100644 --- a/crates/languages/src/python/indents.scm +++ b/crates/languages/src/python/indents.scm @@ -1,72 +1,17 @@ -(_ "(" ")" @end) @indent (_ "[" "]" @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 -) +(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