Detailed changes
@@ -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",
@@ -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"
@@ -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 {
@@ -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,
@@ -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
+ });
+ }
+}
@@ -1,6 +1,8 @@
[
- (field_expression)
- (assignment_expression)
+ (field_expression)
+ (assignment_expression)
+ (if_statement)
+ (for_statement)
] @indent
(_ "{" "}" @end) @indent
@@ -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
+ });
+ }
+}
@@ -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.*:"
@@ -1,4 +1,3 @@
-(_ (block)) @indent
(_ "[" "]" @end) @indent
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent
@@ -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
+ });
+ }
}