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>> + '_> {
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<Point> = None;
let mut end: Option<Point> = None;
let mut outdent: Option<Point> = 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,6 +2941,9 @@ 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
@ -2944,6 +2956,7 @@ impl BufferSnapshot {
range_to_truncate.end = outdent_position;
}
}
}
// Find the suggested indentation increases and decreased based on regexes.
let mut indent_change_rows = Vec::<(u32, Ordering)>::new();
@ -3011,10 +3024,16 @@ 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);
}
}
}
let within_error = error_ranges
.iter()

View file

@ -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(),
}
}
}

View file

@ -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.*:"

View file

@ -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
)