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.
This commit is contained in:
Smit Barmase 2025-05-07 23:05:42 +05:30 committed by GitHub
parent 22ad207baf
commit 7c76cee16d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 84 additions and 34 deletions

View file

@ -2857,6 +2857,7 @@ impl BufferSnapshot {
) -> Option<impl Iterator<Item = Option<IndentSuggestion>> + '_> { ) -> Option<impl Iterator<Item = Option<IndentSuggestion>> + '_> {
let config = &self.language.as_ref()?.config; let config = &self.language.as_ref()?.config;
let prev_non_blank_row = self.prev_non_blank_row(row_range.start); 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. // 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 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() { while let Some(mat) = matches.peek() {
let mut start: Option<Point> = None; let mut start: Option<Point> = None;
let mut end: Option<Point> = None; let mut end: Option<Point> = None;
let mut outdent: Option<Point> = None;
let config = &indent_configs[mat.grammar_index]; let config = &indent_configs[mat.grammar_index];
for capture in mat.captures { for capture in mat.captures {
@ -2887,16 +2889,23 @@ impl BufferSnapshot {
} else if Some(capture.index) == config.end_capture_ix { } else if Some(capture.index) == config.end_capture_ix {
end = Some(Point::from_ts_point(capture.node.start_position())); end = Some(Point::from_ts_point(capture.node.start_position()));
} else if Some(capture.index) == config.outdent_capture_ix { } 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(); 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 let Some((start, end)) = start.zip(end) {
if start.row == end.row { if start.row == end.row && !significant_indentation {
continue; continue;
} }
let range = start..end; let range = start..end;
match indent_ranges.binary_search_by_key(&range.start, |r| r.start) { match indent_ranges.binary_search_by_key(&range.start, |r| r.start) {
Err(ix) => indent_ranges.insert(ix, range), Err(ix) => indent_ranges.insert(ix, range),
@ -2932,16 +2941,20 @@ impl BufferSnapshot {
matches.advance(); matches.advance();
} }
outdent_positions.sort(); // we don't use outdent positions to truncate in case of significant indentation
for outdent_position in outdent_positions { // rather we use them to expand (handled above)
// find the innermost indent range containing this outdent_position if !significant_indentation {
// set its end to the outdent position outdent_positions.sort();
if let Some(range_to_truncate) = indent_ranges for outdent_position in outdent_positions {
.iter_mut() // find the innermost indent range containing this outdent_position
.filter(|indent_range| indent_range.contains(&outdent_position)) // set its end to the outdent position
.next_back() if let Some(range_to_truncate) = indent_ranges
{ .iter_mut()
range_to_truncate.end = outdent_position; .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 { if range.start.row == prev_row && range.end > row_start {
indent_from_prev_row = true; indent_from_prev_row = true;
} }
if range.end > prev_row_start && range.end <= row_start { if significant_indentation && self.is_line_blank(row) && range.start.row == prev_row
outdent_to_row = outdent_to_row.min(range.start.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);
}
} }
} }

View file

@ -681,6 +681,10 @@ pub struct LanguageConfig {
#[serde(default)] #[serde(default)]
#[schemars(schema_with = "bracket_pair_config_json_schema")] #[schemars(schema_with = "bracket_pair_config_json_schema")]
pub brackets: BracketPairConfig, 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 /// If set to true, auto indentation uses last non empty line to determine
/// the indentation level for a new line. /// the indentation level for a new line.
#[serde(default = "auto_indent_using_last_non_empty_line_default")] #[serde(default = "auto_indent_using_last_non_empty_line_default")]
@ -884,6 +888,7 @@ impl Default for LanguageConfig {
jsx_tag_auto_close: None, jsx_tag_auto_close: None,
completion_query_characters: Default::default(), completion_query_characters: Default::default(),
debuggers: Default::default(), debuggers: Default::default(),
significant_indentation: Default::default(),
} }
} }
} }

View file

@ -1209,7 +1209,7 @@ mod tests {
append(&mut buffer, "foo(\n1)", cx); append(&mut buffer, "foo(\n1)", cx);
assert_eq!( assert_eq!(
buffer.text(), 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 // 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 // dedent "else" on the line after a closing paren
append(&mut buffer, "\n else:\n", cx); 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 buffer
}); });

View file

@ -27,6 +27,7 @@ brackets = [
] ]
auto_indent_using_last_non_empty_line = false auto_indent_using_last_non_empty_line = false
increase_indent_pattern = "^[^#].*:\\s*$"
decrease_indent_pattern = "^\\s*(else|elif|except|finally)\\b.*:"
debuggers = ["Debugpy"] debuggers = ["Debugpy"]
significant_indentation = true
increase_indent_pattern = "^\\s*(try)\\b.*:"
decrease_indent_pattern = "^\\s*(else|elif|except|finally)\\b.*:"

View file

@ -1,18 +1,43 @@
(_ "[" "]" @end) @indent (function_definition
(_ "{" "}" @end) @indent ":" @start
(_ "(" ")" @end) @indent body: (block) @indent
)
(try_statement
body: (_) @start
[(except_clause) (finally_clause)] @end
) @indent
(if_statement (if_statement
consequence: (_) @start ":" @start
alternative: (_) @end consequence: (block) @indent
) @indent alternative: (_)? @outdent
)
(_ (else_clause
alternative: (elif_clause) @start ":" @start
alternative: (_) @end body: (block) @indent
) @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
)