language: Fix indent suggestions for significant indented languages like Python (#29625)
Smit Barmase
created
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.
@@ -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,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);
+ }
}
}
@@ -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(),
}
}
}
@@ -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
});