Start work on auto-indenting lines on tab

Co-authored-by: Julia Risley <floc@unpromptedtirade.com>
This commit is contained in:
Max Brunsfeld 2022-08-01 14:07:47 -07:00
parent 33638c0c11
commit 115677ec5d
3 changed files with 283 additions and 84 deletions

View file

@ -2913,8 +2913,80 @@ impl Editor {
return;
}
let mut selections = self.selections.all_adjusted(cx);
if selections.iter().all(|s| s.is_empty()) {
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;
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);
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;
@ -2941,6 +3013,7 @@ impl Editor {
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,
@ -2957,9 +3030,6 @@ impl Editor {
s.select(selections);
});
});
} else {
self.indent(&Indent, cx);
}
}
pub fn indent(&mut self, _: &Indent, cx: &mut ViewContext<Self>) {
@ -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;

View file

@ -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);

View file

@ -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);
}
fn set_indent_size_for_line(
&mut self,
row: u32,
size: IndentSize,
cx: &mut ModelContext<Self>,
) {
let edits: Vec<_> = indent_sizes
.into_iter()
.filter_map(|(row, indent_size)| {
let current_size = indent_size_for_line(&self, row);
if size.kind != current_size.kind && current_size.len > 0 {
return;
Self::edit_for_indent_size_adjustment(row, current_size, indent_size)
})
.collect();
self.edit(edits, None, cx);
}
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)
pub fn edit_for_indent_size_adjustment(
row: u32,
current_size: IndentSize,
new_size: IndentSize,
) -> Option<(Range<Point>, String)> {
if new_size.kind != current_size.kind && current_size.len > 0 {
return None;
}
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>(),
)],
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,
);
))
} 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>,