Restructure autoindent to preserve relative indentation of inserted text

This commit is contained in:
Max Brunsfeld 2022-07-26 17:43:43 -07:00
parent 537530bf76
commit f547c268ce
2 changed files with 137 additions and 82 deletions

View file

@ -231,11 +231,16 @@ struct SyntaxTree {
#[derive(Clone)] #[derive(Clone)]
struct AutoindentRequest { struct AutoindentRequest {
before_edit: BufferSnapshot, before_edit: BufferSnapshot,
edited: Vec<Anchor>, entries: Vec<AutoindentRequestEntry>,
inserted: Vec<Range<Anchor>>,
indent_size: IndentSize, indent_size: IndentSize,
} }
#[derive(Clone)]
struct AutoindentRequestEntry {
range: Range<Anchor>,
first_line_is_new: bool,
}
#[derive(Debug)] #[derive(Debug)]
struct IndentSuggestion { struct IndentSuggestion {
basis_row: u32, basis_row: u32,
@ -796,17 +801,20 @@ impl Buffer {
Some(async move { Some(async move {
let mut indent_sizes = BTreeMap::new(); let mut indent_sizes = BTreeMap::new();
for request in autoindent_requests { for request in autoindent_requests {
let old_to_new_rows = request let mut row_ranges = Vec::new();
.edited let mut old_to_new_rows = BTreeMap::new();
.iter() for entry in &request.entries {
.map(|anchor| anchor.summary::<Point>(&request.before_edit).row) let position = entry.range.start;
.zip( let new_row = position.to_point(&snapshot).row;
request let new_end_row = entry.range.end.to_point(&snapshot).row + 1;
.edited if !entry.first_line_is_new {
.iter() let old_row = position.to_point(&request.before_edit).row;
.map(|anchor| anchor.summary::<Point>(&snapshot).row), old_to_new_rows.insert(old_row, new_row);
) }
.collect::<BTreeMap<u32, u32>>(); if new_end_row > new_row {
row_ranges.push(new_row..new_end_row);
}
}
let mut old_suggestions = BTreeMap::<u32, IndentSize>::default(); let mut old_suggestions = BTreeMap::<u32, IndentSize>::default();
let old_edited_ranges = let old_edited_ranges =
@ -835,10 +843,13 @@ impl Buffer {
yield_now().await; yield_now().await;
} }
// At this point, old_suggestions contains the suggested indentation for all edited lines with respect to the state of the // At this point, old_suggestions contains the suggested indentation for all edited lines
// buffer before the edit, but keyed by the row for these lines after the edits were applied. // with respect to the state of the buffer before the edit, but keyed by the row for these
let new_edited_row_ranges = // lines after the edits were applied.
contiguous_ranges(old_to_new_rows.values().copied(), max_rows_between_yields); let new_edited_row_ranges = contiguous_ranges(
row_ranges.iter().map(|range| range.start),
max_rows_between_yields,
);
for new_edited_row_range in new_edited_row_ranges { for new_edited_row_range in new_edited_row_ranges {
let suggestions = snapshot let suggestions = snapshot
.suggest_autoindents(new_edited_row_range.clone()) .suggest_autoindents(new_edited_row_range.clone())
@ -866,32 +877,31 @@ impl Buffer {
yield_now().await; yield_now().await;
} }
let inserted_row_ranges = contiguous_ranges( for row_range in row_ranges {
request if row_range.len() > 1 {
.inserted if let Some(new_indent_size) = indent_sizes.get(&row_range.start).copied() {
.iter() let old_indent_size = snapshot.indent_size_for_line(row_range.start);
.map(|range| range.to_point(&snapshot)) if new_indent_size.kind == old_indent_size.kind {
.flat_map(|range| range.start.row..range.end.row + 1), let delta = new_indent_size.len as i64 - old_indent_size.len as i64;
max_rows_between_yields, if delta != 0 {
); for row in row_range.skip(1) {
for inserted_row_range in inserted_row_ranges { indent_sizes.entry(row).or_insert_with(|| {
let suggestions = snapshot let mut size = snapshot.indent_size_for_line(row);
.suggest_autoindents(inserted_row_range.clone()) if size.kind == new_indent_size.kind {
.into_iter() if delta > 0 {
.flatten(); size.len += delta as u32;
for (row, suggestion) in inserted_row_range.zip(suggestions) { } else if delta < 0 {
if let Some(suggestion) = suggestion { size.len =
let suggested_indent = indent_sizes size.len.saturating_sub(-delta as u32);
.get(&suggestion.basis_row) }
.copied() }
.unwrap_or_else(|| { size
snapshot.indent_size_for_line(suggestion.basis_row) });
}) }
.with_delta(suggestion.delta, request.indent_size); }
indent_sizes.insert(row, suggested_indent); }
} }
} }
yield_now().await;
} }
} }
@ -1200,56 +1210,54 @@ impl Buffer {
let edit_id = edit_operation.local_timestamp(); let edit_id = edit_operation.local_timestamp();
if let Some((before_edit, size)) = autoindent_request { if let Some((before_edit, size)) = autoindent_request {
let mut inserted = Vec::new();
let mut edited = Vec::new();
let mut delta = 0isize; let mut delta = 0isize;
for ((range, _), new_text) in edits let entries = edits
.into_iter() .into_iter()
.zip(&edit_operation.as_edit().unwrap().new_text) .zip(&edit_operation.as_edit().unwrap().new_text)
{ .map(|((range, _), new_text)| {
let new_text_len = new_text.len(); let new_text_len = new_text.len();
let first_newline_ix = new_text.find('\n'); let first_newline_ix = new_text.find('\n');
let old_start = range.start.to_point(&before_edit); let old_start = range.start.to_point(&before_edit);
let new_start = (delta + range.start as isize) as usize;
let start = (delta + range.start as isize) as usize;
delta += new_text_len as isize - (range.end as isize - range.start as isize); delta += new_text_len as isize - (range.end as isize - range.start as isize);
let mut relative_range = 0..new_text_len;
let mut first_line_is_new = false;
// When inserting multiple lines of text at the beginning of a line, // When inserting multiple lines of text at the beginning of a line,
// treat all of the affected lines as newly-inserted. // treat the insertion as new.
if first_newline_ix.is_some() if first_newline_ix.is_some()
&& old_start.column < before_edit.indent_size_for_line(old_start.row).len && old_start.column < before_edit.indent_size_for_line(old_start.row).len
{ {
inserted first_line_is_new = true;
.push(self.anchor_before(start)..self.anchor_after(start + new_text_len));
continue;
} }
// When inserting a newline at the end of an existing line, avoid
// When inserting a newline at the end of an existing line, treat the following // auto-indenting that existing line, but treat the subsequent text as new.
// line as newly-inserted. else if first_newline_ix == Some(0)
if first_newline_ix == Some(0)
&& old_start.column == before_edit.line_len(old_start.row) && old_start.column == before_edit.line_len(old_start.row)
{ {
inserted.push( relative_range.start += 1;
self.anchor_before(start + 1)..self.anchor_after(start + new_text_len), first_line_is_new = true;
); }
continue; // Avoid auto-indenting subsequent lines when inserting text with trailing
// newlines
while !relative_range.is_empty()
&& new_text[relative_range.clone()].ends_with('\n')
{
relative_range.end -= 1;
} }
// Otherwise, mark the start of the edit as edited, and any subsequent AutoindentRequestEntry {
// lines as newly inserted. first_line_is_new,
edited.push(before_edit.anchor_before(range.start)); range: before_edit.anchor_before(new_start + relative_range.start)
if let Some(ix) = first_newline_ix { ..self.anchor_after(new_start + relative_range.end),
inserted.push(
self.anchor_before(start + ix + 1)..self.anchor_after(start + new_text_len),
);
}
} }
})
.collect();
self.autoindent_requests.push(Arc::new(AutoindentRequest { self.autoindent_requests.push(Arc::new(AutoindentRequest {
before_edit, before_edit,
edited, entries,
inserted,
indent_size: size, indent_size: size,
})); }));
} }

View file

@ -899,6 +899,53 @@ fn test_autoindent_multi_line_insertion(cx: &mut MutableAppContext) {
}); });
} }
#[gpui::test]
fn test_autoindent_preserves_relative_indentation_in_multi_line_insertion(
cx: &mut MutableAppContext,
) {
cx.add_model(|cx| {
let text = "
fn a() {
b();
}
"
.unindent();
let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
let pasted_text = r#"
"
c
d
e
"
"#
.unindent();
// insert at the beginning of a line
buffer.edit_with_autoindent(
[(Point::new(2, 0)..Point::new(2, 0), pasted_text.clone())],
IndentSize::spaces(4),
cx,
);
assert_eq!(
buffer.text(),
r#"
fn a() {
b();
"
c
d
e
"
}
"#
.unindent()
);
buffer
});
}
#[gpui::test] #[gpui::test]
fn test_autoindent_disabled(cx: &mut MutableAppContext) { fn test_autoindent_disabled(cx: &mut MutableAppContext) {
cx.add_model(|cx| { cx.add_model(|cx| {