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,55 +2913,125 @@ impl Editor {
return; return;
} }
let mut selections = self.selections.all_adjusted(cx); let selections = self.selections.all_adjusted(cx);
if selections.iter().all(|s| s.is_empty()) { let buffer = self.buffer.read(cx).read(cx);
self.transact(cx, |this, cx| { let suggested_indents =
this.buffer.update(cx, |buffer, cx| { buffer.suggested_indents(selections.iter().map(|s| s.head().row), cx);
let mut prev_cursor_row = 0; let mut all_selections_empty = true;
let mut row_delta = 0; let mut all_cursors_before_suggested_indent = true;
for selection in &mut selections { let mut all_cursors_in_leading_whitespace = true;
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()); for selection in &selections {
let settings = cx.global::<Settings>(); let cursor = selection.head();
let tab_size = if settings.hard_tabs(language_name.as_deref()) { if !selection.is_empty() {
IndentSize::tab() all_selections_empty = false;
} else { }
let tab_size = settings.tab_size(language_name.as_deref()).get(); if cursor.column > buffer.indent_size_for_line(cursor.row).len {
let char_column = buffer all_cursors_in_leading_whitespace = false;
.read(cx) }
.text_for_range(Point::new(cursor.row, 0)..cursor) if let Some(indent) = suggested_indents.get(&cursor.row) {
.flat_map(str::chars) if cursor.column >= indent.len {
.count(); all_cursors_before_suggested_indent = false;
let chars_to_next_tab_stop = tab_size - (char_column as u32 % tab_size); }
IndentSize::spaces(chars_to_next_tab_stop) } else {
}; all_cursors_before_suggested_indent = false;
buffer.edit( }
[(cursor..cursor, tab_size.chars().collect::<String>())], }
None, drop(buffer);
cx,
);
cursor.column += tab_size.len;
selection.start = cursor;
selection.end = cursor;
row_delta += tab_size.len; if all_selections_empty {
} if all_cursors_in_leading_whitespace && all_cursors_before_suggested_indent {
}); self.auto_indent(suggested_indents, selections, cx);
this.change_selections(Some(Autoscroll::Fit), cx, |s| { } else {
s.select(selections); self.insert_tab(selections, cx);
}); }
});
} else { } else {
self.indent(&Indent, cx); 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>) { pub fn indent(&mut self, _: &Indent, cx: &mut ViewContext<Self>) {
let mut selections = self.selections.all::<Point>(cx); let mut selections = self.selections.all::<Point>(cx);
self.transact(cx, |this, 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] #[gpui::test]
async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
let mut cx = EditorTestContext::new(cx).await; let mut cx = EditorTestContext::new(cx).await;

View file

@ -3,7 +3,7 @@ mod anchor;
pub use anchor::{Anchor, AnchorRangeExt}; pub use anchor::{Anchor, AnchorRangeExt};
use anyhow::Result; use anyhow::Result;
use clock::ReplicaId; use clock::ReplicaId;
use collections::{Bound, HashMap, HashSet}; use collections::{BTreeMap, Bound, HashMap, HashSet};
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
pub use language::Completion; pub use language::Completion;
use language::{ 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 { pub fn indent_size_for_line(&self, row: u32) -> IndentSize {
if let Some((buffer, range)) = self.buffer_line_for_row(row) { if let Some((buffer, range)) = self.buffer_line_for_row(row) {
let mut size = buffer.indent_size_for_line(range.start.row); let mut size = buffer.indent_size_for_line(range.start.row);

View file

@ -957,45 +957,42 @@ impl Buffer {
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
self.autoindent_requests.clear(); self.autoindent_requests.clear();
self.start_transaction();
for (row, indent_size) in &indent_sizes { let edits: Vec<_> = indent_sizes
self.set_indent_size_for_line(*row, *indent_size, cx); .into_iter()
} .filter_map(|(row, indent_size)| {
self.end_transaction(cx); 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( pub fn edit_for_indent_size_adjustment(
&mut self,
row: u32, row: u32,
size: IndentSize, current_size: IndentSize,
cx: &mut ModelContext<Self>, new_size: IndentSize,
) { ) -> Option<(Range<Point>, String)> {
let current_size = indent_size_for_line(&self, row); if new_size.kind != current_size.kind && current_size.len > 0 {
if size.kind != current_size.kind && current_size.len > 0 { return None;
return;
} }
if size.len > current_size.len { if new_size.len > current_size.len {
let offset = Point::new(row, 0).to_offset(&*self); let point = Point::new(row, 0);
self.edit( Some((
[( point..point,
offset..offset, iter::repeat(new_size.char())
iter::repeat(size.char()) .take((new_size.len - current_size.len) as usize)
.take((size.len - current_size.len) as usize) .collect::<String>(),
.collect::<String>(), ))
)], } else if new_size.len < current_size.len {
None, Some((
cx, Point::new(row, 0)..Point::new(row, current_size.len - new_size.len),
); String::new(),
} else if size.len < current_size.len { ))
self.edit( } else {
[( None
Point::new(row, 0)..Point::new(row, current_size.len - size.len),
"",
)],
None,
cx,
);
} }
} }
@ -1225,13 +1222,7 @@ impl Buffer {
let edit_id = edit_operation.local_timestamp(); let edit_id = edit_operation.local_timestamp();
if let Some((before_edit, mode)) = autoindent_request { if let Some((before_edit, mode)) = autoindent_request {
let language_name = self.language().map(|language| language.name()); let indent_size = before_edit.single_indent_size(cx);
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 (start_columns, is_block_mode) = match mode { let (start_columns, is_block_mode) = match mode {
AutoindentMode::Block { AutoindentMode::Block {
original_indent_columns: start_columns, original_indent_columns: start_columns,
@ -1611,6 +1602,47 @@ impl BufferSnapshot {
indent_size_for_line(&self, row) 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>( fn suggest_autoindents<'a>(
&'a self, &'a self,
row_range: Range<u32>, row_range: Range<u32>,