Detailed changes
@@ -2913,55 +2913,125 @@ impl Editor {
return;
}
- let mut selections = self.selections.all_adjusted(cx);
- if selections.iter().all(|s| s.is_empty()) {
- self.transact(cx, |this, cx| {
- this.buffer.update(cx, |buffer, cx| {
- let mut prev_cursor_row = 0;
- let mut row_delta = 0;
- for selection in &mut selections {
- let mut cursor = selection.start;
- if cursor.row != prev_cursor_row {
- row_delta = 0;
- prev_cursor_row = cursor.row;
- }
- cursor.column += row_delta;
+ let selections = self.selections.all_adjusted(cx);
+ let buffer = self.buffer.read(cx).read(cx);
+ let suggested_indents =
+ buffer.suggested_indents(selections.iter().map(|s| s.head().row), cx);
+ let mut all_selections_empty = true;
+ let mut all_cursors_before_suggested_indent = true;
+ let mut all_cursors_in_leading_whitespace = true;
- let language_name = buffer.language_at(cursor, cx).map(|l| l.name());
- let settings = cx.global::<Settings>();
- let tab_size = if settings.hard_tabs(language_name.as_deref()) {
- IndentSize::tab()
- } else {
- let tab_size = settings.tab_size(language_name.as_deref()).get();
- let char_column = buffer
- .read(cx)
- .text_for_range(Point::new(cursor.row, 0)..cursor)
- .flat_map(str::chars)
- .count();
- let chars_to_next_tab_stop = tab_size - (char_column as u32 % tab_size);
- IndentSize::spaces(chars_to_next_tab_stop)
- };
- buffer.edit(
- [(cursor..cursor, tab_size.chars().collect::<String>())],
- None,
- cx,
- );
- cursor.column += tab_size.len;
- selection.start = cursor;
- selection.end = cursor;
+ for selection in &selections {
+ let cursor = selection.head();
+ if !selection.is_empty() {
+ all_selections_empty = false;
+ }
+ if cursor.column > buffer.indent_size_for_line(cursor.row).len {
+ all_cursors_in_leading_whitespace = false;
+ }
+ if let Some(indent) = suggested_indents.get(&cursor.row) {
+ if cursor.column >= indent.len {
+ all_cursors_before_suggested_indent = false;
+ }
+ } else {
+ all_cursors_before_suggested_indent = false;
+ }
+ }
+ drop(buffer);
- row_delta += tab_size.len;
- }
- });
- this.change_selections(Some(Autoscroll::Fit), cx, |s| {
- s.select(selections);
- });
- });
+ if all_selections_empty {
+ if all_cursors_in_leading_whitespace && all_cursors_before_suggested_indent {
+ self.auto_indent(suggested_indents, selections, cx);
+ } else {
+ self.insert_tab(selections, cx);
+ }
} else {
self.indent(&Indent, cx);
}
}
+ fn auto_indent(
+ &mut self,
+ suggested_indents: BTreeMap<u32, IndentSize>,
+ mut selections: Vec<Selection<Point>>,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ self.transact(cx, |this, cx| {
+ let mut rows = Vec::new();
+ let buffer = this.buffer.read(cx).read(cx);
+ for selection in &mut selections {
+ selection.end.column = buffer.indent_size_for_line(selection.end.row).len;
+ selection.start = selection.end;
+ rows.push(selection.end.row);
+ }
+ drop(buffer);
+
+ this.change_selections(Some(Autoscroll::Fit), cx, |s| {
+ s.select(selections);
+ });
+
+ this.buffer.update(cx, |buffer, cx| {
+ let snapshot = buffer.read(cx);
+ let edits: Vec<_> = suggested_indents
+ .into_iter()
+ .filter_map(|(row, new_indent)| {
+ let current_indent = snapshot.indent_size_for_line(row);
+ Buffer::edit_for_indent_size_adjustment(row, current_indent, new_indent)
+ })
+ .collect();
+ drop(snapshot);
+
+ buffer.edit(edits, None, cx);
+ });
+ });
+ }
+
+ fn insert_tab(&mut self, mut selections: Vec<Selection<Point>>, cx: &mut ViewContext<Editor>) {
+ self.transact(cx, |this, cx| {
+ this.buffer.update(cx, |buffer, cx| {
+ let mut prev_cursor_row = 0;
+ let mut row_delta = 0;
+ for selection in &mut selections {
+ let mut cursor = selection.start;
+ if cursor.row != prev_cursor_row {
+ row_delta = 0;
+ prev_cursor_row = cursor.row;
+ }
+ cursor.column += row_delta;
+
+ let language_name = buffer.language_at(cursor, cx).map(|l| l.name());
+ let settings = cx.global::<Settings>();
+ let tab_size = if settings.hard_tabs(language_name.as_deref()) {
+ IndentSize::tab()
+ } else {
+ let tab_size = settings.tab_size(language_name.as_deref()).get();
+ let char_column = buffer
+ .read(cx)
+ .text_for_range(Point::new(cursor.row, 0)..cursor)
+ .flat_map(str::chars)
+ .count();
+ let chars_to_next_tab_stop = tab_size - (char_column as u32 % tab_size);
+ IndentSize::spaces(chars_to_next_tab_stop)
+ };
+
+ buffer.edit(
+ [(cursor..cursor, tab_size.chars().collect::<String>())],
+ None,
+ cx,
+ );
+ cursor.column += tab_size.len;
+ selection.start = cursor;
+ selection.end = cursor;
+
+ row_delta += tab_size.len;
+ }
+ });
+ this.change_selections(Some(Autoscroll::Fit), cx, |s| {
+ s.select(selections);
+ });
+ });
+ }
+
pub fn indent(&mut self, _: &Indent, cx: &mut ViewContext<Self>) {
let mut selections = self.selections.all::<Point>(cx);
self.transact(cx, |this, cx| {
@@ -7966,6 +8036,56 @@ mod tests {
"});
}
+ #[gpui::test]
+ async fn test_tab_on_blank_line_auto_indents(cx: &mut gpui::TestAppContext) {
+ let mut cx = EditorTestContext::new(cx).await;
+ let language = Arc::new(
+ Language::new(
+ LanguageConfig::default(),
+ Some(tree_sitter_rust::language()),
+ )
+ .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
+ .unwrap(),
+ );
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+ cx.set_state(indoc! {"
+ const a: B = (
+ c(
+ d(
+ |
+ )
+ |
+ )
+ );
+ "});
+
+ // autoindent when one or more cursor is to the left of the correct level.
+ cx.update_editor(|e, cx| e.tab(&Tab, cx));
+ cx.assert_editor_state(indoc! {"
+ const a: B = (
+ c(
+ d(
+ |
+ )
+ |
+ )
+ );
+ "});
+
+ // when already at the correct indentation level, insert a tab.
+ cx.update_editor(|e, cx| e.tab(&Tab, cx));
+ cx.assert_editor_state(indoc! {"
+ const a: B = (
+ c(
+ d(
+ |
+ )
+ |
+ )
+ );
+ "});
+ }
+
#[gpui::test]
async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
let mut cx = EditorTestContext::new(cx).await;
@@ -3,7 +3,7 @@ mod anchor;
pub use anchor::{Anchor, AnchorRangeExt};
use anyhow::Result;
use clock::ReplicaId;
-use collections::{Bound, HashMap, HashSet};
+use collections::{BTreeMap, Bound, HashMap, HashSet};
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
pub use language::Completion;
use language::{
@@ -1939,6 +1939,53 @@ impl MultiBufferSnapshot {
}
}
+ pub fn suggested_indents(
+ &self,
+ rows: impl IntoIterator<Item = u32>,
+ cx: &AppContext,
+ ) -> BTreeMap<u32, IndentSize> {
+ let mut result = BTreeMap::new();
+
+ let mut rows_for_excerpt = Vec::new();
+ let mut cursor = self.excerpts.cursor::<Point>();
+
+ let mut rows = rows.into_iter().peekable();
+ while let Some(row) = rows.next() {
+ cursor.seek(&Point::new(row, 0), Bias::Right, &());
+ let excerpt = match cursor.item() {
+ Some(excerpt) => excerpt,
+ _ => continue,
+ };
+
+ let single_indent_size = excerpt.buffer.single_indent_size(cx);
+ let start_buffer_row = excerpt.range.context.start.to_point(&excerpt.buffer).row;
+ let start_multibuffer_row = cursor.start().row;
+
+ rows_for_excerpt.push(row);
+ while let Some(next_row) = rows.peek().copied() {
+ if cursor.end(&()).row > next_row {
+ rows_for_excerpt.push(next_row);
+ rows.next();
+ } else {
+ break;
+ }
+ }
+
+ let buffer_rows = rows_for_excerpt
+ .drain(..)
+ .map(|row| start_buffer_row + row - start_multibuffer_row);
+ let buffer_indents = excerpt
+ .buffer
+ .suggested_indents(buffer_rows, single_indent_size);
+ let multibuffer_indents = buffer_indents
+ .into_iter()
+ .map(|(row, indent)| (start_multibuffer_row + row - start_buffer_row, indent));
+ result.extend(multibuffer_indents);
+ }
+
+ result
+ }
+
pub fn indent_size_for_line(&self, row: u32) -> IndentSize {
if let Some((buffer, range)) = self.buffer_line_for_row(row) {
let mut size = buffer.indent_size_for_line(range.start.row);
@@ -957,45 +957,42 @@ impl Buffer {
cx: &mut ModelContext<Self>,
) {
self.autoindent_requests.clear();
- self.start_transaction();
- for (row, indent_size) in &indent_sizes {
- self.set_indent_size_for_line(*row, *indent_size, cx);
- }
- self.end_transaction(cx);
+
+ let edits: Vec<_> = indent_sizes
+ .into_iter()
+ .filter_map(|(row, indent_size)| {
+ let current_size = indent_size_for_line(&self, row);
+ Self::edit_for_indent_size_adjustment(row, current_size, indent_size)
+ })
+ .collect();
+
+ self.edit(edits, None, cx);
}
- fn set_indent_size_for_line(
- &mut self,
+ pub fn edit_for_indent_size_adjustment(
row: u32,
- size: IndentSize,
- cx: &mut ModelContext<Self>,
- ) {
- let current_size = indent_size_for_line(&self, row);
- if size.kind != current_size.kind && current_size.len > 0 {
- return;
+ current_size: IndentSize,
+ new_size: IndentSize,
+ ) -> Option<(Range<Point>, String)> {
+ if new_size.kind != current_size.kind && current_size.len > 0 {
+ return None;
}
- if size.len > current_size.len {
- let offset = Point::new(row, 0).to_offset(&*self);
- self.edit(
- [(
- offset..offset,
- iter::repeat(size.char())
- .take((size.len - current_size.len) as usize)
- .collect::<String>(),
- )],
- None,
- cx,
- );
- } else if size.len < current_size.len {
- self.edit(
- [(
- Point::new(row, 0)..Point::new(row, current_size.len - size.len),
- "",
- )],
- None,
- cx,
- );
+ if new_size.len > current_size.len {
+ let point = Point::new(row, 0);
+ Some((
+ point..point,
+ iter::repeat(new_size.char())
+ .take((new_size.len - current_size.len) as usize)
+ .collect::<String>(),
+ ))
+ } else if new_size.len < current_size.len {
+ Some((
+ Point::new(row, 0)..Point::new(row, current_size.len - new_size.len),
+ String::new(),
+ ))
+ } else {
+ None
}
}
@@ -1225,13 +1222,7 @@ impl Buffer {
let edit_id = edit_operation.local_timestamp();
if let Some((before_edit, mode)) = autoindent_request {
- let language_name = self.language().map(|language| language.name());
- let settings = cx.global::<Settings>();
- let indent_size = if settings.hard_tabs(language_name.as_deref()) {
- IndentSize::tab()
- } else {
- IndentSize::spaces(settings.tab_size(language_name.as_deref()).get())
- };
+ let indent_size = before_edit.single_indent_size(cx);
let (start_columns, is_block_mode) = match mode {
AutoindentMode::Block {
original_indent_columns: start_columns,
@@ -1611,6 +1602,47 @@ impl BufferSnapshot {
indent_size_for_line(&self, row)
}
+ pub fn single_indent_size(&self, cx: &AppContext) -> IndentSize {
+ let language_name = self.language().map(|language| language.name());
+ let settings = cx.global::<Settings>();
+ if settings.hard_tabs(language_name.as_deref()) {
+ IndentSize::tab()
+ } else {
+ IndentSize::spaces(settings.tab_size(language_name.as_deref()).get())
+ }
+ }
+
+ pub fn suggested_indents(
+ &self,
+ rows: impl Iterator<Item = u32>,
+ single_indent_size: IndentSize,
+ ) -> BTreeMap<u32, IndentSize> {
+ let mut result = BTreeMap::new();
+
+ for row_range in contiguous_ranges(rows, 10) {
+ let suggestions = match self.suggest_autoindents(row_range.clone()) {
+ Some(suggestions) => suggestions,
+ _ => break,
+ };
+
+ for (row, suggestion) in row_range.zip(suggestions) {
+ let indent_size = if let Some(suggestion) = suggestion {
+ result
+ .get(&suggestion.basis_row)
+ .copied()
+ .unwrap_or_else(|| self.indent_size_for_line(suggestion.basis_row))
+ .with_delta(suggestion.delta, single_indent_size)
+ } else {
+ self.indent_size_for_line(row)
+ };
+
+ result.insert(row, indent_size);
+ }
+ }
+
+ result
+ }
+
fn suggest_autoindents<'a>(
&'a self,
row_range: Range<u32>,