From 115677ec5df4e184e73ead073a56523ca9a48cae Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 1 Aug 2022 14:07:47 -0700 Subject: [PATCH] Start work on auto-indenting lines on tab Co-authored-by: Julia Risley --- crates/editor/src/editor.rs | 204 ++++++++++++++++++++++++------ crates/editor/src/multi_buffer.rs | 49 ++++++- crates/language/src/buffer.rs | 114 +++++++++++------ 3 files changed, 283 insertions(+), 84 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 4ca4533ffee7725d538460c8b1186f3a97fe8c26..4859faf058d62be73e564d0a9cbce72faa861437 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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::(); - 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::())], - 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, + mut selections: Vec>, + cx: &mut ViewContext, + ) { + 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>, cx: &mut ViewContext) { + 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::(); + 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::())], + 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) { let mut selections = self.selections.all::(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; diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 1fc7cf0560c262106aad9f63a6504d4ea57f40b0..b9e8b7e6829b05c6d82a87d2677ac8cc8098d8e7 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -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, + cx: &AppContext, + ) -> BTreeMap { + let mut result = BTreeMap::new(); + + let mut rows_for_excerpt = Vec::new(); + let mut cursor = self.excerpts.cursor::(); + + 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); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 9ad27cca792c11136218b62d7e9df35dfae2ea3a..d35a622522930b3bf215b42dd4ce1a3fd3097c55 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -957,45 +957,42 @@ impl Buffer { cx: &mut ModelContext, ) { 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, - ) { - 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, 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::(), - )], - 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::(), + )) + } 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::(); - 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::(); + 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, + single_indent_size: IndentSize, + ) -> BTreeMap { + 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,