diff --git a/Cargo.lock b/Cargo.lock index 35042b1eb4..a301a33780 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index eed1297cea..c70ad6b731 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -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" diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 7fb414166d..0c94cfdf9f 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -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, - ) -> Option + 'a> { - // Get the "indentation ranges" that intersect this row range. - let grammar = self.grammar()?; + ) -> Option> + '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::>::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, 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> { 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 { diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index d7c7a9185a..2bdafddb1b 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -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 = 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, pub path_suffixes: Vec, pub brackets: Vec, + #[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, + #[serde(default, deserialize_with = "deserialize_regex")] + pub decrease_indent_pattern: Option, #[serde(default)] pub autoclose_before: String, pub line_comment: Option, @@ -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, D::Error> { + let source = Option::::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, diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 74b48ffee8..5dc36b1971 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -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 + }); + } +} diff --git a/crates/zed/src/languages/c/indents.scm b/crates/zed/src/languages/c/indents.scm index a17f4c4821..fa40ce215e 100644 --- a/crates/zed/src/languages/c/indents.scm +++ b/crates/zed/src/languages/c/indents.scm @@ -1,6 +1,8 @@ [ - (field_expression) - (assignment_expression) + (field_expression) + (assignment_expression) + (if_statement) + (for_statement) ] @indent (_ "{" "}" @end) @indent diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index be5b58b4b5..3c0acd0945 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -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| { + 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 + }); + } +} diff --git a/crates/zed/src/languages/python/config.toml b/crates/zed/src/languages/python/config.toml index da49d3709a..c6b41ed700 100644 --- a/crates/zed/src/languages/python/config.toml +++ b/crates/zed/src/languages/python/config.toml @@ -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.*:" \ No newline at end of file diff --git a/crates/zed/src/languages/python/indents.scm b/crates/zed/src/languages/python/indents.scm index ad262fd501..112b414aa4 100644 --- a/crates/zed/src/languages/python/indents.scm +++ b/crates/zed/src/languages/python/indents.scm @@ -1,4 +1,3 @@ -(_ (block)) @indent (_ "[" "]" @end) @indent (_ "{" "}" @end) @indent (_ "(" ")" @end) @indent diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 1abeeb097c..4a83cccc3b 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -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 + }); + } }