Skip to content

language: Add context-aware decrease indent for Python #33370

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 84 additions & 53 deletions crates/editor/src/editor_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 _:
Expand Down Expand Up @@ -21561,53 +21561,104 @@ 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():
try:
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:
i = 2
except:
j = 2
else:
k = 2
for i in range(n):
pass
ˇ
"});
cx.update_editor(|editor, window, cx| {
Expand All @@ -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
Expand Down
98 changes: 63 additions & 35 deletions crates/language/src/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2895,7 +2895,12 @@ 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;

#[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);
Expand All @@ -2911,13 +2916,13 @@ impl BufferSnapshot {
.collect::<Vec<_>>();

let mut indent_ranges = Vec::<Range<Point>>::new();
let mut start_positions = Vec::<StartPosition>::new();
let mut outdent_positions = Vec::<Point>::new();
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];
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()));
Expand All @@ -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;
Expand Down Expand Up @@ -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)
Expand All @@ -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;
}
}
},
);

Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading