From 7c76cee16d4818c8695ff364d901ed0739fd583b Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 7 May 2025 23:05:42 +0530 Subject: [PATCH] language: Fix indent suggestions for significant indented languages like Python (#29625) Closes #26157 This fixes multiple cases where Python indentation breaks: - [x] Adding a new line after `if`, `try`, etc. correctly indents in that scope - [x] Multi-cursor tabs correctly preserve relative indents - [x] Adding a new line after `else`, `finally`, etc. correctly outdents them - [x] Existing Tests Future Todo: I need to add new tests for all the above cases. Before/After: 1. Multi-cursor tabs correctly preserve relative indents https://github.com/user-attachments/assets/08a46ddf-5371-4e26-ae7d-f8aa0b31c4a2 2. Adding a new line after `if`, `try`, etc. correctly indents in that scope https://github.com/user-attachments/assets/9affae97-1a50-43c9-9e9f-c1ea3a747813 Release Notes: - Fixes indentation-related issues involving tab, newline, etc for Python. --- crates/language/src/buffer.rs | 49 +++++++++++++++------- crates/language/src/language.rs | 5 +++ crates/languages/src/python.rs | 4 +- crates/languages/src/python/config.toml | 5 ++- crates/languages/src/python/indents.scm | 55 ++++++++++++++++++------- 5 files changed, 84 insertions(+), 34 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 700157496e..972982e56c 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2857,6 +2857,7 @@ 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; // Find the suggested indentation ranges based on the syntax tree. let start = Point::new(prev_non_blank_row.unwrap_or(row_range.start), 0); @@ -2876,6 +2877,7 @@ impl BufferSnapshot { 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]; for capture in mat.captures { @@ -2887,16 +2889,23 @@ 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())); + let point = Point::from_ts_point(capture.node.start_position()); + outdent.get_or_insert(point); + outdent_positions.push(point); } } 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 { + if start.row == end.row && !significant_indentation { continue; } - let range = start..end; match indent_ranges.binary_search_by_key(&range.start, |r| r.start) { Err(ix) => indent_ranges.insert(ix, range), @@ -2932,16 +2941,20 @@ impl BufferSnapshot { matches.advance(); } - 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; + // 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; + } } } @@ -3011,8 +3024,14 @@ impl BufferSnapshot { if range.start.row == prev_row && range.end > row_start { indent_from_prev_row = true; } - if range.end > prev_row_start && range.end <= row_start { - outdent_to_row = outdent_to_row.min(range.start.row); + 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); + } } } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 93abc6f49d..f4bee8253d 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -681,6 +681,10 @@ 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")] @@ -884,6 +888,7 @@ impl Default for LanguageConfig { jsx_tag_auto_close: None, completion_query_characters: Default::default(), debuggers: Default::default(), + significant_indentation: Default::default(), } } } diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 2373d3a225..c81eb178ff 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -1209,7 +1209,7 @@ mod tests { append(&mut buffer, "foo(\n1)", cx); assert_eq!( buffer.text(), - "def a():\n \n if a:\n b()\n else:\n foo(\n 1)" + "def a():\n \n if a:\n b()\n else:\n foo(\n 1)" ); // dedent the closing paren if it is shifted to the beginning of the line @@ -1255,7 +1255,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 8181c0581a..f878cb3966 100644 --- a/crates/languages/src/python/config.toml +++ b/crates/languages/src/python/config.toml @@ -27,6 +27,7 @@ brackets = [ ] auto_indent_using_last_non_empty_line = false -increase_indent_pattern = "^[^#].*:\\s*$" -decrease_indent_pattern = "^\\s*(else|elif|except|finally)\\b.*:" debuggers = ["Debugpy"] +significant_indentation = true +increase_indent_pattern = "^\\s*(try)\\b.*:" +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 134b648cb6..3f5fd87294 100644 --- a/crates/languages/src/python/indents.scm +++ b/crates/languages/src/python/indents.scm @@ -1,18 +1,43 @@ -(_ "[" "]" @end) @indent -(_ "{" "}" @end) @indent -(_ "(" ")" @end) @indent - -(try_statement - body: (_) @start - [(except_clause) (finally_clause)] @end - ) @indent +(function_definition + ":" @start + body: (block) @indent +) (if_statement - consequence: (_) @start - alternative: (_) @end - ) @indent + ":" @start + consequence: (block) @indent + alternative: (_)? @outdent +) -(_ - alternative: (elif_clause) @start - alternative: (_) @end - ) @indent +(else_clause + ":" @start + body: (block) @indent +) + +(elif_clause + ":" @start + consequence: (block) @indent +) + +(for_statement + ":" @start + body: (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 +)