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

Closes #33238, follow-up to
https://github.com/zed-industries/zed/pull/29625.

Changes:

- Removed `significant_indentation`, which was the way to introduce
indentation scoping in languages like Python. However, it turned out to
be unnecessarily complicated to define and maintain.
- Introduced `decrease_indent_patterns`, which takes a `pattern` keyword
to automatically outdent and `valid_after` keywords to treat as valid
code points to snap to. The outdent happens to the most recent
`valid_after` keyword that also has less or equal indentation than the
currently typed keyword.

Fixes:

1. In Python, typing `except`, `finally`, `else`, and so on now
automatically indents intelligently based on the context in which it
appears. For instance:

```py
try:
    if a == 1:
        try:
             b = 2
             ^  # <-- typing "except:" here would indent it to inner try block
```

but,

```py
try:
    if a == 1:
        try:
             b = 2
    ^  # <-- typing "except:" here would indent it to outer try block
```

2. Fixes comments not maintaining indent.

Release Notes:

- Improved auto outdent for Python while typing keywords like `except`,
`else`, `finally`, etc.
- Fixed the issue where comments in Python would not maintain their
indentation.
This commit is contained in:
Smit Barmase 2025-06-26 11:11:03 +05:30 committed by GitHub
parent 1753432406
commit d09c7eb317
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 211 additions and 181 deletions

View file

@ -696,10 +696,6 @@ 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")]
@ -717,6 +713,12 @@ pub struct LanguageConfig {
#[serde(default, deserialize_with = "deserialize_regex")]
#[schemars(schema_with = "regex_json_schema")]
pub decrease_indent_pattern: Option<Regex>,
/// A list of rules for decreasing indentation. Each rule pairs a regex with a set of valid
/// "block-starting" tokens. When a line matches a pattern, its indentation is aligned with
/// the most recent line that began with a corresponding token. This enables context-aware
/// outdenting, like aligning an `else` with its `if`.
#[serde(default)]
pub decrease_indent_patterns: Vec<DecreaseIndentConfig>,
/// A list of characters that trigger the automatic insertion of a closing
/// bracket when they immediately precede the point where an opening
/// bracket is inserted.
@ -776,6 +778,15 @@ pub struct LanguageConfig {
pub documentation: Option<DocumentationConfig>,
}
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
pub struct DecreaseIndentConfig {
#[serde(default, deserialize_with = "deserialize_regex")]
#[schemars(schema_with = "regex_json_schema")]
pub pattern: Option<Regex>,
#[serde(default)]
pub valid_after: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
pub struct LanguageMatcher {
/// Given a list of `LanguageConfig`'s, the language of a file can be determined based on the path extension matching any of the `path_suffixes`.
@ -899,6 +910,7 @@ impl Default for LanguageConfig {
auto_indent_on_paste: None,
increase_indent_pattern: Default::default(),
decrease_indent_pattern: Default::default(),
decrease_indent_patterns: Default::default(),
autoclose_before: Default::default(),
line_comments: Default::default(),
block_comment: Default::default(),
@ -914,7 +926,6 @@ impl Default for LanguageConfig {
jsx_tag_auto_close: None,
completion_query_characters: Default::default(),
debuggers: Default::default(),
significant_indentation: Default::default(),
documentation: None,
}
}
@ -1092,6 +1103,7 @@ struct IndentConfig {
start_capture_ix: Option<u32>,
end_capture_ix: Option<u32>,
outdent_capture_ix: Option<u32>,
suffixed_start_captures: HashMap<u32, SharedString>,
}
pub struct OutlineConfig {
@ -1522,6 +1534,14 @@ impl Language {
("outdent", &mut outdent_capture_ix),
],
);
let mut suffixed_start_captures = HashMap::default();
for (ix, name) in query.capture_names().iter().enumerate() {
if let Some(suffix) = name.strip_prefix("start.") {
suffixed_start_captures.insert(ix as u32, suffix.to_owned().into());
}
}
if let Some(indent_capture_ix) = indent_capture_ix {
grammar.indents_config = Some(IndentConfig {
query,
@ -1529,6 +1549,7 @@ impl Language {
start_capture_ix,
end_capture_ix,
outdent_capture_ix,
suffixed_start_captures,
});
}
Ok(self)