Merge pull request #1246 from zed-industries/python-autoindent

Fix Python auto-indent using new auto-indent features
This commit is contained in:
Antonio Scandurra 2022-06-28 10:14:22 +02:00 committed by GitHub
commit 6cf9514e00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 371 additions and 69 deletions

10
Cargo.lock generated
View File

@ -2397,6 +2397,7 @@ dependencies = [
"parking_lot 0.11.2",
"postage",
"rand 0.8.5",
"regex",
"rpc",
"serde",
"serde_json",
@ -2408,6 +2409,7 @@ dependencies = [
"theme",
"tree-sitter",
"tree-sitter-json 0.19.0",
"tree-sitter-python",
"tree-sitter-rust",
"tree-sitter-typescript",
"unindent",
@ -5214,9 +5216,9 @@ dependencies = [
[[package]]
name = "tree-sitter"
version = "0.20.7"
version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "549a9faf45679ad50b7f603253635598cf5e007d8ceb806a23f95355938f76a0"
checksum = "268bf3e3ca0c09e5d21b59c2638e12cb6dcf7ea2681250a696a2d0936cb57ba0"
dependencies = [
"cc",
"regex",
@ -5281,9 +5283,9 @@ dependencies = [
[[package]]
name = "tree-sitter-python"
version = "0.20.1"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713170684ba94376b784b0c6dd23693461e15f96a806ed1848e40996e3cda7c7"
checksum = "dda114f58048f5059dcf158aff691dffb8e113e6d2b50d94263fd68711975287"
dependencies = [
"cc",
"tree-sitter",

View File

@ -40,6 +40,7 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
postage = { version = "0.4.1", features = ["futures-traits"] }
rand = { version = "0.8.3", optional = true }
regex = "1.5"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = { version = "1", features = ["preserve_order"] }
similar = "1.3"
@ -61,5 +62,6 @@ env_logger = "0.9"
rand = "0.8.3"
tree-sitter-json = "*"
tree-sitter-rust = "*"
tree-sitter-python = "*"
tree-sitter-typescript = "*"
unindent = "0.1.7"

View File

@ -237,7 +237,7 @@ struct AutoindentRequest {
#[derive(Debug)]
struct IndentSuggestion {
basis_row: u32,
indent: bool,
delta: Ordering,
}
pub(crate) struct TextProvider<'a>(pub(crate) &'a Rope);
@ -812,19 +812,23 @@ impl Buffer {
.into_iter()
.flatten();
for (old_row, suggestion) in old_edited_range.zip(suggestions) {
let mut suggested_indent = old_to_new_rows
.get(&suggestion.basis_row)
.and_then(|from_row| old_suggestions.get(from_row).copied())
.unwrap_or_else(|| {
request
.before_edit
.indent_size_for_line(suggestion.basis_row)
});
if suggestion.indent {
suggested_indent += request.indent_size;
if let Some(suggestion) = suggestion {
let mut suggested_indent = old_to_new_rows
.get(&suggestion.basis_row)
.and_then(|from_row| old_suggestions.get(from_row).copied())
.unwrap_or_else(|| {
request
.before_edit
.indent_size_for_line(suggestion.basis_row)
});
if suggestion.delta.is_gt() {
suggested_indent += request.indent_size;
} else if suggestion.delta.is_lt() {
suggested_indent -= request.indent_size;
}
old_suggestions
.insert(*old_to_new_rows.get(&old_row).unwrap(), suggested_indent);
}
old_suggestions
.insert(*old_to_new_rows.get(&old_row).unwrap(), suggested_indent);
}
yield_now().await;
}
@ -839,18 +843,26 @@ impl Buffer {
.into_iter()
.flatten();
for (new_row, suggestion) in new_edited_row_range.zip(suggestions) {
let mut suggested_indent = indent_sizes
.get(&suggestion.basis_row)
.copied()
.unwrap_or_else(|| snapshot.indent_size_for_line(suggestion.basis_row));
if suggestion.indent {
suggested_indent += request.indent_size;
}
if old_suggestions
.get(&new_row)
.map_or(true, |old_indentation| suggested_indent != *old_indentation)
{
indent_sizes.insert(new_row, suggested_indent);
if let Some(suggestion) = suggestion {
let mut suggested_indent = indent_sizes
.get(&suggestion.basis_row)
.copied()
.unwrap_or_else(|| {
snapshot.indent_size_for_line(suggestion.basis_row)
});
if suggestion.delta.is_gt() {
suggested_indent += request.indent_size;
} else if suggestion.delta.is_lt() {
suggested_indent -= request.indent_size;
}
if old_suggestions
.get(&new_row)
.map_or(true, |old_indentation| {
suggested_indent != *old_indentation
})
{
indent_sizes.insert(new_row, suggested_indent);
}
}
}
yield_now().await;
@ -870,16 +882,20 @@ impl Buffer {
.into_iter()
.flatten();
for (row, suggestion) in inserted_row_range.zip(suggestions) {
let mut suggested_indent = indent_sizes
.get(&suggestion.basis_row)
.copied()
.unwrap_or_else(|| {
snapshot.indent_size_for_line(suggestion.basis_row)
});
if suggestion.indent {
suggested_indent += request.indent_size;
if let Some(suggestion) = suggestion {
let mut suggested_indent = indent_sizes
.get(&suggestion.basis_row)
.copied()
.unwrap_or_else(|| {
snapshot.indent_size_for_line(suggestion.basis_row)
});
if suggestion.delta.is_gt() {
suggested_indent += request.indent_size;
} else if suggestion.delta.is_lt() {
suggested_indent -= request.indent_size;
}
indent_sizes.insert(row, suggested_indent);
}
indent_sizes.insert(row, suggested_indent);
}
yield_now().await;
}
@ -1551,10 +1567,13 @@ impl BufferSnapshot {
fn suggest_autoindents<'a>(
&'a self,
row_range: Range<u32>,
) -> Option<impl Iterator<Item = IndentSuggestion> + 'a> {
// Get the "indentation ranges" that intersect this row range.
let grammar = self.grammar()?;
) -> Option<impl Iterator<Item = Option<IndentSuggestion>> + 'a> {
let language = self.language.as_ref()?;
let grammar = language.grammar.as_ref()?;
let config = &language.config;
let prev_non_blank_row = self.prev_non_blank_row(row_range.start);
// Find the suggested indentation ranges based on the syntax tree.
let indents_query = grammar.indents_query.as_ref()?;
let mut query_cursor = QueryCursorHandle::new();
let indent_capture_ix = indents_query.capture_index_for_name("indent");
@ -1563,6 +1582,7 @@ impl BufferSnapshot {
Point::new(prev_non_blank_row.unwrap_or(row_range.start), 0).to_ts_point()
..Point::new(row_range.end, 0).to_ts_point(),
);
let mut indentation_ranges = Vec::<Range<Point>>::new();
for mat in query_cursor.matches(
indents_query,
@ -1596,48 +1616,98 @@ impl BufferSnapshot {
}
}
let mut prev_row = prev_non_blank_row.unwrap_or(0);
// Find the suggested indentation increases and decreased based on regexes.
let mut indent_changes = Vec::<(u32, Ordering)>::new();
self.for_each_line(
Point::new(prev_non_blank_row.unwrap_or(row_range.start), 0)
..Point::new(row_range.end, 0),
|row, line| {
if config
.decrease_indent_pattern
.as_ref()
.map_or(false, |regex| regex.is_match(line))
{
indent_changes.push((row, Ordering::Less));
}
if config
.increase_indent_pattern
.as_ref()
.map_or(false, |regex| regex.is_match(line))
{
indent_changes.push((row + 1, Ordering::Greater));
}
},
);
let mut indent_changes = indent_changes.into_iter().peekable();
let mut prev_row = 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);
let mut indent_from_prev_row = false;
let mut outdent_from_prev_row = false;
let mut outdent_to_row = u32::MAX;
while let Some((indent_row, delta)) = indent_changes.peek() {
if *indent_row == row {
match delta {
Ordering::Less => outdent_from_prev_row = true,
Ordering::Greater => indent_from_prev_row = true,
_ => {}
}
} else if *indent_row > row {
break;
}
indent_changes.next();
}
for range in &indentation_ranges {
if range.start.row >= row {
break;
}
if range.start.row == prev_row && range.end > row_start {
indent_from_prev_row = true;
}
if range.end.row >= prev_row && range.end <= row_start {
if range.end > prev_row_start && range.end <= row_start {
outdent_to_row = outdent_to_row.min(range.start.row);
}
}
let suggestion = if outdent_to_row == prev_row {
IndentSuggestion {
let suggestion = if outdent_to_row == prev_row
|| (outdent_from_prev_row && indent_from_prev_row)
{
Some(IndentSuggestion {
basis_row: prev_row,
indent: false,
}
delta: Ordering::Equal,
})
} else if indent_from_prev_row {
IndentSuggestion {
Some(IndentSuggestion {
basis_row: prev_row,
indent: true,
}
delta: Ordering::Greater,
})
} else if outdent_to_row < prev_row {
IndentSuggestion {
Some(IndentSuggestion {
basis_row: outdent_to_row,
indent: false,
}
} else {
IndentSuggestion {
delta: Ordering::Equal,
})
} else if outdent_from_prev_row {
Some(IndentSuggestion {
basis_row: prev_row,
indent: false,
}
delta: Ordering::Less,
})
} else if config.auto_indent_using_last_non_empty_line || !self.is_line_blank(prev_row)
{
Some(IndentSuggestion {
basis_row: prev_row,
delta: Ordering::Equal,
})
} else {
None
};
prev_row = row;
prev_row_start = row_start;
suggestion
}))
}
@ -1690,6 +1760,25 @@ impl BufferSnapshot {
)
}
pub fn for_each_line<'a>(&'a self, range: Range<Point>, mut callback: impl FnMut(u32, &str)) {
let mut line = String::new();
let mut row = range.start.row;
for chunk in self
.as_rope()
.chunks_in_range(range.to_offset(self))
.chain(["\n"])
{
for (newline_ix, text) in chunk.split('\n').enumerate() {
if newline_ix > 0 {
callback(row, &line);
row += 1;
line.clear();
}
line.push_str(text);
}
}
}
pub fn language(&self) -> Option<&Arc<Language>> {
self.language.as_ref()
}
@ -2411,6 +2500,14 @@ impl std::ops::AddAssign for IndentSize {
}
}
impl std::ops::SubAssign for IndentSize {
fn sub_assign(&mut self, other: IndentSize) {
if self.kind == other.kind && self.len >= other.len {
self.len -= other.len;
}
}
}
impl Completion {
pub fn sort_key(&self) -> (usize, &str) {
let kind_key = match self.lsp_completion.kind {

View File

@ -17,7 +17,8 @@ use gpui::{MutableAppContext, Task};
use highlight_map::HighlightMap;
use lazy_static::lazy_static;
use parking_lot::{Mutex, RwLock};
use serde::Deserialize;
use regex::Regex;
use serde::{de, Deserialize, Deserializer};
use serde_json::Value;
use std::{
any::Any,
@ -49,10 +50,7 @@ lazy_static! {
pub static ref PLAIN_TEXT: Arc<Language> = Arc::new(Language::new(
LanguageConfig {
name: "Plain Text".into(),
path_suffixes: Default::default(),
brackets: Default::default(),
autoclose_before: Default::default(),
line_comment: None,
..Default::default()
},
None,
));
@ -123,6 +121,12 @@ pub struct LanguageConfig {
pub name: Arc<str>,
pub path_suffixes: Vec<String>,
pub brackets: Vec<BracketPair>,
#[serde(default = "auto_indent_using_last_non_empty_line_default")]
pub auto_indent_using_last_non_empty_line: bool,
#[serde(default, deserialize_with = "deserialize_regex")]
pub increase_indent_pattern: Option<Regex>,
#[serde(default, deserialize_with = "deserialize_regex")]
pub decrease_indent_pattern: Option<Regex>,
#[serde(default)]
pub autoclose_before: String,
pub line_comment: Option<String>,
@ -134,12 +138,28 @@ impl Default for LanguageConfig {
name: "".into(),
path_suffixes: Default::default(),
brackets: Default::default(),
auto_indent_using_last_non_empty_line: auto_indent_using_last_non_empty_line_default(),
increase_indent_pattern: Default::default(),
decrease_indent_pattern: Default::default(),
autoclose_before: Default::default(),
line_comment: Default::default(),
}
}
}
fn auto_indent_using_last_non_empty_line_default() -> bool {
true
}
fn deserialize_regex<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Regex>, D::Error> {
let source = Option::<String>::deserialize(d)?;
if let Some(source) = source {
Ok(Some(regex::Regex::new(&source).map_err(de::Error::custom)?))
} else {
Ok(None)
}
}
#[cfg(any(test, feature = "test-support"))]
pub struct FakeLspAdapter {
pub name: &'static str,

View File

@ -87,14 +87,14 @@ tempdir = { version = "0.3.7" }
thiserror = "1.0.29"
tiny_http = "0.8"
toml = "0.5"
tree-sitter = "0.20.7"
tree-sitter = "0.20.8"
tree-sitter-c = "0.20.1"
tree-sitter-cpp = "0.20.0"
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "137e1ce6a02698fc246cdb9c6b886ed1de9a1ed8" }
tree-sitter-rust = "0.20.1"
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
tree-sitter-python = "0.20.1"
tree-sitter-python = "0.20.2"
tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" }
tree-sitter-typescript = "0.20.1"
url = "2.2"

View File

@ -256,3 +256,41 @@ impl super::LspAdapter for CLspAdapter {
})
}
}
#[cfg(test)]
mod tests {
use gpui::MutableAppContext;
use language::{Buffer, IndentSize};
use std::sync::Arc;
#[gpui::test]
fn test_c_autoindent(cx: &mut MutableAppContext) {
cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
let language = crate::languages::language("c", tree_sitter_c::language(), None);
cx.add_model(|cx| {
let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx);
let size = IndentSize::spaces(2);
// empty function
buffer.edit_with_autoindent([(0..0, "int main() {}")], size, cx);
// indent inside braces
let ix = buffer.len() - 1;
buffer.edit_with_autoindent([(ix..ix, "\n\n")], size, cx);
assert_eq!(buffer.text(), "int main() {\n \n}");
// indent body of single-statement if statement
let ix = buffer.len() - 2;
buffer.edit_with_autoindent([(ix..ix, "if (a)\nb;")], size, cx);
assert_eq!(buffer.text(), "int main() {\n if (a)\n b;\n}");
// indent inside field expression
let ix = buffer.len() - 3;
buffer.edit_with_autoindent([(ix..ix, "\n.c")], size, cx);
assert_eq!(buffer.text(), "int main() {\n if (a)\n b\n .c;\n}");
buffer
});
}
}

View File

@ -1,6 +1,8 @@
[
(field_expression)
(assignment_expression)
(field_expression)
(assignment_expression)
(if_statement)
(for_statement)
] @indent
(_ "{" "}" @end) @indent

View File

@ -151,3 +151,103 @@ impl LspAdapter for PythonLspAdapter {
})
}
}
#[cfg(test)]
mod tests {
use gpui::{ModelContext, MutableAppContext};
use language::{Buffer, IndentSize};
use std::sync::Arc;
#[gpui::test]
fn test_python_autoindent(cx: &mut MutableAppContext) {
cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
let language = crate::languages::language("python", tree_sitter_python::language(), None);
cx.add_model(|cx| {
let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx);
let size = IndentSize::spaces(2);
let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext<Buffer>| {
let ix = buffer.len();
buffer.edit_with_autoindent([(ix..ix, text)], size, cx);
};
// indent after "def():"
append(&mut buffer, "def a():\n", cx);
assert_eq!(buffer.text(), "def a():\n ");
// preserve indent after blank line
append(&mut buffer, "\n ", cx);
assert_eq!(buffer.text(), "def a():\n \n ");
// indent after "if"
append(&mut buffer, "if a:\n ", cx);
assert_eq!(buffer.text(), "def a():\n \n if a:\n ");
// preserve indent after statement
append(&mut buffer, "b()\n", cx);
assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n ");
// preserve indent after statement
append(&mut buffer, "else", cx);
assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n else");
// dedent "else""
append(&mut buffer, ":", cx);
assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n else:");
// indent lines after else
append(&mut buffer, "\n", cx);
assert_eq!(
buffer.text(),
"def a():\n \n if a:\n b()\n else:\n "
);
// indent after an open paren. the closing paren is not indented
// because there is another token before it on the same line.
append(&mut buffer, "foo(\n1)", cx);
assert_eq!(
buffer.text(),
"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
let argument_ix = buffer.text().find("1").unwrap();
buffer.edit_with_autoindent([(argument_ix..argument_ix + 1, "")], size, cx);
assert_eq!(
buffer.text(),
"def a():\n \n if a:\n b()\n else:\n foo(\n )"
);
// preserve indent after the close paren
append(&mut buffer, "\n", cx);
assert_eq!(
buffer.text(),
"def a():\n \n if a:\n b()\n else:\n foo(\n )\n "
);
// manually outdent the last line
let end_whitespace_ix = buffer.len() - 4;
buffer.edit_with_autoindent([(end_whitespace_ix..buffer.len(), "")], size, cx);
assert_eq!(
buffer.text(),
"def a():\n \n if a:\n b()\n else:\n foo(\n )\n"
);
// preserve the newly reduced indentation on the next newline
append(&mut buffer, "\n", cx);
assert_eq!(
buffer.text(),
"def a():\n \n if a:\n b()\n else:\n foo(\n )\n\n"
);
// reset to a simple if statement
buffer.edit([(0..buffer.len(), "if a:\n b(\n )")], cx);
// 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 ");
buffer
});
}
}

View File

@ -9,3 +9,7 @@ brackets = [
{ start = "\"", end = "\"", close = true, newline = false },
{ start = "'", end = "'", close = false, newline = false },
]
auto_indent_using_last_non_empty_line = false
increase_indent_pattern = ":$"
decrease_indent_pattern = "^\\s*(else|elif|except|finally)\\b.*:"

View File

@ -1,4 +1,3 @@
(_ (block)) @indent
(_ "[" "]" @end) @indent
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent

View File

@ -270,7 +270,7 @@ impl LspAdapter for RustLspAdapter {
mod tests {
use super::*;
use crate::languages::{language, LspAdapter};
use gpui::color::Color;
use gpui::{color::Color, MutableAppContext};
use theme::SyntaxTheme;
#[test]
@ -432,4 +432,42 @@ mod tests {
})
);
}
#[gpui::test]
fn test_rust_autoindent(cx: &mut MutableAppContext) {
cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
let language = crate::languages::language("rust", tree_sitter_rust::language(), None);
cx.add_model(|cx| {
let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx);
let size = IndentSize::spaces(2);
// start with empty function
buffer.edit_with_autoindent([(0..0, "fn a() {}")], size, cx);
// indent between braces
let ix = buffer.len() - 1;
buffer.edit_with_autoindent([(ix..ix, "\n\n")], size, cx);
assert_eq!(buffer.text(), "fn a() {\n \n}");
// indent field expression
let ix = buffer.len() - 2;
buffer.edit_with_autoindent([(ix..ix, "b\n.c")], size, cx);
assert_eq!(buffer.text(), "fn a() {\n b\n .c\n}");
// indent chained field expression preceded by blank line
let ix = buffer.len() - 2;
buffer.edit_with_autoindent([(ix..ix, "\n\n.d")], size, cx);
assert_eq!(buffer.text(), "fn a() {\n b\n .c\n \n .d\n}");
// dedent line after the field expression
let ix = buffer.len() - 2;
buffer.edit_with_autoindent([(ix..ix, ";\ne")], size, cx);
assert_eq!(
buffer.text(),
"fn a() {\n b\n .c\n \n .d;\n e\n}"
);
buffer
});
}
}