From 09ed149184356e7e88ab7d4a9117f7511c7af20d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 20 Jul 2022 09:07:54 -0700 Subject: [PATCH 1/9] Improve calculation of which lines are new when auto-indenting --- crates/language/src/buffer.rs | 92 +++++++++++++++++++---------------- crates/language/src/tests.rs | 73 +++++++++++++++++++++++++-- 2 files changed, 121 insertions(+), 44 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index bff9438124313f53d961c4e1bf0cee5f3c9cee35..e2685ba36e7fc4a25fbd76a4a4e4e56611296e80 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -232,7 +232,7 @@ struct SyntaxTree { struct AutoindentRequest { before_edit: BufferSnapshot, edited: Vec, - inserted: Option>>, + inserted: Vec>, indent_size: IndentSize, } @@ -874,9 +874,10 @@ impl Buffer { yield_now().await; } - if let Some(inserted) = request.inserted.as_ref() { + if !request.inserted.is_empty() { let inserted_row_ranges = contiguous_ranges( - inserted + request + .inserted .iter() .map(|range| range.to_point(&snapshot)) .flat_map(|range| range.start.row..range.end.row + 1), @@ -1203,52 +1204,61 @@ impl Buffer { self.start_transaction(); self.pending_autoindent.take(); - let autoindent_request = - self.language - .as_ref() - .and_then(|_| autoindent_size) - .map(|autoindent_size| { - let before_edit = self.snapshot(); - let edited = edits - .iter() - .filter_map(|(range, new_text)| { - let start = range.start.to_point(self); - if new_text.starts_with('\n') - && start.column == self.line_len(start.row) - { - None - } else { - Some(self.anchor_before(range.start)) - } - }) - .collect(); - (before_edit, edited, autoindent_size) - }); + let autoindent_request = self + .language + .as_ref() + .and_then(|_| autoindent_size) + .map(|autoindent_size| (self.snapshot(), autoindent_size)); let edit_operation = self.text.edit(edits.iter().cloned()); let edit_id = edit_operation.local_timestamp(); - if let Some((before_edit, edited, size)) = autoindent_request { - let mut delta = 0isize; + if let Some((before_edit, size)) = autoindent_request { + let mut inserted = Vec::new(); + let mut edited = Vec::new(); - let inserted_ranges = edits + let mut delta = 0isize; + for ((range, _), new_text) in edits .into_iter() .zip(&edit_operation.as_edit().unwrap().new_text) - .filter_map(|((range, _), new_text)| { - let first_newline_ix = new_text.find('\n')?; - let new_text_len = new_text.len(); - let start = (delta + range.start as isize) as usize + first_newline_ix + 1; - let end = (delta + range.start as isize) as usize + new_text_len; - delta += new_text_len as isize - (range.end as isize - range.start as isize); - Some(self.anchor_before(start)..self.anchor_after(end)) - }) - .collect::>>(); + { + let new_text_len = new_text.len(); + let first_newline_ix = new_text.find('\n'); + let old_start = range.start.to_point(&before_edit); - let inserted = if inserted_ranges.is_empty() { - None - } else { - Some(inserted_ranges) - }; + let start = (delta + range.start as isize) as usize; + delta += new_text_len as isize - (range.end as isize - range.start as isize); + + // When inserting multiple lines of text at the beginning of a line, + // treat all of the affected lines as newly-inserted. + if first_newline_ix.is_some() + && old_start.column < before_edit.indent_size_for_line(old_start.row).len + { + inserted + .push(self.anchor_before(start)..self.anchor_after(start + new_text_len)); + continue; + } + + // When inserting a newline at the end of an existing line, treat the following + // line as newly-inserted. + if first_newline_ix == Some(0) + && old_start.column == before_edit.line_len(old_start.row) + { + inserted.push( + self.anchor_before(start + 1)..self.anchor_after(start + new_text_len), + ); + continue; + } + + // Otherwise, mark the start of the edit as edited, and any subsequent + // lines as newly inserted. + edited.push(before_edit.anchor_before(range.start)); + if let Some(ix) = first_newline_ix { + inserted.push( + self.anchor_before(start + ix + 1)..self.anchor_after(start + new_text_len), + ); + } + } self.autoindent_requests.push(Arc::new(AutoindentRequest { before_edit, diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index ac3759c257b21f7deafcd8ef011805208fc9699a..3f1f0f1b8ce92741d99a312835fada5bfda17f09 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -756,8 +756,18 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta }); cx.add_model(|cx| { - let text = "fn a() {\n {\n b()?\n }\n\n Ok(())\n}"; + let text = " + fn a() { + { + b()? + } + Ok(()) + } + " + .unindent(); let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + + // Delete a closing curly brace changes the suggested indent for the line. buffer.edit_with_autoindent( [(Point::new(3, 4)..Point::new(3, 5), "")], IndentSize::spaces(4), @@ -765,9 +775,19 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta ); assert_eq!( buffer.text(), - "fn a() {\n {\n b()?\n \n\n Ok(())\n}" + " + fn a() { + { + b()? + | + Ok(()) + } + " + .replace("|", "") // included in the string to preserve trailing whites + .unindent() ); + // Manually editing the leading whitespace buffer.edit_with_autoindent( [(Point::new(3, 0)..Point::new(3, 12), "")], IndentSize::spaces(4), @@ -775,7 +795,15 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta ); assert_eq!( buffer.text(), - "fn a() {\n {\n b()?\n\n\n Ok(())\n}" + " + fn a() { + { + b()? + + Ok(()) + } + " + .unindent() ); buffer }); @@ -832,6 +860,45 @@ fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut MutableAppContext) { }); } +#[gpui::test] +fn test_autoindent_multi_line_insertion(cx: &mut MutableAppContext) { + cx.add_model(|cx| { + let text = " + const a: usize = 1; + fn b() { + if c { + let d = 2; + } + } + " + .unindent(); + + let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + buffer.edit_with_autoindent( + [(Point::new(3, 0)..Point::new(3, 0), "e(\n f()\n);\n")], + IndentSize::spaces(4), + cx, + ); + assert_eq!( + buffer.text(), + " + const a: usize = 1; + fn b() { + if c { + e( + f() + ); + let d = 2; + } + } + " + .unindent() + ); + + buffer + }); +} + #[gpui::test] fn test_autoindent_disabled(cx: &mut MutableAppContext) { cx.add_model(|cx| { From 537530bf76a011dbda1982203276302be68bc60b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 26 Jul 2022 08:56:22 -0700 Subject: [PATCH 2/9] :art: compute_autoindents --- crates/language/src/buffer.rs | 109 +++++++++++++++------------------- 1 file changed, 48 insertions(+), 61 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index e2685ba36e7fc4a25fbd76a4a4e4e56611296e80..c349ed50f27951b217d871173afa95b551f50bb2 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -19,7 +19,7 @@ use smol::future::yield_now; use std::{ any::Any, cmp::{self, Ordering}, - collections::{BTreeMap, HashMap}, + collections::BTreeMap, ffi::OsStr, future::Future, iter::{self, Iterator, Peekable}, @@ -808,7 +808,7 @@ impl Buffer { ) .collect::>(); - let mut old_suggestions = HashMap::::default(); + let mut old_suggestions = BTreeMap::::default(); let old_edited_ranges = contiguous_ranges(old_to_new_rows.keys().copied(), max_rows_between_yields); for old_edited_range in old_edited_ranges { @@ -819,19 +819,15 @@ impl Buffer { .flatten(); for (old_row, suggestion) in old_edited_range.zip(suggestions) { if let Some(suggestion) = suggestion { - let mut suggested_indent = old_to_new_rows + let suggested_indent = old_to_new_rows .get(&suggestion.basis_row) .and_then(|from_row| old_suggestions.get(from_row).copied()) .unwrap_or_else(|| { request .before_edit .indent_size_for_line(suggestion.basis_row) - }); - if suggestion.delta.is_gt() { - suggested_indent += request.indent_size; - } else if suggestion.delta.is_lt() { - suggested_indent -= request.indent_size; - } + }) + .with_delta(suggestion.delta, request.indent_size); old_suggestions .insert(*old_to_new_rows.get(&old_row).unwrap(), suggested_indent); } @@ -850,17 +846,13 @@ impl Buffer { .flatten(); for (new_row, suggestion) in new_edited_row_range.zip(suggestions) { if let Some(suggestion) = suggestion { - let mut suggested_indent = indent_sizes + let suggested_indent = indent_sizes .get(&suggestion.basis_row) .copied() .unwrap_or_else(|| { snapshot.indent_size_for_line(suggestion.basis_row) - }); - if suggestion.delta.is_gt() { - suggested_indent += request.indent_size; - } else if suggestion.delta.is_lt() { - suggested_indent -= request.indent_size; - } + }) + .with_delta(suggestion.delta, request.indent_size); if old_suggestions .get(&new_row) .map_or(true, |old_indentation| { @@ -874,38 +866,32 @@ impl Buffer { yield_now().await; } - if !request.inserted.is_empty() { - let inserted_row_ranges = contiguous_ranges( - request - .inserted - .iter() - .map(|range| range.to_point(&snapshot)) - .flat_map(|range| range.start.row..range.end.row + 1), - max_rows_between_yields, - ); - for inserted_row_range in inserted_row_ranges { - let suggestions = snapshot - .suggest_autoindents(inserted_row_range.clone()) - .into_iter() - .flatten(); - for (row, suggestion) in inserted_row_range.zip(suggestions) { - if let Some(suggestion) = suggestion { - let mut suggested_indent = indent_sizes - .get(&suggestion.basis_row) - .copied() - .unwrap_or_else(|| { - snapshot.indent_size_for_line(suggestion.basis_row) - }); - if suggestion.delta.is_gt() { - suggested_indent += request.indent_size; - } else if suggestion.delta.is_lt() { - suggested_indent -= request.indent_size; - } - indent_sizes.insert(row, suggested_indent); - } + let inserted_row_ranges = contiguous_ranges( + request + .inserted + .iter() + .map(|range| range.to_point(&snapshot)) + .flat_map(|range| range.start.row..range.end.row + 1), + max_rows_between_yields, + ); + for inserted_row_range in inserted_row_ranges { + let suggestions = snapshot + .suggest_autoindents(inserted_row_range.clone()) + .into_iter() + .flatten(); + for (row, suggestion) in inserted_row_range.zip(suggestions) { + if let Some(suggestion) = suggestion { + let suggested_indent = indent_sizes + .get(&suggestion.basis_row) + .copied() + .unwrap_or_else(|| { + snapshot.indent_size_for_line(suggestion.basis_row) + }) + .with_delta(suggestion.delta, request.indent_size); + indent_sizes.insert(row, suggested_indent); } - yield_now().await; } + yield_now().await; } } @@ -2513,23 +2499,24 @@ impl IndentSize { IndentKind::Tab => '\t', } } -} - -impl std::ops::AddAssign for IndentSize { - fn add_assign(&mut self, other: IndentSize) { - if self.len == 0 { - *self = other; - } else if self.kind == other.kind { - self.len += other.len; - } - } -} -impl std::ops::SubAssign for IndentSize { - fn sub_assign(&mut self, other: IndentSize) { - if self.kind == other.kind && self.len >= other.len { - self.len -= other.len; + pub fn with_delta(mut self, direction: Ordering, size: IndentSize) -> Self { + match direction { + Ordering::Less => { + if self.kind == size.kind && self.len >= size.len { + self.len -= size.len; + } + } + Ordering::Equal => {} + Ordering::Greater => { + if self.len == 0 { + self = size; + } else if self.kind == size.kind { + self.len += size.len; + } + } } + self } } From f547c268ced1b1a84b57cf4f29574084298f66e7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 26 Jul 2022 17:43:43 -0700 Subject: [PATCH 3/9] Restructure autoindent to preserve relative indentation of inserted text --- crates/language/src/buffer.rs | 176 ++++++++++++++++++---------------- crates/language/src/tests.rs | 47 +++++++++ 2 files changed, 139 insertions(+), 84 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index c349ed50f27951b217d871173afa95b551f50bb2..ef9b2663152e1f4690d1ac33875cf7b0e4bb2038 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -231,11 +231,16 @@ struct SyntaxTree { #[derive(Clone)] struct AutoindentRequest { before_edit: BufferSnapshot, - edited: Vec, - inserted: Vec>, + entries: Vec, indent_size: IndentSize, } +#[derive(Clone)] +struct AutoindentRequestEntry { + range: Range, + first_line_is_new: bool, +} + #[derive(Debug)] struct IndentSuggestion { basis_row: u32, @@ -796,17 +801,20 @@ impl Buffer { Some(async move { let mut indent_sizes = BTreeMap::new(); for request in autoindent_requests { - let old_to_new_rows = request - .edited - .iter() - .map(|anchor| anchor.summary::(&request.before_edit).row) - .zip( - request - .edited - .iter() - .map(|anchor| anchor.summary::(&snapshot).row), - ) - .collect::>(); + let mut row_ranges = Vec::new(); + let mut old_to_new_rows = BTreeMap::new(); + for entry in &request.entries { + let position = entry.range.start; + let new_row = position.to_point(&snapshot).row; + let new_end_row = entry.range.end.to_point(&snapshot).row + 1; + if !entry.first_line_is_new { + let old_row = position.to_point(&request.before_edit).row; + old_to_new_rows.insert(old_row, new_row); + } + if new_end_row > new_row { + row_ranges.push(new_row..new_end_row); + } + } let mut old_suggestions = BTreeMap::::default(); let old_edited_ranges = @@ -835,10 +843,13 @@ impl Buffer { yield_now().await; } - // At this point, old_suggestions contains the suggested indentation for all edited lines with respect to the state of the - // buffer before the edit, but keyed by the row for these lines after the edits were applied. - let new_edited_row_ranges = - contiguous_ranges(old_to_new_rows.values().copied(), max_rows_between_yields); + // At this point, old_suggestions contains the suggested indentation for all edited lines + // with respect to the state of the buffer before the edit, but keyed by the row for these + // lines after the edits were applied. + 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 { let suggestions = snapshot .suggest_autoindents(new_edited_row_range.clone()) @@ -866,32 +877,31 @@ impl Buffer { yield_now().await; } - let inserted_row_ranges = contiguous_ranges( - request - .inserted - .iter() - .map(|range| range.to_point(&snapshot)) - .flat_map(|range| range.start.row..range.end.row + 1), - max_rows_between_yields, - ); - for inserted_row_range in inserted_row_ranges { - let suggestions = snapshot - .suggest_autoindents(inserted_row_range.clone()) - .into_iter() - .flatten(); - for (row, suggestion) in inserted_row_range.zip(suggestions) { - if let Some(suggestion) = suggestion { - let suggested_indent = indent_sizes - .get(&suggestion.basis_row) - .copied() - .unwrap_or_else(|| { - snapshot.indent_size_for_line(suggestion.basis_row) - }) - .with_delta(suggestion.delta, request.indent_size); - indent_sizes.insert(row, suggested_indent); + for row_range in row_ranges { + if row_range.len() > 1 { + if let Some(new_indent_size) = indent_sizes.get(&row_range.start).copied() { + let old_indent_size = snapshot.indent_size_for_line(row_range.start); + if new_indent_size.kind == old_indent_size.kind { + let delta = new_indent_size.len as i64 - old_indent_size.len as i64; + if delta != 0 { + for row in row_range.skip(1) { + indent_sizes.entry(row).or_insert_with(|| { + let mut size = snapshot.indent_size_for_line(row); + if size.kind == new_indent_size.kind { + if delta > 0 { + size.len += delta as u32; + } else if delta < 0 { + size.len = + size.len.saturating_sub(-delta as u32); + } + } + size + }); + } + } + } } } - yield_now().await; } } @@ -1200,56 +1210,54 @@ impl Buffer { let edit_id = edit_operation.local_timestamp(); if let Some((before_edit, size)) = autoindent_request { - let mut inserted = Vec::new(); - let mut edited = Vec::new(); - let mut delta = 0isize; - for ((range, _), new_text) in edits + let entries = edits .into_iter() .zip(&edit_operation.as_edit().unwrap().new_text) - { - let new_text_len = new_text.len(); - let first_newline_ix = new_text.find('\n'); - let old_start = range.start.to_point(&before_edit); - - let start = (delta + range.start as isize) as usize; - delta += new_text_len as isize - (range.end as isize - range.start as isize); - - // When inserting multiple lines of text at the beginning of a line, - // treat all of the affected lines as newly-inserted. - if first_newline_ix.is_some() - && old_start.column < before_edit.indent_size_for_line(old_start.row).len - { - inserted - .push(self.anchor_before(start)..self.anchor_after(start + new_text_len)); - continue; - } - - // When inserting a newline at the end of an existing line, treat the following - // line as newly-inserted. - if first_newline_ix == Some(0) - && old_start.column == before_edit.line_len(old_start.row) - { - inserted.push( - self.anchor_before(start + 1)..self.anchor_after(start + new_text_len), - ); - continue; - } + .map(|((range, _), new_text)| { + let new_text_len = new_text.len(); + let first_newline_ix = new_text.find('\n'); + let old_start = range.start.to_point(&before_edit); + let new_start = (delta + range.start as isize) as usize; + 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, + // treat the insertion as new. + if first_newline_ix.is_some() + && old_start.column < before_edit.indent_size_for_line(old_start.row).len + { + first_line_is_new = true; + } + // When inserting a newline at the end of an existing line, avoid + // auto-indenting that existing line, but treat the subsequent text as new. + else if first_newline_ix == Some(0) + && old_start.column == before_edit.line_len(old_start.row) + { + relative_range.start += 1; + first_line_is_new = true; + } + // 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 - // lines as newly inserted. - edited.push(before_edit.anchor_before(range.start)); - if let Some(ix) = first_newline_ix { - inserted.push( - self.anchor_before(start + ix + 1)..self.anchor_after(start + new_text_len), - ); - } - } + AutoindentRequestEntry { + first_line_is_new, + range: before_edit.anchor_before(new_start + relative_range.start) + ..self.anchor_after(new_start + relative_range.end), + } + }) + .collect(); self.autoindent_requests.push(Arc::new(AutoindentRequest { before_edit, - edited, - inserted, + entries, indent_size: size, })); } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 3f1f0f1b8ce92741d99a312835fada5bfda17f09..3f721256cf94a5268b5a2c2f6df45f125c183f79 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -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] fn test_autoindent_disabled(cx: &mut MutableAppContext) { cx.add_model(|cx| { From b1b252ee45fedb2e6b7a7cff051282163d5ce997 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Jul 2022 11:51:03 -0700 Subject: [PATCH 4/9] Fix error in autoindent range calculation --- crates/language/src/buffer.rs | 36 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index ef9b2663152e1f4690d1ac33875cf7b0e4bb2038..8eef0a527424d7a8638a80a79cabbfca6d85cdd6 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -237,7 +237,12 @@ struct AutoindentRequest { #[derive(Clone)] struct AutoindentRequestEntry { + /// A range of the buffer whose indentation should be adjusted. range: Range, + /// Whether or not these lines should be considered brand new, for the + /// purpose of auto-indent. When text is not new, its indentation will + /// only be adjusted if the suggested indentation level has *changed* + /// since the edit was made. first_line_is_new: bool, } @@ -1216,41 +1221,34 @@ impl Buffer { .zip(&edit_operation.as_edit().unwrap().new_text) .map(|((range, _), new_text)| { let new_text_len = new_text.len(); - let first_newline_ix = new_text.find('\n'); let old_start = range.start.to_point(&before_edit); let new_start = (delta + range.start as isize) as usize; delta += new_text_len as isize - (range.end as isize - range.start as isize); - let mut relative_range = 0..new_text_len; + let mut range_of_insertion_to_indent = 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 an entire line at the beginning of an existing line, // treat the insertion as new. - if first_newline_ix.is_some() - && old_start.column < before_edit.indent_size_for_line(old_start.row).len + if new_text.contains('\n') + && old_start.column <= before_edit.indent_size_for_line(old_start.row).len { first_line_is_new = true; } - // When inserting a newline at the end of an existing line, avoid - // auto-indenting that existing line, but treat the subsequent text as new. - else if first_newline_ix == Some(0) - && old_start.column == before_edit.line_len(old_start.row) - { - relative_range.start += 1; + + // Avoid auto-indenting lines before and after the insertion. + if new_text[range_of_insertion_to_indent.clone()].starts_with('\n') { + range_of_insertion_to_indent.start += 1; first_line_is_new = true; } - // 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; + if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') { + range_of_insertion_to_indent.end -= 1; } AutoindentRequestEntry { first_line_is_new, - range: before_edit.anchor_before(new_start + relative_range.start) - ..self.anchor_after(new_start + relative_range.end), + range: self.anchor_before(new_start + range_of_insertion_to_indent.start) + ..self.anchor_after(new_start + range_of_insertion_to_indent.end), } }) .collect(); From cdf6ae25bbdb21ea1cb1906faa72631bd2f3a183 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Jul 2022 13:00:05 -0700 Subject: [PATCH 5/9] Remove `indent_size` parameter from `Buffer::edit_with_autoindent` Instead, compute the indent size by reading the settings inside that method. Co-authored-by: Mikayla Maki --- Cargo.lock | 1 + crates/editor/src/multi_buffer.rs | 23 ++----- crates/language/Cargo.toml | 1 + crates/language/src/buffer.rs | 9 ++- crates/language/src/tests.rs | 98 +++++++++++------------------- crates/zed/src/languages/c.rs | 15 +++-- crates/zed/src/languages/python.rs | 13 ++-- crates/zed/src/languages/rust.rs | 21 ++++--- 8 files changed, 77 insertions(+), 104 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ed860b9ecf9bcf5a8de2c82efe0080c6945caa2d..6a6ba400c7b2bb23a9f25f4239e1e3bb2c649d30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2761,6 +2761,7 @@ dependencies = [ "rpc", "serde", "serde_json", + "settings", "similar", "smallvec", "smol", diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 00ef7b11a0943d30deefc0c9a21f2e666da25c45..fa48cabf05cd379219e81199070577245796ef00 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -11,7 +11,6 @@ use language::{ IndentSize, Language, OffsetRangeExt, Outline, OutlineItem, Selection, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, }; -use settings::Settings; use smallvec::SmallVec; use std::{ borrow::Cow, @@ -347,14 +346,7 @@ impl MultiBuffer { if let Some(buffer) = self.as_singleton() { return buffer.update(cx, |buffer, cx| { if autoindent { - let language_name = buffer.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()) - }; - buffer.edit_with_autoindent(edits, indent_size, cx); + buffer.edit_with_autoindent(edits, cx); } else { buffer.edit(edits, cx); } @@ -471,18 +463,10 @@ impl MultiBuffer { )); } } - let language_name = buffer.language().map(|l| l.name()); if autoindent { - 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()) - }; - - buffer.edit_with_autoindent(deletions, indent_size, cx); - buffer.edit_with_autoindent(insertions, indent_size, cx); + buffer.edit_with_autoindent(deletions, cx); + buffer.edit_with_autoindent(insertions, cx); } else { buffer.edit(deletions, cx); buffer.edit(insertions, cx); @@ -3220,6 +3204,7 @@ mod tests { use gpui::MutableAppContext; use language::{Buffer, Rope}; use rand::prelude::*; + use settings::Settings; use std::{env, rc::Rc}; use text::{Point, RandomCharIter}; use util::test::sample_text; diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index c70ad6b7311576cacf7c509f8dc6d10f15aa2f49..0dc8165bfbb91bb0ed53593b5d089d46a4a3b974 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -27,6 +27,7 @@ fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } lsp = { path = "../lsp" } rpc = { path = "../rpc" } +settings = { path = "../settings" } sum_tree = { path = "../sum_tree" } text = { path = "../text" } theme = { path = "../theme" } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 8eef0a527424d7a8638a80a79cabbfca6d85cdd6..4b0d2566c365320f90842c5677921176ae4112fd 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -14,6 +14,7 @@ use futures::FutureExt as _; use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, MutableAppContext, Task}; use lazy_static::lazy_static; use parking_lot::Mutex; +use settings::Settings; use similar::{ChangeTag, TextDiff}; use smol::future::yield_now; use std::{ @@ -1156,7 +1157,6 @@ impl Buffer { pub fn edit_with_autoindent( &mut self, edits_iter: I, - indent_size: IndentSize, cx: &mut ModelContext, ) -> Option where @@ -1164,6 +1164,13 @@ impl Buffer { S: ToOffset, T: Into>, { + 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()) + }; self.edit_internal(edits_iter, Some(indent_size), cx) } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 3f721256cf94a5268b5a2c2f6df45f125c183f79..0a6fb2ce025893b7ed0a84be8e5a280f771f726b 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -3,6 +3,7 @@ use clock::ReplicaId; use collections::BTreeMap; use gpui::{ModelHandle, MutableAppContext}; use rand::prelude::*; +use settings::Settings; use std::{ cell::RefCell, env, @@ -24,6 +25,7 @@ fn init_logger() { #[gpui::test] fn test_line_endings(cx: &mut gpui::MutableAppContext) { + cx.set_global(Settings::test(cx)); cx.add_model(|cx| { let mut buffer = Buffer::new(0, "one\r\ntwo\rthree", cx).with_language(Arc::new(rust_lang()), cx); @@ -31,11 +33,7 @@ fn test_line_endings(cx: &mut gpui::MutableAppContext) { assert_eq!(buffer.line_ending(), LineEnding::Windows); buffer.check_invariants(); - buffer.edit_with_autoindent( - [(buffer.len()..buffer.len(), "\r\nfour")], - IndentSize::spaces(2), - cx, - ); + buffer.edit_with_autoindent([(buffer.len()..buffer.len(), "\r\nfour")], cx); buffer.edit([(0..0, "zero\r\n")], cx); assert_eq!(buffer.text(), "zero\none\ntwo\nthree\nfour"); assert_eq!(buffer.line_ending(), LineEnding::Windows); @@ -545,6 +543,7 @@ async fn test_symbols_containing(cx: &mut gpui::TestAppContext) { #[gpui::test] fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) { + cx.set_global(Settings::test(cx)); let buffer = cx.add_model(|cx| { let text = " mod x { @@ -620,36 +619,27 @@ fn test_range_for_syntax_ancestor(cx: &mut MutableAppContext) { #[gpui::test] fn test_autoindent_with_soft_tabs(cx: &mut MutableAppContext) { + let settings = Settings::test(cx); + cx.set_global(settings); + cx.add_model(|cx| { let text = "fn a() {}"; let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent([(8..8, "\n\n")], IndentSize::spaces(4), cx); + buffer.edit_with_autoindent([(8..8, "\n\n")], cx); assert_eq!(buffer.text(), "fn a() {\n \n}"); - buffer.edit_with_autoindent( - [(Point::new(1, 4)..Point::new(1, 4), "b()\n")], - IndentSize::spaces(4), - cx, - ); + buffer.edit_with_autoindent([(Point::new(1, 4)..Point::new(1, 4), "b()\n")], cx); assert_eq!(buffer.text(), "fn a() {\n b()\n \n}"); // Create a field expression on a new line, causing that line // to be indented. - buffer.edit_with_autoindent( - [(Point::new(2, 4)..Point::new(2, 4), ".c")], - IndentSize::spaces(4), - cx, - ); + buffer.edit_with_autoindent([(Point::new(2, 4)..Point::new(2, 4), ".c")], cx); assert_eq!(buffer.text(), "fn a() {\n b()\n .c\n}"); // Remove the dot so that the line is no longer a field expression, // causing the line to be outdented. - buffer.edit_with_autoindent( - [(Point::new(2, 8)..Point::new(2, 9), "")], - IndentSize::spaces(4), - cx, - ); + buffer.edit_with_autoindent([(Point::new(2, 8)..Point::new(2, 9), "")], cx); assert_eq!(buffer.text(), "fn a() {\n b()\n c\n}"); buffer @@ -658,36 +648,28 @@ fn test_autoindent_with_soft_tabs(cx: &mut MutableAppContext) { #[gpui::test] fn test_autoindent_with_hard_tabs(cx: &mut MutableAppContext) { + let mut settings = Settings::test(cx); + settings.editor_overrides.hard_tabs = Some(true); + cx.set_global(settings); + cx.add_model(|cx| { let text = "fn a() {}"; let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent([(8..8, "\n\n")], IndentSize::tab(), cx); + buffer.edit_with_autoindent([(8..8, "\n\n")], cx); assert_eq!(buffer.text(), "fn a() {\n\t\n}"); - buffer.edit_with_autoindent( - [(Point::new(1, 1)..Point::new(1, 1), "b()\n")], - IndentSize::tab(), - cx, - ); + buffer.edit_with_autoindent([(Point::new(1, 1)..Point::new(1, 1), "b()\n")], cx); assert_eq!(buffer.text(), "fn a() {\n\tb()\n\t\n}"); // Create a field expression on a new line, causing that line // to be indented. - buffer.edit_with_autoindent( - [(Point::new(2, 1)..Point::new(2, 1), ".c")], - IndentSize::tab(), - cx, - ); + buffer.edit_with_autoindent([(Point::new(2, 1)..Point::new(2, 1), ".c")], cx); assert_eq!(buffer.text(), "fn a() {\n\tb()\n\t\t.c\n}"); // Remove the dot so that the line is no longer a field expression, // causing the line to be outdented. - buffer.edit_with_autoindent( - [(Point::new(2, 2)..Point::new(2, 3), "")], - IndentSize::tab(), - cx, - ); + buffer.edit_with_autoindent([(Point::new(2, 2)..Point::new(2, 3), "")], cx); assert_eq!(buffer.text(), "fn a() {\n\tb()\n\tc\n}"); buffer @@ -696,6 +678,9 @@ fn test_autoindent_with_hard_tabs(cx: &mut MutableAppContext) { #[gpui::test] fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut MutableAppContext) { + let settings = Settings::test(cx); + cx.set_global(settings); + cx.add_model(|cx| { let text = " fn a() { @@ -714,7 +699,6 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta (empty(Point::new(1, 1)), "()"), (empty(Point::new(2, 1)), "()"), ], - IndentSize::spaces(4), cx, ); assert_eq!( @@ -735,7 +719,6 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta (empty(Point::new(1, 1)), "\n.f\n.g"), (empty(Point::new(2, 1)), "\n.f\n.g"), ], - IndentSize::spaces(4), cx, ); assert_eq!( @@ -768,11 +751,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); // Delete a closing curly brace changes the suggested indent for the line. - buffer.edit_with_autoindent( - [(Point::new(3, 4)..Point::new(3, 5), "")], - IndentSize::spaces(4), - cx, - ); + buffer.edit_with_autoindent([(Point::new(3, 4)..Point::new(3, 5), "")], cx); assert_eq!( buffer.text(), " @@ -788,11 +767,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta ); // Manually editing the leading whitespace - buffer.edit_with_autoindent( - [(Point::new(3, 0)..Point::new(3, 12), "")], - IndentSize::spaces(4), - cx, - ); + buffer.edit_with_autoindent([(Point::new(3, 0)..Point::new(3, 12), "")], cx); assert_eq!( buffer.text(), " @@ -811,6 +786,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta #[gpui::test] fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppContext) { + cx.set_global(Settings::test(cx)); cx.add_model(|cx| { let text = " fn a() {} @@ -819,7 +795,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent([(5..5, "\nb")], IndentSize::spaces(4), cx); + buffer.edit_with_autoindent([(5..5, "\nb")], cx); assert_eq!( buffer.text(), " @@ -831,11 +807,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte // The indentation suggestion changed because `@end` node (a close paren) // is now at the beginning of the line. - buffer.edit_with_autoindent( - [(Point::new(1, 4)..Point::new(1, 5), "")], - IndentSize::spaces(4), - cx, - ); + buffer.edit_with_autoindent([(Point::new(1, 4)..Point::new(1, 5), "")], cx); assert_eq!( buffer.text(), " @@ -851,10 +823,11 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte #[gpui::test] fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut MutableAppContext) { + cx.set_global(Settings::test(cx)); cx.add_model(|cx| { let text = "a\nb"; let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent([(0..1, "\n"), (2..3, "\n")], IndentSize::spaces(4), cx); + buffer.edit_with_autoindent([(0..1, "\n"), (2..3, "\n")], cx); assert_eq!(buffer.text(), "\n\n\n"); buffer }); @@ -862,6 +835,7 @@ fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut MutableAppContext) { #[gpui::test] fn test_autoindent_multi_line_insertion(cx: &mut MutableAppContext) { + cx.set_global(Settings::test(cx)); cx.add_model(|cx| { let text = " const a: usize = 1; @@ -876,7 +850,6 @@ fn test_autoindent_multi_line_insertion(cx: &mut MutableAppContext) { let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); buffer.edit_with_autoindent( [(Point::new(3, 0)..Point::new(3, 0), "e(\n f()\n);\n")], - IndentSize::spaces(4), cx, ); assert_eq!( @@ -903,6 +876,7 @@ fn test_autoindent_multi_line_insertion(cx: &mut MutableAppContext) { fn test_autoindent_preserves_relative_indentation_in_multi_line_insertion( cx: &mut MutableAppContext, ) { + cx.set_global(Settings::test(cx)); cx.add_model(|cx| { let text = " fn a() { @@ -924,7 +898,6 @@ fn test_autoindent_preserves_relative_indentation_in_multi_line_insertion( // 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!( @@ -947,7 +920,8 @@ fn test_autoindent_preserves_relative_indentation_in_multi_line_insertion( } #[gpui::test] -fn test_autoindent_disabled(cx: &mut MutableAppContext) { +fn test_autoindent_language_without_indents_query(cx: &mut MutableAppContext) { + cx.set_global(Settings::test(cx)); cx.add_model(|cx| { let text = " * one @@ -967,11 +941,7 @@ fn test_autoindent_disabled(cx: &mut MutableAppContext) { )), cx, ); - buffer.edit_with_autoindent( - [(Point::new(3, 0)..Point::new(3, 0), "\n")], - IndentSize::spaces(4), - cx, - ); + buffer.edit_with_autoindent([(Point::new(3, 0)..Point::new(3, 0), "\n")], cx); assert_eq!( buffer.text(), " diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 54554beaf6c52a47d98b9e16aa8be3dac766a5b8..71fde5bd5e2cfbc97da21f2e310df7d26e9ceb52 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -249,34 +249,37 @@ impl super::LspAdapter for CLspAdapter { #[cfg(test)] mod tests { use gpui::MutableAppContext; - use language::{Buffer, IndentSize}; + use language::Buffer; + use settings::Settings; use std::sync::Arc; #[gpui::test] fn test_c_autoindent(cx: &mut MutableAppContext) { cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); + let mut settings = Settings::test(cx); + settings.editor_overrides.tab_size = Some(2.try_into().unwrap()); + cx.set_global(settings); let language = crate::languages::language("c", tree_sitter_c::language(), None); cx.add_model(|cx| { let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx); - let size = IndentSize::spaces(2); // empty function - buffer.edit_with_autoindent([(0..0, "int main() {}")], size, cx); + buffer.edit_with_autoindent([(0..0, "int main() {}")], cx); // indent inside braces let ix = buffer.len() - 1; - buffer.edit_with_autoindent([(ix..ix, "\n\n")], size, cx); + buffer.edit_with_autoindent([(ix..ix, "\n\n")], cx); assert_eq!(buffer.text(), "int main() {\n \n}"); // indent body of single-statement if statement let ix = buffer.len() - 2; - buffer.edit_with_autoindent([(ix..ix, "if (a)\nb;")], size, cx); + buffer.edit_with_autoindent([(ix..ix, "if (a)\nb;")], cx); assert_eq!(buffer.text(), "int main() {\n if (a)\n b;\n}"); // indent inside field expression let ix = buffer.len() - 3; - buffer.edit_with_autoindent([(ix..ix, "\n.c")], size, cx); + buffer.edit_with_autoindent([(ix..ix, "\n.c")], cx); assert_eq!(buffer.text(), "int main() {\n if (a)\n b\n .c;\n}"); buffer diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index ca0b24bda78f024701ecdec2582cb0225a9dc9da..644d9ece40b6835c78fa40c19d7bd88cc4171182 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -147,20 +147,23 @@ impl LspAdapter for PythonLspAdapter { #[cfg(test)] mod tests { use gpui::{ModelContext, MutableAppContext}; - use language::{Buffer, IndentSize}; + use language::Buffer; + use settings::Settings; use std::sync::Arc; #[gpui::test] fn test_python_autoindent(cx: &mut MutableAppContext) { cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); let language = crate::languages::language("python", tree_sitter_python::language(), None); + let mut settings = Settings::test(cx); + settings.editor_overrides.tab_size = Some(2.try_into().unwrap()); + cx.set_global(settings); cx.add_model(|cx| { let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx); - let size = IndentSize::spaces(2); let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext| { let ix = buffer.len(); - buffer.edit_with_autoindent([(ix..ix, text)], size, cx); + buffer.edit_with_autoindent([(ix..ix, text)], cx); }; // indent after "def():" @@ -204,7 +207,7 @@ mod tests { // dedent the closing paren if it is shifted to the beginning of the line let argument_ix = buffer.text().find("1").unwrap(); - buffer.edit_with_autoindent([(argument_ix..argument_ix + 1, "")], size, cx); + buffer.edit_with_autoindent([(argument_ix..argument_ix + 1, "")], cx); assert_eq!( buffer.text(), "def a():\n \n if a:\n b()\n else:\n foo(\n )" @@ -219,7 +222,7 @@ mod tests { // manually outdent the last line let end_whitespace_ix = buffer.len() - 4; - buffer.edit_with_autoindent([(end_whitespace_ix..buffer.len(), "")], size, cx); + buffer.edit_with_autoindent([(end_whitespace_ix..buffer.len(), "")], cx); assert_eq!( buffer.text(), "def a():\n \n if a:\n b()\n else:\n foo(\n )\n" diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 18d49f78d478840876caadec32a53e5e9b5ee544..856986b1ebb924772af2142db2e9578a3b91b843 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -257,6 +257,7 @@ mod tests { use super::*; use crate::languages::{language, CachedLspAdapter}; use gpui::{color::Color, MutableAppContext}; + use settings::Settings; use theme::SyntaxTheme; #[gpui::test] @@ -433,37 +434,39 @@ mod tests { fn test_rust_autoindent(cx: &mut MutableAppContext) { cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); let language = crate::languages::language("rust", tree_sitter_rust::language(), None); + let mut settings = Settings::test(cx); + settings.editor_overrides.tab_size = Some(2.try_into().unwrap()); + cx.set_global(settings); cx.add_model(|cx| { let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx); - let size = IndentSize::spaces(2); // indent between braces buffer.set_text("fn a() {}", cx); let ix = buffer.len() - 1; - buffer.edit_with_autoindent([(ix..ix, "\n\n")], size, cx); + buffer.edit_with_autoindent([(ix..ix, "\n\n")], cx); assert_eq!(buffer.text(), "fn a() {\n \n}"); // indent between braces, even after empty lines buffer.set_text("fn a() {\n\n\n}", cx); let ix = buffer.len() - 2; - buffer.edit_with_autoindent([(ix..ix, "\n")], size, cx); + buffer.edit_with_autoindent([(ix..ix, "\n")], cx); assert_eq!(buffer.text(), "fn a() {\n\n\n \n}"); // indent a line that continues a field expression buffer.set_text("fn a() {\n \n}", cx); let ix = buffer.len() - 2; - buffer.edit_with_autoindent([(ix..ix, "b\n.c")], size, cx); + buffer.edit_with_autoindent([(ix..ix, "b\n.c")], cx); assert_eq!(buffer.text(), "fn a() {\n b\n .c\n}"); // indent further lines that continue the field expression, even after empty lines let ix = buffer.len() - 2; - buffer.edit_with_autoindent([(ix..ix, "\n\n.d")], size, cx); + buffer.edit_with_autoindent([(ix..ix, "\n\n.d")], cx); assert_eq!(buffer.text(), "fn a() {\n b\n .c\n \n .d\n}"); // dedent the line after the field expression let ix = buffer.len() - 2; - buffer.edit_with_autoindent([(ix..ix, ";\ne")], size, cx); + buffer.edit_with_autoindent([(ix..ix, ";\ne")], cx); assert_eq!( buffer.text(), "fn a() {\n b\n .c\n \n .d;\n e\n}" @@ -472,17 +475,17 @@ mod tests { // indent inside a struct within a call buffer.set_text("const a: B = c(D {});", cx); let ix = buffer.len() - 3; - buffer.edit_with_autoindent([(ix..ix, "\n\n")], size, cx); + buffer.edit_with_autoindent([(ix..ix, "\n\n")], cx); assert_eq!(buffer.text(), "const a: B = c(D {\n \n});"); // indent further inside a nested call let ix = buffer.len() - 4; - buffer.edit_with_autoindent([(ix..ix, "e: f(\n\n)")], size, cx); + buffer.edit_with_autoindent([(ix..ix, "e: f(\n\n)")], cx); assert_eq!(buffer.text(), "const a: B = c(D {\n e: f(\n \n )\n});"); // keep that indent after an empty line let ix = buffer.len() - 8; - buffer.edit_with_autoindent([(ix..ix, "\n")], size, cx); + buffer.edit_with_autoindent([(ix..ix, "\n")], cx); assert_eq!( buffer.text(), "const a: B = c(D {\n e: f(\n \n \n )\n});" From fa5af4383df896164f0de7fe8e5135f269d5d863 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Jul 2022 14:03:31 -0700 Subject: [PATCH 6/9] Introduce AutoindentMode parameter to Buffer::edit This controls whether or not we preserve the relative indentation of inserted text blocks. Co-authored-by: Mikayla Maki --- .../src/activity_indicator.rs | 1 + crates/collab/src/integration_tests.rs | 22 +-- crates/editor/src/display_map.rs | 3 +- crates/editor/src/display_map/block_map.rs | 2 +- crates/editor/src/display_map/fold_map.rs | 7 +- crates/editor/src/editor.rs | 70 ++++++---- crates/editor/src/multi_buffer.rs | 76 ++++------- crates/language/Cargo.toml | 2 + crates/language/src/buffer.rs | 128 +++++++++--------- crates/language/src/tests.rs | 114 +++++++++++----- crates/project/src/project.rs | 6 +- crates/project/src/project_tests.rs | 51 ++++--- crates/search/src/buffer_search.rs | 2 +- crates/vim/src/normal.rs | 4 +- crates/vim/src/visual.rs | 4 +- crates/zed/src/languages/c.rs | 14 +- crates/zed/src/languages/python.rs | 18 ++- crates/zed/src/languages/rust.rs | 20 +-- 18 files changed, 308 insertions(+), 236 deletions(-) diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 3730b5d21afc2a37e22843320b2b4deaf41cbef3..02ad100df81e8ba13e5d52ad805017cab6a498b3 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -82,6 +82,7 @@ impl ActivityIndicator { buffer.update(cx, |buffer, cx| { buffer.edit( [(0..0, format!("Language server error: {}\n\n", lsp_name))], + None, cx, ); }); diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 9471967eeecba37a5fa156321198ee4fda9a4aaa..1f3ccef0be0d6ecd3946e2971850d3be0b7b3412 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -842,8 +842,8 @@ async fn test_propagate_saves_and_fs_changes( .update(cx_c, |p, cx| p.open_buffer((worktree_id, "file1"), cx)) .await .unwrap(); - buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "i-am-b, ")], cx)); - buffer_c.update(cx_c, |buf, cx| buf.edit([(0..0, "i-am-c, ")], cx)); + buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "i-am-b, ")], None, cx)); + buffer_c.update(cx_c, |buf, cx| buf.edit([(0..0, "i-am-c, ")], None, cx)); // Open and edit that buffer as the host. let buffer_a = project_a @@ -855,7 +855,7 @@ async fn test_propagate_saves_and_fs_changes( .condition(cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, ") .await; buffer_a.update(cx_a, |buf, cx| { - buf.edit([(buf.len()..buf.len(), "i-am-a")], cx) + buf.edit([(buf.len()..buf.len(), "i-am-a")], None, cx) }); // Wait for edits to propagate @@ -871,7 +871,7 @@ async fn test_propagate_saves_and_fs_changes( // Edit the buffer as the host and concurrently save as guest B. let save_b = buffer_b.update(cx_b, |buf, cx| buf.save(cx)); - buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], cx)); + buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx)); save_b.await.unwrap(); assert_eq!( client_a.fs.load("/a/file1".as_ref()).await.unwrap(), @@ -1237,7 +1237,7 @@ async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut T .await .unwrap(); - buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], cx)); + buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], None, cx)); buffer_b.read_with(cx_b, |buf, _| { assert!(buf.is_dirty()); assert!(!buf.has_conflict()); @@ -1251,7 +1251,7 @@ async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut T assert!(!buf.has_conflict()); }); - buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], cx)); + buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], None, cx)); buffer_b.read_with(cx_b, |buf, _| { assert!(buf.is_dirty()); assert!(!buf.has_conflict()); @@ -1342,9 +1342,9 @@ async fn test_editing_while_guest_opens_buffer( // Edit the buffer as client A while client B is still opening it. cx_b.background().simulate_random_delay().await; - buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], cx)); + buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], None, cx)); cx_b.background().simulate_random_delay().await; - buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], cx)); + buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], None, cx)); let text = buffer_a.read_with(cx_a, |buf, _| buf.text()); let buffer_b = buffer_b.await.unwrap(); @@ -1882,8 +1882,8 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te .await .unwrap(); buffer_b.update(cx_b, |buffer, cx| { - buffer.edit([(4..7, "six")], cx); - buffer.edit([(10..11, "6")], cx); + buffer.edit([(4..7, "six")], None, cx); + buffer.edit([(10..11, "6")], None, cx); assert_eq!(buffer.text(), "let six = 6;"); assert!(buffer.is_dirty()); assert!(!buffer.has_conflict()); @@ -2964,7 +2964,7 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T ); rename.editor.update(cx, |rename_editor, cx| { rename_editor.buffer().update(cx, |rename_buffer, cx| { - rename_buffer.edit([(0..3, "THREE")], cx); + rename_buffer.edit([(0..3, "THREE")], None, cx); }); }); }); diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 6f50da6a7d28a51b67c0711b6dfe35ea1ab0adf7..9b12df60d9d1d3c6a67c6f302ab97847ed6c5ae6 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -897,7 +897,7 @@ pub mod tests { let ix = snapshot.buffer_snapshot.text().find("seven").unwrap(); buffer.update(cx, |buffer, cx| { - buffer.edit([(ix..ix, "and ")], cx); + buffer.edit([(ix..ix, "and ")], None, cx); }); let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); @@ -936,6 +936,7 @@ pub mod tests { (Point::new(1, 1)..Point::new(1, 1), "\t"), (Point::new(2, 1)..Point::new(2, 1), "\t"), ], + None, cx, ) }); diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index ed0df25d69b20d13e519a1baef6568269660a6dc..52379011709c2b3fc81183065fcbf22e643e5114 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -1164,7 +1164,7 @@ mod tests { // Insert a line break, separating two block decorations into separate lines. let buffer_snapshot = buffer.update(cx, |buffer, cx| { - buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "!!!\n")], cx); + buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "!!!\n")], None, cx); buffer.snapshot(cx) }); diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 95c3abb25238adbf76dccf496ad2e719f815b881..a6e5536d15ac6b1ddc4dac29029f11d402351a7c 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1240,6 +1240,7 @@ mod tests { (Point::new(0, 0)..Point::new(0, 1), "123"), (Point::new(2, 3)..Point::new(2, 3), "123"), ], + None, cx, ); buffer.snapshot(cx) @@ -1262,7 +1263,7 @@ mod tests { ); let buffer_snapshot = buffer.update(cx, |buffer, cx| { - buffer.edit([(Point::new(2, 6)..Point::new(4, 3), "456")], cx); + buffer.edit([(Point::new(2, 6)..Point::new(4, 3), "456")], None, cx); buffer.snapshot(cx) }); let (snapshot4, _) = map.read(buffer_snapshot.clone(), subscription.consume().into_inner()); @@ -1318,7 +1319,7 @@ mod tests { // Edit within one of the folds. let buffer_snapshot = buffer.update(cx, |buffer, cx| { - buffer.edit([(0..1, "12345")], cx); + buffer.edit([(0..1, "12345")], None, cx); buffer.snapshot(cx) }); let (snapshot, _) = @@ -1360,7 +1361,7 @@ mod tests { assert_eq!(snapshot.text(), "aa…cccc\nd…eeeee"); let buffer_snapshot = buffer.update(cx, |buffer, cx| { - buffer.edit([(Point::new(2, 2)..Point::new(3, 1), "")], cx); + buffer.edit([(Point::new(2, 2)..Point::new(3, 1), "")], None, cx); buffer.snapshot(cx) }); let (snapshot, _) = map.read(buffer_snapshot.clone(), subscription.consume().into_inner()); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f570fb2fb672234a80a17ca807080f48395444dc..4543aa3b27b28874e1f45d5d5fc942f2fefa2b05 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -38,9 +38,9 @@ use hover_popover::{hide_hover, HoverState}; pub use items::MAX_TAB_TITLE_LEN; pub use language::{char_kind, CharKind}; use language::{ - BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticSeverity, - IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, - TransactionId, + AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, + DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Point, + Selection, SelectionGoal, TransactionId, }; use link_go_to_definition::LinkGoToDefinitionState; pub use multi_buffer::{ @@ -1464,7 +1464,8 @@ impl Editor { S: ToOffset, T: Into>, { - self.buffer.update(cx, |buffer, cx| buffer.edit(edits, cx)); + self.buffer + .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); } pub fn edit_with_autoindent(&mut self, edits: I, cx: &mut ViewContext) @@ -1473,8 +1474,9 @@ impl Editor { S: ToOffset, T: Into>, { - self.buffer - .update(cx, |buffer, cx| buffer.edit_with_autoindent(edits, cx)); + self.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, Some(AutoindentMode::Independent), cx) + }); } fn select(&mut self, Select(phase): &Select, cx: &mut ViewContext) { @@ -1887,9 +1889,7 @@ impl Editor { .unzip() }; - this.buffer.update(cx, |buffer, cx| { - buffer.edit_with_autoindent(edits, cx); - }); + this.edit_with_autoindent(edits, cx); let buffer = this.buffer.read(cx).snapshot(cx); let new_selections = selection_fixup_info .into_iter() @@ -1922,10 +1922,11 @@ impl Editor { }) .collect::>() }; - buffer.edit_with_autoindent( + buffer.edit( old_selections .iter() .map(|s| (s.start..s.end, text.clone())), + Some(AutoindentMode::Block), cx, ); anchors @@ -1986,6 +1987,7 @@ impl Editor { (s.end.clone()..s.end.clone(), pair_end.clone()), ] }), + None, cx, ); }); @@ -2061,6 +2063,7 @@ impl Editor { selection_ranges .iter() .map(|range| (range.clone(), pair_end.clone())), + None, cx, ); snapshot = buffer.snapshot(cx); @@ -2363,8 +2366,11 @@ impl Editor { this.insert_snippet(&ranges, snippet, cx).log_err(); } else { this.buffer.update(cx, |buffer, cx| { - buffer - .edit_with_autoindent(ranges.iter().map(|range| (range.clone(), text)), cx); + buffer.edit( + ranges.iter().map(|range| (range.clone(), text)), + Some(AutoindentMode::Block), + cx, + ); }); } }); @@ -2725,11 +2731,12 @@ impl Editor { ) -> Result<()> { let tabstops = self.buffer.update(cx, |buffer, cx| { let snippet_text: Arc = snippet.text.clone().into(); - buffer.edit_with_autoindent( + buffer.edit( insertion_ranges .iter() .cloned() .map(|range| (range, snippet_text.clone())), + Some(AutoindentMode::Independent), cx, ); @@ -2933,7 +2940,11 @@ 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::())], cx); + buffer.edit( + [(cursor..cursor, tab_size.chars().collect::())], + None, + cx, + ); cursor.column += tab_size.len; selection.start = cursor; selection.end = cursor; @@ -3006,6 +3017,7 @@ impl Editor { row_start..row_start, indent_delta.chars().collect::(), )], + None, cx, ); @@ -3080,6 +3092,7 @@ impl Editor { deletion_ranges .into_iter() .map(|range| (range, empty_str.clone())), + None, cx, ); }); @@ -3145,6 +3158,7 @@ impl Editor { edit_ranges .into_iter() .map(|range| (range, empty_str.clone())), + None, cx, ); buffer.snapshot(cx) @@ -3202,7 +3216,7 @@ impl Editor { self.transact(cx, |this, cx| { this.buffer.update(cx, |buffer, cx| { - buffer.edit(edits, cx); + buffer.edit(edits, None, cx); }); this.request_autoscroll(Autoscroll::Fit, cx); @@ -3311,7 +3325,7 @@ impl Editor { this.unfold_ranges(unfold_ranges, true, cx); this.buffer.update(cx, |buffer, cx| { for (range, text) in edits { - buffer.edit([(range, text)], cx); + buffer.edit([(range, text)], None, cx); } }); this.fold_ranges(refold_ranges, cx); @@ -3416,7 +3430,7 @@ impl Editor { this.unfold_ranges(unfold_ranges, true, cx); this.buffer.update(cx, |buffer, cx| { for (range, text) in edits { - buffer.edit([(range, text)], cx); + buffer.edit([(range, text)], None, cx); } }); this.fold_ranges(refold_ranges, cx); @@ -3467,7 +3481,8 @@ impl Editor { }); edits }); - this.buffer.update(cx, |buffer, cx| buffer.edit(edits, cx)); + this.buffer + .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); let selections = this.selections.all::(cx); this.change_selections(Some(Autoscroll::Fit), cx, |s| { s.select(selections); @@ -3597,7 +3612,7 @@ impl Editor { edits.push((range, to_insert)); } drop(snapshot); - buffer.edit_with_autoindent(edits, cx); + buffer.edit(edits, Some(AutoindentMode::Block), cx); }); let selections = this.selections.all::(cx); @@ -4432,6 +4447,7 @@ impl Editor { .iter() .cloned() .map(|range| (range, empty_str.clone())), + None, cx, ); } else { @@ -4441,7 +4457,7 @@ impl Editor { let position = Point::new(range.start.row, min_column); (position..position, full_comment_prefix.clone()) }); - buffer.edit(edits, cx); + buffer.edit(edits, None, cx); } } } @@ -4875,9 +4891,9 @@ impl Editor { editor.override_text_style = Some(Box::new(move |style| old_highlight_id.style(&style.syntax))); } - editor - .buffer - .update(cx, |buffer, cx| buffer.edit([(0..0, old_name.clone())], cx)); + editor.buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, old_name.clone())], None, cx) + }); editor.select_all(&SelectAll, cx); editor }); @@ -6658,8 +6674,8 @@ mod tests { // Simulate an edit in another editor buffer.update(cx, |buffer, cx| { buffer.start_transaction_at(now, cx); - buffer.edit([(0..1, "a")], cx); - buffer.edit([(1..1, "b")], cx); + buffer.edit([(0..1, "a")], None, cx); + buffer.edit([(1..1, "b")], None, cx); buffer.end_transaction_at(now, cx); }); @@ -7200,6 +7216,7 @@ mod tests { (Point::new(1, 0)..Point::new(1, 0), "\t"), (Point::new(1, 1)..Point::new(1, 1), "\t"), ], + None, cx, ); }); @@ -7836,6 +7853,7 @@ mod tests { (Point::new(1, 2)..Point::new(3, 0), ""), (Point::new(4, 2)..Point::new(6, 0), ""), ], + None, cx, ); assert_eq!( @@ -7894,7 +7912,7 @@ mod tests { // Edit the buffer directly, deleting ranges surrounding the editor's selections buffer.update(cx, |buffer, cx| { - buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], cx); + buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx); assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent()); }); diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index fa48cabf05cd379219e81199070577245796ef00..a12285dbbe4a96693ab05e1f0d21abe83b132192 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -7,9 +7,9 @@ use collections::{Bound, HashMap, HashSet}; use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; pub use language::Completion; use language::{ - char_kind, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, DiagnosticEntry, Event, File, - IndentSize, Language, OffsetRangeExt, Outline, OutlineItem, Selection, ToOffset as _, - ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, + char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, + DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, Outline, OutlineItem, + Selection, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, }; use smallvec::SmallVec; use std::{ @@ -302,28 +302,10 @@ impl MultiBuffer { self.read(cx).symbols_containing(offset, theme) } - pub fn edit(&mut self, edits: I, cx: &mut ModelContext) - where - I: IntoIterator, T)>, - S: ToOffset, - T: Into>, - { - self.edit_internal(edits, false, cx) - } - - pub fn edit_with_autoindent(&mut self, edits: I, cx: &mut ModelContext) - where - I: IntoIterator, T)>, - S: ToOffset, - T: Into>, - { - self.edit_internal(edits, true, cx) - } - - pub fn edit_internal( + pub fn edit( &mut self, edits: I, - autoindent: bool, + autoindent_mode: Option, cx: &mut ModelContext, ) where I: IntoIterator, T)>, @@ -345,11 +327,7 @@ impl MultiBuffer { if let Some(buffer) = self.as_singleton() { return buffer.update(cx, |buffer, cx| { - if autoindent { - buffer.edit_with_autoindent(edits, cx); - } else { - buffer.edit(edits, cx); - } + buffer.edit(edits, autoindent_mode, cx); }); } @@ -464,13 +442,8 @@ impl MultiBuffer { } } - if autoindent { - buffer.edit_with_autoindent(deletions, cx); - buffer.edit_with_autoindent(insertions, cx); - } else { - buffer.edit(deletions, cx); - buffer.edit(insertions, cx); - } + buffer.edit(deletions, autoindent_mode, cx); + buffer.edit(insertions, autoindent_mode, cx); }) } } @@ -1386,7 +1359,7 @@ impl MultiBuffer { log::info!("mutating multi-buffer with {:?}", edits); drop(snapshot); - self.edit(edits, cx); + self.edit(edits, None, cx); } pub fn randomly_edit_excerpts( @@ -3224,7 +3197,7 @@ mod tests { .collect::>() ); - buffer.update(cx, |buffer, cx| buffer.edit([(1..3, "XXX\n")], cx)); + buffer.update(cx, |buffer, cx| buffer.edit([(1..3, "XXX\n")], None, cx)); let snapshot = multibuffer.read(cx).snapshot(cx); assert_eq!(snapshot.text(), buffer.read(cx).text()); @@ -3247,11 +3220,11 @@ mod tests { let snapshot = multibuffer.read(cx).snapshot(cx); assert_eq!(snapshot.text(), "a"); - guest_buffer.update(cx, |buffer, cx| buffer.edit([(1..1, "b")], cx)); + guest_buffer.update(cx, |buffer, cx| buffer.edit([(1..1, "b")], None, cx)); let snapshot = multibuffer.read(cx).snapshot(cx); assert_eq!(snapshot.text(), "ab"); - guest_buffer.update(cx, |buffer, cx| buffer.edit([(2..2, "c")], cx)); + guest_buffer.update(cx, |buffer, cx| buffer.edit([(2..2, "c")], None, cx)); let snapshot = multibuffer.read(cx).snapshot(cx); assert_eq!(snapshot.text(), "abc"); } @@ -3392,6 +3365,7 @@ mod tests { (Point::new(0, 0)..Point::new(0, 0), text), (Point::new(2, 1)..Point::new(2, 3), text), ], + None, cx, ); }); @@ -3529,8 +3503,8 @@ mod tests { let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); let old_snapshot = multibuffer.read(cx).snapshot(cx); buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "X")], cx); - buffer.edit([(5..5, "Y")], cx); + buffer.edit([(0..0, "X")], None, cx); + buffer.edit([(5..5, "Y")], None, cx); }); let new_snapshot = multibuffer.read(cx).snapshot(cx); @@ -3577,12 +3551,12 @@ mod tests { assert_eq!(Anchor::max().to_offset(&old_snapshot), 10); buffer_1.update(cx, |buffer, cx| { - buffer.edit([(0..0, "W")], cx); - buffer.edit([(5..5, "X")], cx); + buffer.edit([(0..0, "W")], None, cx); + buffer.edit([(5..5, "X")], None, cx); }); buffer_2.update(cx, |buffer, cx| { - buffer.edit([(0..0, "Y")], cx); - buffer.edit([(6..6, "Z")], cx); + buffer.edit([(0..0, "Y")], None, cx); + buffer.edit([(6..6, "Z")], None, cx); }); let new_snapshot = multibuffer.read(cx).snapshot(cx); @@ -3611,7 +3585,7 @@ mod tests { // Create an insertion id in buffer 1 that doesn't exist in buffer 2. // Add an excerpt from buffer 1 that spans this new insertion. - buffer_1.update(cx, |buffer, cx| buffer.edit([(4..4, "123")], cx)); + buffer_1.update(cx, |buffer, cx| buffer.edit([(4..4, "123")], None, cx)); let excerpt_id_1 = multibuffer.update(cx, |multibuffer, cx| { multibuffer .push_excerpts( @@ -4184,6 +4158,7 @@ mod tests { (Point::new(0, 0)..Point::new(0, 0), "A"), (Point::new(1, 0)..Point::new(1, 0), "A"), ], + None, cx, ); multibuffer.edit( @@ -4191,6 +4166,7 @@ mod tests { (Point::new(0, 1)..Point::new(0, 1), "B"), (Point::new(1, 1)..Point::new(1, 1), "B"), ], + None, cx, ); multibuffer.end_transaction_at(now, cx); @@ -4199,19 +4175,19 @@ mod tests { // Edit buffer 1 through the multibuffer now += 2 * group_interval; multibuffer.start_transaction_at(now, cx); - multibuffer.edit([(2..2, "C")], cx); + multibuffer.edit([(2..2, "C")], None, cx); multibuffer.end_transaction_at(now, cx); assert_eq!(multibuffer.read(cx).text(), "ABC1234\nAB5678"); // Edit buffer 1 independently buffer_1.update(cx, |buffer_1, cx| { buffer_1.start_transaction_at(now); - buffer_1.edit([(3..3, "D")], cx); + buffer_1.edit([(3..3, "D")], None, cx); buffer_1.end_transaction_at(now, cx); now += 2 * group_interval; buffer_1.start_transaction_at(now); - buffer_1.edit([(4..4, "E")], cx); + buffer_1.edit([(4..4, "E")], None, cx); buffer_1.end_transaction_at(now, cx); }); assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678"); @@ -4252,7 +4228,7 @@ mod tests { // Redo stack gets cleared after an edit. now += 2 * group_interval; multibuffer.start_transaction_at(now, cx); - multibuffer.edit([(0..0, "X")], cx); + multibuffer.edit([(0..0, "X")], None, cx); multibuffer.end_transaction_at(now, cx); assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); multibuffer.redo(cx); diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 0dc8165bfbb91bb0ed53593b5d089d46a4a3b974..6e9f368e77be909c1a8fb2d149be679b3aa0b66b 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -16,6 +16,7 @@ test-support = [ "text/test-support", "tree-sitter-rust", "tree-sitter-typescript", + "settings/test-support", "util/test-support", ] @@ -57,6 +58,7 @@ collections = { path = "../collections", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] } text = { path = "../text", features = ["test-support"] } +settings = { path = "../settings", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } ctor = "0.1" env_logger = "0.9" diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 4b0d2566c365320f90842c5677921176ae4112fd..8f0f7d40ec269edbebd7e85abf88e478515550b9 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -229,11 +229,18 @@ struct SyntaxTree { version: clock::Global, } +#[derive(Clone, Copy)] +pub enum AutoindentMode { + Block, + Independent, +} + #[derive(Clone)] struct AutoindentRequest { before_edit: BufferSnapshot, entries: Vec, indent_size: IndentSize, + mode: AutoindentMode, } #[derive(Clone)] @@ -852,8 +859,12 @@ impl Buffer { // At this point, old_suggestions contains the suggested indentation for all edited lines // with respect to the state of the buffer before the edit, but keyed by the row for these // lines after the edits were applied. + let new_edited_row_ranges = contiguous_ranges( - row_ranges.iter().map(|range| range.start), + row_ranges.iter().flat_map(|range| match request.mode { + AutoindentMode::Block => range.start..range.start + 1, + AutoindentMode::Independent => range.clone(), + }), max_rows_between_yields, ); for new_edited_row_range in new_edited_row_ranges { @@ -883,26 +894,32 @@ impl Buffer { yield_now().await; } - for row_range in row_ranges { - if row_range.len() > 1 { - if let Some(new_indent_size) = indent_sizes.get(&row_range.start).copied() { - let old_indent_size = snapshot.indent_size_for_line(row_range.start); - if new_indent_size.kind == old_indent_size.kind { - let delta = new_indent_size.len as i64 - old_indent_size.len as i64; - if delta != 0 { - for row in row_range.skip(1) { - indent_sizes.entry(row).or_insert_with(|| { - let mut size = snapshot.indent_size_for_line(row); - if size.kind == new_indent_size.kind { - if delta > 0 { - size.len += delta as u32; - } else if delta < 0 { - size.len = - size.len.saturating_sub(-delta as u32); + if matches!(request.mode, AutoindentMode::Block) { + for row_range in row_ranges { + if row_range.len() > 1 { + if let Some(new_indent_size) = + indent_sizes.get(&row_range.start).copied() + { + let old_indent_size = + snapshot.indent_size_for_line(row_range.start); + if new_indent_size.kind == old_indent_size.kind { + let delta = + new_indent_size.len as i64 - old_indent_size.len as i64; + if delta != 0 { + for row in row_range.skip(1) { + indent_sizes.entry(row).or_insert_with(|| { + let mut size = snapshot.indent_size_for_line(row); + if size.kind == new_indent_size.kind { + if delta > 0 { + size.len += delta as u32; + } else if delta < 0 { + size.len = + size.len.saturating_sub(-delta as u32); + } } - } - size - }); + size + }); + } } } } @@ -948,6 +965,7 @@ impl Buffer { .take((size.len - current_size.len) as usize) .collect::(), )], + None, cx, ); } else if size.len < current_size.len { @@ -956,6 +974,7 @@ impl Buffer { Point::new(row, 0)..Point::new(row, current_size.len - size.len), "", )], + None, cx, ); } @@ -993,7 +1012,7 @@ impl Buffer { match tag { ChangeTag::Equal => offset += len, ChangeTag::Delete => { - self.edit([(range, "")], cx); + self.edit([(range, "")], None, cx); } ChangeTag::Insert => { self.edit( @@ -1002,6 +1021,7 @@ impl Buffer { &diff.new_text[range.start - diff.start_offset ..range.end - diff.start_offset], )], + None, cx, ); offset += len; @@ -1138,46 +1158,13 @@ impl Buffer { where T: Into>, { - self.edit_internal([(0..self.len(), text)], None, cx) + self.edit([(0..self.len(), text)], None, cx) } pub fn edit( &mut self, edits_iter: I, - cx: &mut ModelContext, - ) -> Option - where - I: IntoIterator, T)>, - S: ToOffset, - T: Into>, - { - self.edit_internal(edits_iter, None, cx) - } - - pub fn edit_with_autoindent( - &mut self, - edits_iter: I, - cx: &mut ModelContext, - ) -> Option - where - I: IntoIterator, T)>, - S: ToOffset, - T: Into>, - { - 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()) - }; - self.edit_internal(edits_iter, Some(indent_size), cx) - } - - pub fn edit_internal( - &mut self, - edits_iter: I, - autoindent_size: Option, + autoindent_mode: Option, cx: &mut ModelContext, ) -> Option where @@ -1212,16 +1199,21 @@ impl Buffer { self.start_transaction(); self.pending_autoindent.take(); - let autoindent_request = self - .language - .as_ref() - .and_then(|_| autoindent_size) - .map(|autoindent_size| (self.snapshot(), autoindent_size)); + let autoindent_request = autoindent_mode + .and_then(|mode| self.language.as_ref().map(|_| (self.snapshot(), mode))); let edit_operation = self.text.edit(edits.iter().cloned()); let edit_id = edit_operation.local_timestamp(); - if let Some((before_edit, size)) = autoindent_request { + 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 mut delta = 0isize; let entries = edits .into_iter() @@ -1248,8 +1240,11 @@ impl Buffer { range_of_insertion_to_indent.start += 1; first_line_is_new = true; } - if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') { - range_of_insertion_to_indent.end -= 1; + + if matches!(mode, AutoindentMode::Block) { + if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') { + range_of_insertion_to_indent.end -= 1; + } } AutoindentRequestEntry { @@ -1263,7 +1258,8 @@ impl Buffer { self.autoindent_requests.push(Arc::new(AutoindentRequest { before_edit, entries, - indent_size: size, + indent_size, + mode, })); } @@ -1550,7 +1546,7 @@ impl Buffer { edits.push((range, new_text)); } log::info!("mutating buffer {} with {:?}", self.replica_id(), edits); - self.edit(edits, cx); + self.edit(edits, None, cx); } pub fn randomly_undo_redo(&mut self, rng: &mut impl rand::Rng, cx: &mut ModelContext) { diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 0a6fb2ce025893b7ed0a84be8e5a280f771f726b..acfaf5980d9df806cdcf3830364539383296275c 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -33,8 +33,12 @@ fn test_line_endings(cx: &mut gpui::MutableAppContext) { assert_eq!(buffer.line_ending(), LineEnding::Windows); buffer.check_invariants(); - buffer.edit_with_autoindent([(buffer.len()..buffer.len(), "\r\nfour")], cx); - buffer.edit([(0..0, "zero\r\n")], cx); + buffer.edit( + [(buffer.len()..buffer.len(), "\r\nfour")], + Some(AutoindentMode::Independent), + cx, + ); + buffer.edit([(0..0, "zero\r\n")], None, cx); assert_eq!(buffer.text(), "zero\none\ntwo\nthree\nfour"); assert_eq!(buffer.line_ending(), LineEnding::Windows); buffer.check_invariants(); @@ -114,7 +118,7 @@ fn test_edit_events(cx: &mut gpui::MutableAppContext) { // An edit emits an edited event, followed by a dirty changed event, // since the buffer was previously in a clean state. - buffer.edit([(2..4, "XYZ")], cx); + buffer.edit([(2..4, "XYZ")], None, cx); // An empty transaction does not emit any events. buffer.start_transaction(); @@ -123,8 +127,8 @@ fn test_edit_events(cx: &mut gpui::MutableAppContext) { // A transaction containing two edits emits one edited event. now += Duration::from_secs(1); buffer.start_transaction_at(now); - buffer.edit([(5..5, "u")], cx); - buffer.edit([(6..6, "w")], cx); + buffer.edit([(5..5, "u")], None, cx); + buffer.edit([(6..6, "w")], None, cx); buffer.end_transaction_at(now, cx); // Undoing a transaction emits one edited event. @@ -224,11 +228,11 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) { buf.start_transaction(); let offset = buf.text().find(")").unwrap(); - buf.edit([(offset..offset, "b: C")], cx); + buf.edit([(offset..offset, "b: C")], None, cx); assert!(!buf.is_parsing()); let offset = buf.text().find("}").unwrap(); - buf.edit([(offset..offset, " d; ")], cx); + buf.edit([(offset..offset, " d; ")], None, cx); assert!(!buf.is_parsing()); buf.end_transaction(cx); @@ -253,19 +257,19 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) { // * add a turbofish to the method call buffer.update(cx, |buf, cx| { let offset = buf.text().find(";").unwrap(); - buf.edit([(offset..offset, ".e")], cx); + buf.edit([(offset..offset, ".e")], None, cx); assert_eq!(buf.text(), "fn a(b: C) { d.e; }"); assert!(buf.is_parsing()); }); buffer.update(cx, |buf, cx| { let offset = buf.text().find(";").unwrap(); - buf.edit([(offset..offset, "(f)")], cx); + buf.edit([(offset..offset, "(f)")], None, cx); assert_eq!(buf.text(), "fn a(b: C) { d.e(f); }"); assert!(buf.is_parsing()); }); buffer.update(cx, |buf, cx| { let offset = buf.text().find("(f)").unwrap(); - buf.edit([(offset..offset, "::")], cx); + buf.edit([(offset..offset, "::")], None, cx); assert_eq!(buf.text(), "fn a(b: C) { d.e::(f); }"); assert!(buf.is_parsing()); }); @@ -626,20 +630,32 @@ fn test_autoindent_with_soft_tabs(cx: &mut MutableAppContext) { let text = "fn a() {}"; let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent([(8..8, "\n\n")], cx); + buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::Independent), cx); assert_eq!(buffer.text(), "fn a() {\n \n}"); - buffer.edit_with_autoindent([(Point::new(1, 4)..Point::new(1, 4), "b()\n")], cx); + buffer.edit( + [(Point::new(1, 4)..Point::new(1, 4), "b()\n")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!(buffer.text(), "fn a() {\n b()\n \n}"); // Create a field expression on a new line, causing that line // to be indented. - buffer.edit_with_autoindent([(Point::new(2, 4)..Point::new(2, 4), ".c")], cx); + buffer.edit( + [(Point::new(2, 4)..Point::new(2, 4), ".c")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!(buffer.text(), "fn a() {\n b()\n .c\n}"); // Remove the dot so that the line is no longer a field expression, // causing the line to be outdented. - buffer.edit_with_autoindent([(Point::new(2, 8)..Point::new(2, 9), "")], cx); + buffer.edit( + [(Point::new(2, 8)..Point::new(2, 9), "")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!(buffer.text(), "fn a() {\n b()\n c\n}"); buffer @@ -656,20 +672,32 @@ fn test_autoindent_with_hard_tabs(cx: &mut MutableAppContext) { let text = "fn a() {}"; let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent([(8..8, "\n\n")], cx); + buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::Independent), cx); assert_eq!(buffer.text(), "fn a() {\n\t\n}"); - buffer.edit_with_autoindent([(Point::new(1, 1)..Point::new(1, 1), "b()\n")], cx); + buffer.edit( + [(Point::new(1, 1)..Point::new(1, 1), "b()\n")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!(buffer.text(), "fn a() {\n\tb()\n\t\n}"); // Create a field expression on a new line, causing that line // to be indented. - buffer.edit_with_autoindent([(Point::new(2, 1)..Point::new(2, 1), ".c")], cx); + buffer.edit( + [(Point::new(2, 1)..Point::new(2, 1), ".c")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!(buffer.text(), "fn a() {\n\tb()\n\t\t.c\n}"); // Remove the dot so that the line is no longer a field expression, // causing the line to be outdented. - buffer.edit_with_autoindent([(Point::new(2, 2)..Point::new(2, 3), "")], cx); + buffer.edit( + [(Point::new(2, 2)..Point::new(2, 3), "")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!(buffer.text(), "fn a() {\n\tb()\n\tc\n}"); buffer @@ -694,11 +722,12 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta // Lines 2 and 3 don't match the indentation suggestion. When editing these lines, // their indentation is not adjusted. - buffer.edit_with_autoindent( + buffer.edit( [ (empty(Point::new(1, 1)), "()"), (empty(Point::new(2, 1)), "()"), ], + Some(AutoindentMode::Independent), cx, ); assert_eq!( @@ -714,11 +743,12 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta // When appending new content after these lines, the indentation is based on the // preceding lines' actual indentation. - buffer.edit_with_autoindent( + buffer.edit( [ (empty(Point::new(1, 1)), "\n.f\n.g"), (empty(Point::new(2, 1)), "\n.f\n.g"), ], + Some(AutoindentMode::Independent), cx, ); assert_eq!( @@ -751,7 +781,11 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); // Delete a closing curly brace changes the suggested indent for the line. - buffer.edit_with_autoindent([(Point::new(3, 4)..Point::new(3, 5), "")], cx); + buffer.edit( + [(Point::new(3, 4)..Point::new(3, 5), "")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!( buffer.text(), " @@ -767,7 +801,11 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta ); // Manually editing the leading whitespace - buffer.edit_with_autoindent([(Point::new(3, 0)..Point::new(3, 12), "")], cx); + buffer.edit( + [(Point::new(3, 0)..Point::new(3, 12), "")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!( buffer.text(), " @@ -795,7 +833,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent([(5..5, "\nb")], cx); + buffer.edit([(5..5, "\nb")], Some(AutoindentMode::Independent), cx); assert_eq!( buffer.text(), " @@ -807,7 +845,11 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte // The indentation suggestion changed because `@end` node (a close paren) // is now at the beginning of the line. - buffer.edit_with_autoindent([(Point::new(1, 4)..Point::new(1, 5), "")], cx); + buffer.edit( + [(Point::new(1, 4)..Point::new(1, 5), "")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!( buffer.text(), " @@ -827,7 +869,11 @@ fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut MutableAppContext) { cx.add_model(|cx| { let text = "a\nb"; let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent([(0..1, "\n"), (2..3, "\n")], cx); + buffer.edit( + [(0..1, "\n"), (2..3, "\n")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!(buffer.text(), "\n\n\n"); buffer }); @@ -848,8 +894,9 @@ fn test_autoindent_multi_line_insertion(cx: &mut MutableAppContext) { .unindent(); let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent( + buffer.edit( [(Point::new(3, 0)..Point::new(3, 0), "e(\n f()\n);\n")], + Some(AutoindentMode::Independent), cx, ); assert_eq!( @@ -896,8 +943,9 @@ fn test_autoindent_preserves_relative_indentation_in_multi_line_insertion( .unindent(); // insert at the beginning of a line - buffer.edit_with_autoindent( + buffer.edit( [(Point::new(2, 0)..Point::new(2, 0), pasted_text.clone())], + Some(AutoindentMode::Block), cx, ); assert_eq!( @@ -941,7 +989,11 @@ fn test_autoindent_language_without_indents_query(cx: &mut MutableAppContext) { )), cx, ); - buffer.edit_with_autoindent([(Point::new(3, 0)..Point::new(3, 0), "\n")], cx); + buffer.edit( + [(Point::new(3, 0)..Point::new(3, 0), "\n")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!( buffer.text(), " @@ -963,18 +1015,18 @@ fn test_serialization(cx: &mut gpui::MutableAppContext) { let buffer1 = cx.add_model(|cx| { let mut buffer = Buffer::new(0, "abc", cx); - buffer.edit([(3..3, "D")], cx); + buffer.edit([(3..3, "D")], None, cx); now += Duration::from_secs(1); buffer.start_transaction_at(now); - buffer.edit([(4..4, "E")], cx); + buffer.edit([(4..4, "E")], None, cx); buffer.end_transaction_at(now, cx); assert_eq!(buffer.text(), "abcDE"); buffer.undo(cx); assert_eq!(buffer.text(), "abcD"); - buffer.edit([(4..4, "F")], cx); + buffer.edit([(4..4, "F")], None, cx); assert_eq!(buffer.text(), "abcDF"); buffer }); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 29106b5889170a4a5b4631bbe9cde51b8a5bec71..898dbb5a2f6c3a23457b240b16618d9552f1a7f5 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3168,7 +3168,7 @@ impl Project { buffer.finalize_last_transaction(); buffer.start_transaction(); for (range, text) in edits { - buffer.edit([(range, text)], cx); + buffer.edit([(range, text)], None, cx); } if buffer.end_transaction(cx).is_some() { let transaction = buffer.finalize_last_transaction().unwrap().clone(); @@ -3663,7 +3663,7 @@ impl Project { buffer.finalize_last_transaction(); buffer.start_transaction(); for (range, text) in edits { - buffer.edit([(range, text)], cx); + buffer.edit([(range, text)], None, cx); } let transaction = if buffer.end_transaction(cx).is_some() { let transaction = buffer.finalize_last_transaction().unwrap().clone(); @@ -4023,7 +4023,7 @@ impl Project { buffer.finalize_last_transaction(); buffer.start_transaction(); for (range, text) in edits { - buffer.edit([(range, text)], cx); + buffer.edit([(range, text)], None, cx); } let transaction = if buffer.end_transaction(cx).is_some() { let transaction = buffer.finalize_last_transaction().unwrap().clone(); diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index e36bd2c75a6f066885ed340122ed505799fcf662..4c5e9ef8e1b95994dbe9550b745ef6fb091d79b4 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -169,7 +169,7 @@ async fn test_managing_language_servers( }); // Edit a buffer. The changes are reported to the language server. - rust_buffer.update(cx, |buffer, cx| buffer.edit([(16..16, "2")], cx)); + rust_buffer.update(cx, |buffer, cx| buffer.edit([(16..16, "2")], None, cx)); assert_eq!( fake_rust_server .receive_notification::() @@ -226,8 +226,10 @@ async fn test_managing_language_servers( }); // Changes are reported only to servers matching the buffer's language. - toml_buffer.update(cx, |buffer, cx| buffer.edit([(5..5, "23")], cx)); - rust_buffer2.update(cx, |buffer, cx| buffer.edit([(0..0, "let x = 1;")], cx)); + toml_buffer.update(cx, |buffer, cx| buffer.edit([(5..5, "23")], None, cx)); + rust_buffer2.update(cx, |buffer, cx| { + buffer.edit([(0..0, "let x = 1;")], None, cx) + }); assert_eq!( fake_rust_server .receive_notification::() @@ -348,7 +350,7 @@ async fn test_managing_language_servers( }); // The renamed file's version resets after changing language server. - rust_buffer2.update(cx, |buffer, cx| buffer.edit([(0..0, "// ")], cx)); + rust_buffer2.update(cx, |buffer, cx| buffer.edit([(0..0, "// ")], None, cx)); assert_eq!( fake_json_server .receive_notification::() @@ -972,7 +974,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { .await; // Edit the buffer, moving the content down - buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "\n\n")], cx)); + buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "\n\n")], None, cx)); let change_notification_1 = fake_server .receive_notification::() .await; @@ -1137,9 +1139,13 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { // Keep editing the buffer and ensure disk-based diagnostics get translated according to the // changes since the last save. buffer.update(cx, |buffer, cx| { - buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], cx); - buffer.edit([(Point::new(2, 8)..Point::new(2, 10), "(x: usize)")], cx); - buffer.edit([(Point::new(3, 10)..Point::new(3, 10), "xxx")], cx); + buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], None, cx); + buffer.edit( + [(Point::new(2, 8)..Point::new(2, 10), "(x: usize)")], + None, + cx, + ); + buffer.edit([(Point::new(3, 10)..Point::new(3, 10), "xxx")], None, cx); }); let change_notification_2 = fake_server .receive_notification::() @@ -1330,6 +1336,7 @@ async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) { Point::new(0, 0)..Point::new(0, 0), "// above first function\n", )], + None, cx, ); buffer.edit( @@ -1337,6 +1344,7 @@ async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) { Point::new(2, 0)..Point::new(2, 0), " // inside first function\n", )], + None, cx, ); buffer.edit( @@ -1344,6 +1352,7 @@ async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) { Point::new(6, 4)..Point::new(6, 4), "// inside second function ", )], + None, cx, ); @@ -1405,7 +1414,7 @@ async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) { buffer.update(cx, |buffer, cx| { for (range, new_text) in edits { - buffer.edit([(range, new_text)], cx); + buffer.edit([(range, new_text)], None, cx); } assert_eq!( buffer.text(), @@ -1517,7 +1526,7 @@ async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestApp ); for (range, new_text) in edits { - buffer.edit([(range, new_text)], cx); + buffer.edit([(range, new_text)], None, cx); } assert_eq!( buffer.text(), @@ -1620,7 +1629,7 @@ async fn test_invalid_edits_from_lsp(cx: &mut gpui::TestAppContext) { ); for (range, new_text) in edits { - buffer.edit([(range, new_text)], cx); + buffer.edit([(range, new_text)], None, cx); } assert_eq!( buffer.text(), @@ -2025,7 +2034,7 @@ async fn test_save_file(cx: &mut gpui::TestAppContext) { buffer .update(cx, |buffer, cx| { assert_eq!(buffer.text(), "the old contents"); - buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], cx); + buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx); buffer.save(cx) }) .await @@ -2053,7 +2062,7 @@ async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) { .unwrap(); buffer .update(cx, |buffer, cx| { - buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], cx); + buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx); buffer.save(cx) }) .await @@ -2073,7 +2082,7 @@ async fn test_save_as(cx: &mut gpui::TestAppContext) { project.create_buffer("", None, cx).unwrap() }); buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "abc")], cx); + buffer.edit([(0..0, "abc")], None, cx); assert!(buffer.is_dirty()); assert!(!buffer.has_conflict()); }); @@ -2329,7 +2338,7 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { assert!(!buffer.is_dirty()); assert!(events.borrow().is_empty()); - buffer.edit([(1..2, "")], cx); + buffer.edit([(1..2, "")], None, cx); }); // after the first edit, the buffer is dirty, and emits a dirtied event. @@ -2356,8 +2365,8 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { assert_eq!(*events.borrow(), &[language::Event::Saved]); events.borrow_mut().clear(); - buffer.edit([(1..1, "B")], cx); - buffer.edit([(2..2, "D")], cx); + buffer.edit([(1..1, "B")], None, cx); + buffer.edit([(2..2, "D")], None, cx); }); // after editing again, the buffer is dirty, and emits another dirty event. @@ -2376,7 +2385,7 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { // After restoring the buffer to its previously-saved state, // the buffer is not considered dirty anymore. - buffer.edit([(1..3, "")], cx); + buffer.edit([(1..3, "")], None, cx); assert!(buffer.text() == "ac"); assert!(!buffer.is_dirty()); }); @@ -2427,7 +2436,7 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { }); buffer3.update(cx, |buffer, cx| { - buffer.edit([(0..0, "x")], cx); + buffer.edit([(0..0, "x")], None, cx); }); events.borrow_mut().clear(); fs.remove_file("/dir/file3".as_ref(), Default::default()) @@ -2495,7 +2504,7 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { // Modify the buffer buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, " ")], cx); + buffer.edit([(0..0, " ")], None, cx); assert!(buffer.is_dirty()); assert!(!buffer.has_conflict()); }); @@ -2986,7 +2995,7 @@ async fn test_search(cx: &mut gpui::TestAppContext) { .unwrap(); buffer_4.update(cx, |buffer, cx| { let text = "two::TWO"; - buffer.edit([(20..28, text), (31..43, text)], cx); + buffer.edit([(20..28, text), (31..43, text)], None, cx); }); assert_eq!( diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 529da6f7b6ea3b70d5e6e85593c0a721a44a202d..52631e71b4b71827e6ad03ede931034e1f021b68 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -260,7 +260,7 @@ impl BufferSearchBar { self.query_editor.update(cx, |query_editor, cx| { query_editor.buffer().update(cx, |query_buffer, cx| { let len = query_buffer.len(cx); - query_buffer.edit([(0..len, query)], cx); + query_buffer.edit([(0..len, query)], None, cx); }); }); } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 65909702ba5f9d37cf68988411e646fa0c018c3b..8ec0fea9a9daadf0b7b6d681a3036b5dc9c0157f 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -13,7 +13,7 @@ use change::init as change_init; use collections::HashSet; use editor::{Autoscroll, Bias, ClipboardSelection, DisplayPoint}; use gpui::{actions, MutableAppContext, ViewContext}; -use language::{Point, SelectionGoal}; +use language::{AutoindentMode, Point, SelectionGoal}; use workspace::Workspace; use self::{change::change_over, delete::delete_over, yank::yank_over}; @@ -278,7 +278,7 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext) { } } drop(snapshot); - buffer.edit_with_autoindent(edits, cx); + buffer.edit(edits, Some(AutoindentMode::Independent), cx); }); editor.change_selections(Some(Autoscroll::Fit), cx, |s| { diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index fedb999cad2718b7877ff72af40c041a7e30420d..a81ac1c455f7d9678035e8eed6ab780388a0ff02 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use collections::HashMap; use editor::{display_map::ToDisplayPoint, Autoscroll, Bias, ClipboardSelection}; use gpui::{actions, MutableAppContext, ViewContext}; -use language::SelectionGoal; +use language::{AutoindentMode, SelectionGoal}; use workspace::Workspace; use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim}; @@ -254,7 +254,7 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext } } drop(snapshot); - buffer.edit_with_autoindent(edits, cx); + buffer.edit(edits, Some(AutoindentMode::Independent), cx); }); editor.change_selections(Some(Autoscroll::Fit), cx, |s| { diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 71fde5bd5e2cfbc97da21f2e310df7d26e9ceb52..074f51bc8fa9b084963a716b327abd60a318101a 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -249,7 +249,7 @@ impl super::LspAdapter for CLspAdapter { #[cfg(test)] mod tests { use gpui::MutableAppContext; - use language::Buffer; + use language::{AutoindentMode, Buffer}; use settings::Settings; use std::sync::Arc; @@ -265,21 +265,25 @@ mod tests { let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx); // empty function - buffer.edit_with_autoindent([(0..0, "int main() {}")], cx); + buffer.edit([(0..0, "int main() {}")], None, cx); // indent inside braces let ix = buffer.len() - 1; - buffer.edit_with_autoindent([(ix..ix, "\n\n")], cx); + buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::Independent), cx); assert_eq!(buffer.text(), "int main() {\n \n}"); // indent body of single-statement if statement let ix = buffer.len() - 2; - buffer.edit_with_autoindent([(ix..ix, "if (a)\nb;")], cx); + buffer.edit( + [(ix..ix, "if (a)\nb;")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!(buffer.text(), "int main() {\n if (a)\n b;\n}"); // indent inside field expression let ix = buffer.len() - 3; - buffer.edit_with_autoindent([(ix..ix, "\n.c")], cx); + buffer.edit([(ix..ix, "\n.c")], Some(AutoindentMode::Independent), cx); assert_eq!(buffer.text(), "int main() {\n if (a)\n b\n .c;\n}"); buffer diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index 644d9ece40b6835c78fa40c19d7bd88cc4171182..f76b4e0fbaad03a97cd0bce946a9d77ae4260aaf 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -147,7 +147,7 @@ impl LspAdapter for PythonLspAdapter { #[cfg(test)] mod tests { use gpui::{ModelContext, MutableAppContext}; - use language::Buffer; + use language::{AutoindentMode, Buffer}; use settings::Settings; use std::sync::Arc; @@ -163,7 +163,7 @@ mod tests { let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx); let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext| { let ix = buffer.len(); - buffer.edit_with_autoindent([(ix..ix, text)], cx); + buffer.edit([(ix..ix, text)], Some(AutoindentMode::Independent), cx); }; // indent after "def():" @@ -207,7 +207,11 @@ mod tests { // dedent the closing paren if it is shifted to the beginning of the line let argument_ix = buffer.text().find("1").unwrap(); - buffer.edit_with_autoindent([(argument_ix..argument_ix + 1, "")], cx); + buffer.edit( + [(argument_ix..argument_ix + 1, "")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!( buffer.text(), "def a():\n \n if a:\n b()\n else:\n foo(\n )" @@ -222,7 +226,11 @@ mod tests { // manually outdent the last line let end_whitespace_ix = buffer.len() - 4; - buffer.edit_with_autoindent([(end_whitespace_ix..buffer.len(), "")], cx); + buffer.edit( + [(end_whitespace_ix..buffer.len(), "")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!( buffer.text(), "def a():\n \n if a:\n b()\n else:\n foo(\n )\n" @@ -236,7 +244,7 @@ mod tests { ); // reset to a simple if statement - buffer.edit([(0..buffer.len(), "if a:\n b(\n )")], cx); + buffer.edit([(0..buffer.len(), "if a:\n b(\n )")], None, cx); // dedent "else" on the line after a closing paren append(&mut buffer, "\n else:\n", cx); diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 856986b1ebb924772af2142db2e9578a3b91b843..8e804f0c7f89cff2856bbafd435dcea8185668e9 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -444,29 +444,29 @@ mod tests { // indent between braces buffer.set_text("fn a() {}", cx); let ix = buffer.len() - 1; - buffer.edit_with_autoindent([(ix..ix, "\n\n")], cx); + buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::Independent), cx); assert_eq!(buffer.text(), "fn a() {\n \n}"); // indent between braces, even after empty lines buffer.set_text("fn a() {\n\n\n}", cx); let ix = buffer.len() - 2; - buffer.edit_with_autoindent([(ix..ix, "\n")], cx); + buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::Independent), cx); assert_eq!(buffer.text(), "fn a() {\n\n\n \n}"); // indent a line that continues a field expression buffer.set_text("fn a() {\n \n}", cx); let ix = buffer.len() - 2; - buffer.edit_with_autoindent([(ix..ix, "b\n.c")], cx); + buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::Independent), cx); assert_eq!(buffer.text(), "fn a() {\n b\n .c\n}"); // indent further lines that continue the field expression, even after empty lines let ix = buffer.len() - 2; - buffer.edit_with_autoindent([(ix..ix, "\n\n.d")], cx); + buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::Independent), cx); assert_eq!(buffer.text(), "fn a() {\n b\n .c\n \n .d\n}"); // dedent the line after the field expression let ix = buffer.len() - 2; - buffer.edit_with_autoindent([(ix..ix, ";\ne")], cx); + buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::Independent), cx); assert_eq!( buffer.text(), "fn a() {\n b\n .c\n \n .d;\n e\n}" @@ -475,17 +475,21 @@ mod tests { // indent inside a struct within a call buffer.set_text("const a: B = c(D {});", cx); let ix = buffer.len() - 3; - buffer.edit_with_autoindent([(ix..ix, "\n\n")], cx); + buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::Independent), cx); assert_eq!(buffer.text(), "const a: B = c(D {\n \n});"); // indent further inside a nested call let ix = buffer.len() - 4; - buffer.edit_with_autoindent([(ix..ix, "e: f(\n\n)")], cx); + buffer.edit( + [(ix..ix, "e: f(\n\n)")], + Some(AutoindentMode::Independent), + cx, + ); assert_eq!(buffer.text(), "const a: B = c(D {\n e: f(\n \n )\n});"); // keep that indent after an empty line let ix = buffer.len() - 8; - buffer.edit_with_autoindent([(ix..ix, "\n")], cx); + buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::Independent), cx); assert_eq!( buffer.text(), "const a: B = c(D {\n e: f(\n \n \n )\n});" From 2d05f906f1abc77a459790940174b382d6910f09 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Jul 2022 17:29:42 -0700 Subject: [PATCH 7/9] Start work on adjusting pasted text based on old start column --- crates/language/src/buffer.rs | 89 +++++++++++++++++++++-------------- crates/language/src/tests.rs | 38 +++++++++++---- 2 files changed, 84 insertions(+), 43 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 8f0f7d40ec269edbebd7e85abf88e478515550b9..ddacefec06e4d6f83f1e2d77dc5159f47c12da82 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -252,6 +252,8 @@ struct AutoindentRequestEntry { /// only be adjusted if the suggested indentation level has *changed* /// since the edit was made. first_line_is_new: bool, + /// The original indentation of the text that was inserted into this range. + original_indent: Option, } #[derive(Debug)] @@ -814,6 +816,8 @@ impl Buffer { Some(async move { let mut indent_sizes = BTreeMap::new(); for request in autoindent_requests { + // Resolve each edited range to its row in the current buffer and in the + // buffer before this batch of edits. let mut row_ranges = Vec::new(); let mut old_to_new_rows = BTreeMap::new(); for entry in &request.entries { @@ -824,11 +828,12 @@ impl Buffer { let old_row = position.to_point(&request.before_edit).row; old_to_new_rows.insert(old_row, new_row); } - if new_end_row > new_row { - row_ranges.push(new_row..new_end_row); - } + row_ranges.push((new_row..new_end_row, entry.original_indent)); } + // Build a map containing the suggested indentation for each of the edited lines + // with respect to the state of the buffer before these edits. This map is keyed + // by the rows for these lines in the current state of the buffer. let mut old_suggestions = BTreeMap::::default(); let old_edited_ranges = contiguous_ranges(old_to_new_rows.keys().copied(), max_rows_between_yields); @@ -856,17 +861,18 @@ impl Buffer { yield_now().await; } - // At this point, old_suggestions contains the suggested indentation for all edited lines - // with respect to the state of the buffer before the edit, but keyed by the row for these - // lines after the edits were applied. - + // In block mode, only compute indentation suggestions for the first line + // of each insertion. Otherwise, compute suggestions for every inserted line. let new_edited_row_ranges = contiguous_ranges( - row_ranges.iter().flat_map(|range| match request.mode { + row_ranges.iter().flat_map(|(range, _)| match request.mode { AutoindentMode::Block => range.start..range.start + 1, AutoindentMode::Independent => range.clone(), }), max_rows_between_yields, ); + + // Compute new suggestions for each line, but only include them in the result + // if they differ from the old suggestion for that line. for new_edited_row_range in new_edited_row_ranges { let suggestions = snapshot .suggest_autoindents(new_edited_row_range.clone()) @@ -894,34 +900,38 @@ impl Buffer { yield_now().await; } + // For each block of inserted text, adjust the indentation of the remaining + // lines of the block by the same amount as the first line was adjusted. if matches!(request.mode, AutoindentMode::Block) { - for row_range in row_ranges { - if row_range.len() > 1 { - if let Some(new_indent_size) = - indent_sizes.get(&row_range.start).copied() - { - let old_indent_size = - snapshot.indent_size_for_line(row_range.start); - if new_indent_size.kind == old_indent_size.kind { - let delta = - new_indent_size.len as i64 - old_indent_size.len as i64; - if delta != 0 { - for row in row_range.skip(1) { - indent_sizes.entry(row).or_insert_with(|| { - let mut size = snapshot.indent_size_for_line(row); - if size.kind == new_indent_size.kind { - if delta > 0 { - size.len += delta as u32; - } else if delta < 0 { - size.len = - size.len.saturating_sub(-delta as u32); - } - } - size - }); + for (row_range, original_indent) in + row_ranges + .into_iter() + .filter_map(|(range, original_indent)| { + if range.len() > 1 { + Some((range, original_indent?)) + } else { + None + } + }) + { + let new_indent = indent_sizes + .get(&row_range.start) + .copied() + .unwrap_or_else(|| snapshot.indent_size_for_line(row_range.start)); + let delta = new_indent.len as i64 - original_indent.len as i64; + if new_indent.kind == original_indent.kind && delta != 0 { + for row in row_range.skip(1) { + indent_sizes.entry(row).or_insert_with(|| { + let mut size = snapshot.indent_size_for_line(row); + if size.kind == new_indent.kind { + if delta > 0 { + size.len = size.len + delta as u32; + } else if delta < 0 { + size.len = size.len.saturating_sub(-delta as u32); } } - } + size + }); } } } @@ -1226,6 +1236,7 @@ impl Buffer { let mut range_of_insertion_to_indent = 0..new_text_len; let mut first_line_is_new = false; + let mut original_indent = None; // When inserting an entire line at the beginning of an existing line, // treat the insertion as new. @@ -1235,13 +1246,16 @@ impl Buffer { first_line_is_new = true; } - // Avoid auto-indenting lines before and after the insertion. + // When inserting text starting with a newline, avoid auto-indenting the + // previous line. if new_text[range_of_insertion_to_indent.clone()].starts_with('\n') { range_of_insertion_to_indent.start += 1; first_line_is_new = true; } + // Avoid auto-indenting before the insertion. if matches!(mode, AutoindentMode::Block) { + original_indent = Some(indent_size_for_text(new_text.chars())); if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') { range_of_insertion_to_indent.end -= 1; } @@ -1249,6 +1263,7 @@ impl Buffer { AutoindentRequestEntry { first_line_is_new, + original_indent, range: self.anchor_before(new_start + range_of_insertion_to_indent.start) ..self.anchor_after(new_start + range_of_insertion_to_indent.end), } @@ -2144,8 +2159,12 @@ impl BufferSnapshot { } pub fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize { + indent_size_for_text(text.chars_at(Point::new(row, 0))) +} + +pub fn indent_size_for_text(text: impl Iterator) -> IndentSize { let mut result = IndentSize::spaces(0); - for c in text.chars_at(Point::new(row, 0)) { + for c in text { let kind = match c { ' ' => IndentKind::Space, '\t' => IndentKind::Tab, diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index acfaf5980d9df806cdcf3830364539383296275c..0657e256042fb4ebd304512fba380e86e5410b7d 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -920,20 +920,18 @@ fn test_autoindent_multi_line_insertion(cx: &mut MutableAppContext) { } #[gpui::test] -fn test_autoindent_preserves_relative_indentation_in_multi_line_insertion( - cx: &mut MutableAppContext, -) { +fn test_autoindent_block_mode(cx: &mut MutableAppContext) { cx.set_global(Settings::test(cx)); cx.add_model(|cx| { - let text = " + let text = r#" fn a() { b(); } - " + "# .unindent(); let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - let pasted_text = r#" + let inserted_text = r#" " c d @@ -942,9 +940,33 @@ fn test_autoindent_preserves_relative_indentation_in_multi_line_insertion( "# .unindent(); - // insert at the beginning of a line + // Insert the block at column zero. The entire block is indented + // so that the first line matches the previous line's indentation. + buffer.edit( + [(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())], + Some(AutoindentMode::Block), + cx, + ); + assert_eq!( + buffer.text(), + r#" + fn a() { + b(); + " + c + d + e + " + } + "# + .unindent() + ); + + // Insert the block at a deeper indent level. The entire block is outdented. + buffer.undo(cx); + buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], None, cx); buffer.edit( - [(Point::new(2, 0)..Point::new(2, 0), pasted_text.clone())], + [(Point::new(2, 8)..Point::new(2, 8), inserted_text.clone())], Some(AutoindentMode::Block), cx, ); From 7a26fa18c7fee3fe031b991e18b55fd8f9c4eb1b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Jul 2022 18:09:24 -0700 Subject: [PATCH 8/9] Record start columns when writing to the clipboard from Zed --- crates/editor/src/editor.rs | 132 +++++++++++++++++++++++++++++- crates/editor/src/multi_buffer.rs | 51 +++++++++--- crates/language/src/buffer.rs | 63 +++++++------- crates/language/src/tests.rs | 8 +- crates/vim/src/utils.rs | 5 ++ 5 files changed, 214 insertions(+), 45 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 4543aa3b27b28874e1f45d5d5fc942f2fefa2b05..1f65451a834e67ba6a4ff9fbd56650d06fc6257b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -879,6 +879,7 @@ struct ActiveDiagnosticGroup { pub struct ClipboardSelection { pub len: usize, pub is_entire_line: bool, + pub first_line_indent: u32, } #[derive(Debug)] @@ -1926,7 +1927,7 @@ impl Editor { old_selections .iter() .map(|s| (s.start..s.end, text.clone())), - Some(AutoindentMode::Block), + Some(AutoindentMode::Independent), cx, ); anchors @@ -2368,7 +2369,7 @@ impl Editor { this.buffer.update(cx, |buffer, cx| { buffer.edit( ranges.iter().map(|range| (range.clone(), text)), - Some(AutoindentMode::Block), + Some(AutoindentMode::Independent), cx, ); }); @@ -3512,6 +3513,10 @@ impl Editor { clipboard_selections.push(ClipboardSelection { len, is_entire_line, + first_line_indent: cmp::min( + selection.start.column, + buffer.indent_size_for_line(selection.start.row).len, + ), }); } } @@ -3549,6 +3554,10 @@ impl Editor { clipboard_selections.push(ClipboardSelection { len, is_entire_line, + first_line_indent: cmp::min( + start.column, + buffer.indent_size_for_line(start.row).len, + ), }); } } @@ -3583,18 +3592,22 @@ impl Editor { let snapshot = buffer.read(cx); let mut start_offset = 0; let mut edits = Vec::new(); + let mut start_columns = Vec::new(); let line_mode = this.selections.line_mode; for (ix, selection) in old_selections.iter().enumerate() { let to_insert; let entire_line; + let start_column; if let Some(clipboard_selection) = clipboard_selections.get(ix) { let end_offset = start_offset + clipboard_selection.len; to_insert = &clipboard_text[start_offset..end_offset]; entire_line = clipboard_selection.is_entire_line; start_offset = end_offset; + start_column = clipboard_selection.first_line_indent; } else { to_insert = clipboard_text.as_str(); entire_line = all_selections_were_entire_line; + start_column = 0; } // If the corresponding selection was empty when this slice of the @@ -3610,9 +3623,10 @@ impl Editor { }; edits.push((range, to_insert)); + start_columns.push(start_column); } drop(snapshot); - buffer.edit(edits, Some(AutoindentMode::Block), cx); + buffer.edit(edits, Some(AutoindentMode::Block { start_columns }), cx); }); let selections = this.selections.all::(cx); @@ -8649,6 +8663,118 @@ mod tests { t|he lazy dog"}); } + #[gpui::test] + async fn test_paste_multiline(cx: &mut gpui::TestAppContext) { + let mut cx = EditorTestContext::new(cx).await; + let language = Arc::new(Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::language()), + )); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + + // Cut an indented block, without the leading whitespace. + cx.set_state(indoc! {" + const a = ( + b(), + [c( + d, + e + )} + ); + "}); + cx.update_editor(|e, cx| e.cut(&Cut, cx)); + cx.assert_editor_state(indoc! {" + const a = ( + b(), + | + ); + "}); + + // Paste it at the same position. + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state(indoc! {" + const a = ( + b(), + c( + d, + e + )| + ); + "}); + + // Paste it at a line with a lower indent level. + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.set_state(indoc! {" + | + const a = ( + b(), + ); + "}); + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state(indoc! {" + c( + d, + e + )| + const a = ( + b(), + ); + "}); + + // Cut an indented block, with the leading whitespace. + cx.set_state(indoc! {" + const a = ( + b(), + [ c( + d, + e + ) + }); + "}); + cx.update_editor(|e, cx| e.cut(&Cut, cx)); + cx.assert_editor_state(indoc! {" + const a = ( + b(), + |); + "}); + + // Paste it at the same position. + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state(indoc! {" + const a = ( + b(), + c( + d, + e + ) + |); + "}); + + // Paste it at a line with a higher indent level. + cx.set_state(indoc! {" + const a = ( + b(), + c( + d, + e| + ) + ); + "}); + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.set_state(indoc! {" + const a = ( + b(), + c( + d, + ec( + d, + e + )| + ) + ); + "}); + } + #[gpui::test] fn test_select_all(cx: &mut gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index a12285dbbe4a96693ab05e1f0d21abe83b132192..be9972c3d5d26e7430bfef3832e351c26de2c035 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -305,7 +305,7 @@ impl MultiBuffer { pub fn edit( &mut self, edits: I, - autoindent_mode: Option, + mut autoindent_mode: Option, cx: &mut ModelContext, ) where I: IntoIterator, T)>, @@ -331,11 +331,17 @@ impl MultiBuffer { }); } - let mut buffer_edits: HashMap, Arc, bool)>> = + let indent_start_columns = match &mut autoindent_mode { + Some(AutoindentMode::Block { start_columns }) => mem::take(start_columns), + _ => Default::default(), + }; + + let mut buffer_edits: HashMap, Arc, bool, u32)>> = Default::default(); let mut cursor = snapshot.excerpts.cursor::(); - for (range, new_text) in edits { + for (ix, (range, new_text)) in edits.enumerate() { let new_text: Arc = new_text.into(); + let start_column = indent_start_columns.get(ix).copied().unwrap_or(0); cursor.seek(&range.start, Bias::Right, &()); if cursor.item().is_none() && range.start == *cursor.start() { cursor.prev(&()); @@ -366,7 +372,7 @@ impl MultiBuffer { buffer_edits .entry(start_excerpt.buffer_id) .or_insert(Vec::new()) - .push((buffer_start..buffer_end, new_text, true)); + .push((buffer_start..buffer_end, new_text, true, start_column)); } else { let start_excerpt_range = buffer_start ..start_excerpt @@ -383,11 +389,11 @@ impl MultiBuffer { buffer_edits .entry(start_excerpt.buffer_id) .or_insert(Vec::new()) - .push((start_excerpt_range, new_text.clone(), true)); + .push((start_excerpt_range, new_text.clone(), true, start_column)); buffer_edits .entry(end_excerpt.buffer_id) .or_insert(Vec::new()) - .push((end_excerpt_range, new_text.clone(), false)); + .push((end_excerpt_range, new_text.clone(), false, start_column)); cursor.seek(&range.start, Bias::Right, &()); cursor.next(&()); @@ -402,6 +408,7 @@ impl MultiBuffer { excerpt.range.context.to_offset(&excerpt.buffer), new_text.clone(), false, + start_column, )); cursor.next(&()); } @@ -409,19 +416,21 @@ impl MultiBuffer { } for (buffer_id, mut edits) in buffer_edits { - edits.sort_unstable_by_key(|(range, _, _)| range.start); + edits.sort_unstable_by_key(|(range, _, _, _)| range.start); self.buffers.borrow()[&buffer_id] .buffer .update(cx, |buffer, cx| { let mut edits = edits.into_iter().peekable(); let mut insertions = Vec::new(); + let mut insertion_start_columns = Vec::new(); let mut deletions = Vec::new(); let empty_str: Arc = "".into(); - while let Some((mut range, new_text, mut is_insertion)) = edits.next() { - while let Some((next_range, _, next_is_insertion)) = edits.peek() { + while let Some((mut range, new_text, mut is_insertion, start_column)) = + edits.next() + { + while let Some((next_range, _, next_is_insertion, _)) = edits.peek() { if range.end >= next_range.start { range.end = cmp::max(next_range.end, range.end); - is_insertion |= *next_is_insertion; edits.next(); } else { @@ -430,6 +439,7 @@ impl MultiBuffer { } if is_insertion { + insertion_start_columns.push(start_column); insertions.push(( buffer.anchor_before(range.start)..buffer.anchor_before(range.end), new_text.clone(), @@ -442,8 +452,25 @@ impl MultiBuffer { } } - buffer.edit(deletions, autoindent_mode, cx); - buffer.edit(insertions, autoindent_mode, cx); + let deletion_autoindent_mode = + if let Some(AutoindentMode::Block { .. }) = autoindent_mode { + Some(AutoindentMode::Block { + start_columns: Default::default(), + }) + } else { + None + }; + let insertion_autoindent_mode = + if let Some(AutoindentMode::Block { .. }) = autoindent_mode { + Some(AutoindentMode::Block { + start_columns: insertion_start_columns, + }) + } else { + None + }; + + buffer.edit(deletions, deletion_autoindent_mode, cx); + buffer.edit(insertions, insertion_autoindent_mode, cx); }) } } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index ddacefec06e4d6f83f1e2d77dc5159f47c12da82..b2d45717f093a5d13d1b792f82bde517e7b55bd3 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -229,9 +229,9 @@ struct SyntaxTree { version: clock::Global, } -#[derive(Clone, Copy)] +#[derive(Clone, Debug)] pub enum AutoindentMode { - Block, + Block { start_columns: Vec }, Independent, } @@ -240,7 +240,7 @@ struct AutoindentRequest { before_edit: BufferSnapshot, entries: Vec, indent_size: IndentSize, - mode: AutoindentMode, + is_block_mode: bool, } #[derive(Clone)] @@ -252,8 +252,7 @@ struct AutoindentRequestEntry { /// only be adjusted if the suggested indentation level has *changed* /// since the edit was made. first_line_is_new: bool, - /// The original indentation of the text that was inserted into this range. - original_indent: Option, + start_column: Option, } #[derive(Debug)] @@ -828,7 +827,7 @@ impl Buffer { let old_row = position.to_point(&request.before_edit).row; old_to_new_rows.insert(old_row, new_row); } - row_ranges.push((new_row..new_end_row, entry.original_indent)); + row_ranges.push((new_row..new_end_row, entry.start_column)); } // Build a map containing the suggested indentation for each of the edited lines @@ -864,9 +863,12 @@ impl Buffer { // In block mode, only compute indentation suggestions for the first line // of each insertion. Otherwise, compute suggestions for every inserted line. let new_edited_row_ranges = contiguous_ranges( - row_ranges.iter().flat_map(|(range, _)| match request.mode { - AutoindentMode::Block => range.start..range.start + 1, - AutoindentMode::Independent => range.clone(), + row_ranges.iter().flat_map(|(range, _)| { + if request.is_block_mode { + range.start..range.start + 1 + } else { + range.clone() + } }), max_rows_between_yields, ); @@ -902,24 +904,22 @@ impl Buffer { // For each block of inserted text, adjust the indentation of the remaining // lines of the block by the same amount as the first line was adjusted. - if matches!(request.mode, AutoindentMode::Block) { - for (row_range, original_indent) in - row_ranges - .into_iter() - .filter_map(|(range, original_indent)| { - if range.len() > 1 { - Some((range, original_indent?)) - } else { - None - } - }) + if request.is_block_mode { + for (row_range, start_column) in + row_ranges.into_iter().filter_map(|(range, start_column)| { + if range.len() > 1 { + Some((range, start_column?)) + } else { + None + } + }) { let new_indent = indent_sizes .get(&row_range.start) .copied() .unwrap_or_else(|| snapshot.indent_size_for_line(row_range.start)); - let delta = new_indent.len as i64 - original_indent.len as i64; - if new_indent.kind == original_indent.kind && delta != 0 { + let delta = new_indent.len as i64 - start_column as i64; + if delta != 0 { for row in row_range.skip(1) { indent_sizes.entry(row).or_insert_with(|| { let mut size = snapshot.indent_size_for_line(row); @@ -1223,12 +1223,17 @@ impl Buffer { } else { IndentSize::spaces(settings.tab_size(language_name.as_deref()).get()) }; + let (start_columns, is_block_mode) = match mode { + AutoindentMode::Block { start_columns } => (start_columns, true), + AutoindentMode::Independent => (Default::default(), false), + }; let mut delta = 0isize; let entries = edits .into_iter() + .enumerate() .zip(&edit_operation.as_edit().unwrap().new_text) - .map(|((range, _), new_text)| { + .map(|((ix, (range, _)), new_text)| { let new_text_len = new_text.len(); let old_start = range.start.to_point(&before_edit); let new_start = (delta + range.start as isize) as usize; @@ -1236,7 +1241,7 @@ impl Buffer { let mut range_of_insertion_to_indent = 0..new_text_len; let mut first_line_is_new = false; - let mut original_indent = None; + let mut start_column = None; // When inserting an entire line at the beginning of an existing line, // treat the insertion as new. @@ -1254,8 +1259,10 @@ impl Buffer { } // Avoid auto-indenting before the insertion. - if matches!(mode, AutoindentMode::Block) { - original_indent = Some(indent_size_for_text(new_text.chars())); + if is_block_mode { + start_column = start_columns + .get(ix) + .map(|start| start + indent_size_for_text(new_text.chars()).len); if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') { range_of_insertion_to_indent.end -= 1; } @@ -1263,7 +1270,7 @@ impl Buffer { AutoindentRequestEntry { first_line_is_new, - original_indent, + start_column, range: self.anchor_before(new_start + range_of_insertion_to_indent.start) ..self.anchor_after(new_start + range_of_insertion_to_indent.end), } @@ -1274,7 +1281,7 @@ impl Buffer { before_edit, entries, indent_size, - mode, + is_block_mode, })); } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 0657e256042fb4ebd304512fba380e86e5410b7d..bee320a932ebf1fd36c44c480431dc4c8e6ee80c 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -944,7 +944,9 @@ fn test_autoindent_block_mode(cx: &mut MutableAppContext) { // so that the first line matches the previous line's indentation. buffer.edit( [(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())], - Some(AutoindentMode::Block), + Some(AutoindentMode::Block { + start_columns: vec![0], + }), cx, ); assert_eq!( @@ -967,7 +969,9 @@ fn test_autoindent_block_mode(cx: &mut MutableAppContext) { buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], None, cx); buffer.edit( [(Point::new(2, 8)..Point::new(2, 8), inserted_text.clone())], - Some(AutoindentMode::Block), + Some(AutoindentMode::Block { + start_columns: vec![0], + }), cx, ); assert_eq!( diff --git a/crates/vim/src/utils.rs b/crates/vim/src/utils.rs index cb6a736c6344d0c91cfdb7b5b22458ac0e9fed2e..75bb7445ffb42e8c62ff0b15a2e517eb6b0c5ea3 100644 --- a/crates/vim/src/utils.rs +++ b/crates/vim/src/utils.rs @@ -1,5 +1,6 @@ use editor::{ClipboardSelection, Editor}; use gpui::{ClipboardItem, MutableAppContext}; +use std::cmp; pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut MutableAppContext) { let selections = editor.selections.all_adjusted(cx); @@ -17,6 +18,10 @@ pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut Mut clipboard_selections.push(ClipboardSelection { len: text.len() - initial_len, is_entire_line: linewise, + first_line_indent: cmp::min( + start.column, + buffer.indent_size_for_line(start.row).len, + ), }); } } From 868c46062008bb0bcab2d41a38b4295996b9b958 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Jul 2022 22:34:56 -0700 Subject: [PATCH 9/9] :art: Rename and simplify some autoindent stuff --- crates/editor/src/editor.rs | 26 ++++++++--------- crates/editor/src/multi_buffer.rs | 47 +++++++++++++++++++++--------- crates/language/src/buffer.rs | 47 ++++++++++++++++++------------ crates/language/src/tests.rs | 40 ++++++++++++------------- crates/vim/src/normal.rs | 2 +- crates/vim/src/utils.rs | 6 +--- crates/vim/src/visual.rs | 2 +- crates/zed/src/languages/c.rs | 10 ++----- crates/zed/src/languages/python.rs | 6 ++-- crates/zed/src/languages/rust.rs | 20 +++++-------- 10 files changed, 112 insertions(+), 94 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1f65451a834e67ba6a4ff9fbd56650d06fc6257b..819aa5ccd5ebdf139117183a1d9568e47b6d0a2a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1476,7 +1476,7 @@ impl Editor { T: Into>, { self.buffer.update(cx, |buffer, cx| { - buffer.edit(edits, Some(AutoindentMode::Independent), cx) + buffer.edit(edits, Some(AutoindentMode::EachLine), cx) }); } @@ -1927,7 +1927,7 @@ impl Editor { old_selections .iter() .map(|s| (s.start..s.end, text.clone())), - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); anchors @@ -2369,7 +2369,7 @@ impl Editor { this.buffer.update(cx, |buffer, cx| { buffer.edit( ranges.iter().map(|range| (range.clone(), text)), - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); }); @@ -2737,7 +2737,7 @@ impl Editor { .iter() .cloned() .map(|range| (range, snippet_text.clone())), - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); @@ -3513,10 +3513,7 @@ impl Editor { clipboard_selections.push(ClipboardSelection { len, is_entire_line, - first_line_indent: cmp::min( - selection.start.column, - buffer.indent_size_for_line(selection.start.row).len, - ), + first_line_indent: buffer.indent_size_for_line(selection.start.row).len, }); } } @@ -3554,10 +3551,7 @@ impl Editor { clipboard_selections.push(ClipboardSelection { len, is_entire_line, - first_line_indent: cmp::min( - start.column, - buffer.indent_size_for_line(start.row).len, - ), + first_line_indent: buffer.indent_size_for_line(start.row).len, }); } } @@ -3626,7 +3620,13 @@ impl Editor { start_columns.push(start_column); } drop(snapshot); - buffer.edit(edits, Some(AutoindentMode::Block { start_columns }), cx); + buffer.edit( + edits, + Some(AutoindentMode::Block { + original_indent_columns: start_columns, + }), + cx, + ); }); let selections = this.selections.all::(cx); diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index be9972c3d5d26e7430bfef3832e351c26de2c035..1fc7cf0560c262106aad9f63a6504d4ea57f40b0 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -331,8 +331,10 @@ impl MultiBuffer { }); } - let indent_start_columns = match &mut autoindent_mode { - Some(AutoindentMode::Block { start_columns }) => mem::take(start_columns), + let original_indent_columns = match &mut autoindent_mode { + Some(AutoindentMode::Block { + original_indent_columns, + }) => mem::take(original_indent_columns), _ => Default::default(), }; @@ -341,7 +343,7 @@ impl MultiBuffer { let mut cursor = snapshot.excerpts.cursor::(); for (ix, (range, new_text)) in edits.enumerate() { let new_text: Arc = new_text.into(); - let start_column = indent_start_columns.get(ix).copied().unwrap_or(0); + let original_indent_column = original_indent_columns.get(ix).copied().unwrap_or(0); cursor.seek(&range.start, Bias::Right, &()); if cursor.item().is_none() && range.start == *cursor.start() { cursor.prev(&()); @@ -372,7 +374,12 @@ impl MultiBuffer { buffer_edits .entry(start_excerpt.buffer_id) .or_insert(Vec::new()) - .push((buffer_start..buffer_end, new_text, true, start_column)); + .push(( + buffer_start..buffer_end, + new_text, + true, + original_indent_column, + )); } else { let start_excerpt_range = buffer_start ..start_excerpt @@ -389,11 +396,21 @@ impl MultiBuffer { buffer_edits .entry(start_excerpt.buffer_id) .or_insert(Vec::new()) - .push((start_excerpt_range, new_text.clone(), true, start_column)); + .push(( + start_excerpt_range, + new_text.clone(), + true, + original_indent_column, + )); buffer_edits .entry(end_excerpt.buffer_id) .or_insert(Vec::new()) - .push((end_excerpt_range, new_text.clone(), false, start_column)); + .push(( + end_excerpt_range, + new_text.clone(), + false, + original_indent_column, + )); cursor.seek(&range.start, Bias::Right, &()); cursor.next(&()); @@ -408,7 +425,7 @@ impl MultiBuffer { excerpt.range.context.to_offset(&excerpt.buffer), new_text.clone(), false, - start_column, + original_indent_column, )); cursor.next(&()); } @@ -422,11 +439,15 @@ impl MultiBuffer { .update(cx, |buffer, cx| { let mut edits = edits.into_iter().peekable(); let mut insertions = Vec::new(); - let mut insertion_start_columns = Vec::new(); + let mut original_indent_columns = Vec::new(); let mut deletions = Vec::new(); let empty_str: Arc = "".into(); - while let Some((mut range, new_text, mut is_insertion, start_column)) = - edits.next() + while let Some(( + mut range, + new_text, + mut is_insertion, + original_indent_column, + )) = edits.next() { while let Some((next_range, _, next_is_insertion, _)) = edits.peek() { if range.end >= next_range.start { @@ -439,7 +460,7 @@ impl MultiBuffer { } if is_insertion { - insertion_start_columns.push(start_column); + original_indent_columns.push(original_indent_column); insertions.push(( buffer.anchor_before(range.start)..buffer.anchor_before(range.end), new_text.clone(), @@ -455,7 +476,7 @@ impl MultiBuffer { let deletion_autoindent_mode = if let Some(AutoindentMode::Block { .. }) = autoindent_mode { Some(AutoindentMode::Block { - start_columns: Default::default(), + original_indent_columns: Default::default(), }) } else { None @@ -463,7 +484,7 @@ impl MultiBuffer { let insertion_autoindent_mode = if let Some(AutoindentMode::Block { .. }) = autoindent_mode { Some(AutoindentMode::Block { - start_columns: insertion_start_columns, + original_indent_columns, }) } else { None diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index b2d45717f093a5d13d1b792f82bde517e7b55bd3..e6b0d48820b0db06de9cf96c4c887dbaff561607 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -231,8 +231,15 @@ struct SyntaxTree { #[derive(Clone, Debug)] pub enum AutoindentMode { - Block { start_columns: Vec }, - Independent, + /// Indent each line of inserted text. + EachLine, + /// Apply the same indentation adjustment to all of the lines + /// in a given insertion. + Block { + /// The original indentation level of the first line of each + /// insertion, if it has been copied. + original_indent_columns: Vec, + }, } #[derive(Clone)] @@ -252,7 +259,7 @@ struct AutoindentRequestEntry { /// only be adjusted if the suggested indentation level has *changed* /// since the edit was made. first_line_is_new: bool, - start_column: Option, + original_indent_column: Option, } #[derive(Debug)] @@ -827,7 +834,7 @@ impl Buffer { let old_row = position.to_point(&request.before_edit).row; old_to_new_rows.insert(old_row, new_row); } - row_ranges.push((new_row..new_end_row, entry.start_column)); + row_ranges.push((new_row..new_end_row, entry.original_indent_column)); } // Build a map containing the suggested indentation for each of the edited lines @@ -905,20 +912,22 @@ impl Buffer { // For each block of inserted text, adjust the indentation of the remaining // lines of the block by the same amount as the first line was adjusted. if request.is_block_mode { - for (row_range, start_column) in - row_ranges.into_iter().filter_map(|(range, start_column)| { - if range.len() > 1 { - Some((range, start_column?)) - } else { - None - } - }) + for (row_range, original_indent_column) in + row_ranges + .into_iter() + .filter_map(|(range, original_indent_column)| { + if range.len() > 1 { + Some((range, original_indent_column?)) + } else { + None + } + }) { let new_indent = indent_sizes .get(&row_range.start) .copied() .unwrap_or_else(|| snapshot.indent_size_for_line(row_range.start)); - let delta = new_indent.len as i64 - start_column as i64; + let delta = new_indent.len as i64 - original_indent_column as i64; if delta != 0 { for row in row_range.skip(1) { indent_sizes.entry(row).or_insert_with(|| { @@ -1224,8 +1233,10 @@ impl Buffer { IndentSize::spaces(settings.tab_size(language_name.as_deref()).get()) }; let (start_columns, is_block_mode) = match mode { - AutoindentMode::Block { start_columns } => (start_columns, true), - AutoindentMode::Independent => (Default::default(), false), + AutoindentMode::Block { + original_indent_columns: start_columns, + } => (start_columns, true), + AutoindentMode::EachLine => (Default::default(), false), }; let mut delta = 0isize; @@ -1260,9 +1271,7 @@ impl Buffer { // Avoid auto-indenting before the insertion. if is_block_mode { - start_column = start_columns - .get(ix) - .map(|start| start + indent_size_for_text(new_text.chars()).len); + start_column = start_columns.get(ix).copied(); if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') { range_of_insertion_to_indent.end -= 1; } @@ -1270,7 +1279,7 @@ impl Buffer { AutoindentRequestEntry { first_line_is_new, - start_column, + original_indent_column: start_column, range: self.anchor_before(new_start + range_of_insertion_to_indent.start) ..self.anchor_after(new_start + range_of_insertion_to_indent.end), } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index bee320a932ebf1fd36c44c480431dc4c8e6ee80c..937ff069305cabe280d4d5de5949ea5423181054 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -35,7 +35,7 @@ fn test_line_endings(cx: &mut gpui::MutableAppContext) { buffer.check_invariants(); buffer.edit( [(buffer.len()..buffer.len(), "\r\nfour")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); buffer.edit([(0..0, "zero\r\n")], None, cx); @@ -630,12 +630,12 @@ fn test_autoindent_with_soft_tabs(cx: &mut MutableAppContext) { let text = "fn a() {}"; let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::Independent), cx); + buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n \n}"); buffer.edit( [(Point::new(1, 4)..Point::new(1, 4), "b()\n")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!(buffer.text(), "fn a() {\n b()\n \n}"); @@ -644,7 +644,7 @@ fn test_autoindent_with_soft_tabs(cx: &mut MutableAppContext) { // to be indented. buffer.edit( [(Point::new(2, 4)..Point::new(2, 4), ".c")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!(buffer.text(), "fn a() {\n b()\n .c\n}"); @@ -653,7 +653,7 @@ fn test_autoindent_with_soft_tabs(cx: &mut MutableAppContext) { // causing the line to be outdented. buffer.edit( [(Point::new(2, 8)..Point::new(2, 9), "")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!(buffer.text(), "fn a() {\n b()\n c\n}"); @@ -672,12 +672,12 @@ fn test_autoindent_with_hard_tabs(cx: &mut MutableAppContext) { let text = "fn a() {}"; let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::Independent), cx); + buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n\t\n}"); buffer.edit( [(Point::new(1, 1)..Point::new(1, 1), "b()\n")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!(buffer.text(), "fn a() {\n\tb()\n\t\n}"); @@ -686,7 +686,7 @@ fn test_autoindent_with_hard_tabs(cx: &mut MutableAppContext) { // to be indented. buffer.edit( [(Point::new(2, 1)..Point::new(2, 1), ".c")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!(buffer.text(), "fn a() {\n\tb()\n\t\t.c\n}"); @@ -695,7 +695,7 @@ fn test_autoindent_with_hard_tabs(cx: &mut MutableAppContext) { // causing the line to be outdented. buffer.edit( [(Point::new(2, 2)..Point::new(2, 3), "")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!(buffer.text(), "fn a() {\n\tb()\n\tc\n}"); @@ -727,7 +727,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta (empty(Point::new(1, 1)), "()"), (empty(Point::new(2, 1)), "()"), ], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!( @@ -748,7 +748,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta (empty(Point::new(1, 1)), "\n.f\n.g"), (empty(Point::new(2, 1)), "\n.f\n.g"), ], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!( @@ -783,7 +783,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta // Delete a closing curly brace changes the suggested indent for the line. buffer.edit( [(Point::new(3, 4)..Point::new(3, 5), "")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!( @@ -803,7 +803,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta // Manually editing the leading whitespace buffer.edit( [(Point::new(3, 0)..Point::new(3, 12), "")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!( @@ -833,7 +833,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit([(5..5, "\nb")], Some(AutoindentMode::Independent), cx); + buffer.edit([(5..5, "\nb")], Some(AutoindentMode::EachLine), cx); assert_eq!( buffer.text(), " @@ -847,7 +847,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte // is now at the beginning of the line. buffer.edit( [(Point::new(1, 4)..Point::new(1, 5), "")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!( @@ -871,7 +871,7 @@ fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut MutableAppContext) { let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); buffer.edit( [(0..1, "\n"), (2..3, "\n")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!(buffer.text(), "\n\n\n"); @@ -896,7 +896,7 @@ fn test_autoindent_multi_line_insertion(cx: &mut MutableAppContext) { let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); buffer.edit( [(Point::new(3, 0)..Point::new(3, 0), "e(\n f()\n);\n")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!( @@ -945,7 +945,7 @@ fn test_autoindent_block_mode(cx: &mut MutableAppContext) { buffer.edit( [(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())], Some(AutoindentMode::Block { - start_columns: vec![0], + original_indent_columns: vec![0], }), cx, ); @@ -970,7 +970,7 @@ fn test_autoindent_block_mode(cx: &mut MutableAppContext) { buffer.edit( [(Point::new(2, 8)..Point::new(2, 8), inserted_text.clone())], Some(AutoindentMode::Block { - start_columns: vec![0], + original_indent_columns: vec![0], }), cx, ); @@ -1017,7 +1017,7 @@ fn test_autoindent_language_without_indents_query(cx: &mut MutableAppContext) { ); buffer.edit( [(Point::new(3, 0)..Point::new(3, 0), "\n")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!( diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 13055a9df7dad1fe8094d8198a410d9abea857bc..39663e0db42ebf28974dc075a6f3f3cea06c2059 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -278,7 +278,7 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext) { } } drop(snapshot); - buffer.edit(edits, Some(AutoindentMode::Independent), cx); + buffer.edit(edits, Some(AutoindentMode::EachLine), cx); }); editor.change_selections(Some(Autoscroll::Fit), cx, |s| { diff --git a/crates/vim/src/utils.rs b/crates/vim/src/utils.rs index 75bb7445ffb42e8c62ff0b15a2e517eb6b0c5ea3..6f682f61462ab731f79d961d3ad031df8c20c202 100644 --- a/crates/vim/src/utils.rs +++ b/crates/vim/src/utils.rs @@ -1,6 +1,5 @@ use editor::{ClipboardSelection, Editor}; use gpui::{ClipboardItem, MutableAppContext}; -use std::cmp; pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut MutableAppContext) { let selections = editor.selections.all_adjusted(cx); @@ -18,10 +17,7 @@ pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut Mut clipboard_selections.push(ClipboardSelection { len: text.len() - initial_len, is_entire_line: linewise, - first_line_indent: cmp::min( - start.column, - buffer.indent_size_for_line(start.row).len, - ), + first_line_indent: buffer.indent_size_for_line(start.row).len, }); } } diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index d9772c031b7af99a4b44e33090e2efe01e89bb6d..76fea2e2051ae80899ae9d66d2f787dc8f9ecd5f 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -254,7 +254,7 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext } } drop(snapshot); - buffer.edit(edits, Some(AutoindentMode::Independent), cx); + buffer.edit(edits, Some(AutoindentMode::EachLine), cx); }); editor.change_selections(Some(Autoscroll::Fit), cx, |s| { diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 074f51bc8fa9b084963a716b327abd60a318101a..a4db4b9a755d030de8d98039beea31fa5312e109 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -269,21 +269,17 @@ mod tests { // indent inside braces let ix = buffer.len() - 1; - buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::Independent), cx); + buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "int main() {\n \n}"); // indent body of single-statement if statement let ix = buffer.len() - 2; - buffer.edit( - [(ix..ix, "if (a)\nb;")], - Some(AutoindentMode::Independent), - cx, - ); + buffer.edit([(ix..ix, "if (a)\nb;")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "int main() {\n if (a)\n b;\n}"); // indent inside field expression let ix = buffer.len() - 3; - buffer.edit([(ix..ix, "\n.c")], Some(AutoindentMode::Independent), cx); + buffer.edit([(ix..ix, "\n.c")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "int main() {\n if (a)\n b\n .c;\n}"); buffer diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index f76b4e0fbaad03a97cd0bce946a9d77ae4260aaf..801c7c96f9539af14d8013f9292291e6ee42b54f 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -163,7 +163,7 @@ mod tests { let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx); let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext| { let ix = buffer.len(); - buffer.edit([(ix..ix, text)], Some(AutoindentMode::Independent), cx); + buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx); }; // indent after "def():" @@ -209,7 +209,7 @@ mod tests { let argument_ix = buffer.text().find("1").unwrap(); buffer.edit( [(argument_ix..argument_ix + 1, "")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!( @@ -228,7 +228,7 @@ mod tests { let end_whitespace_ix = buffer.len() - 4; buffer.edit( [(end_whitespace_ix..buffer.len(), "")], - Some(AutoindentMode::Independent), + Some(AutoindentMode::EachLine), cx, ); assert_eq!( diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 8e804f0c7f89cff2856bbafd435dcea8185668e9..adbe4312796ed5407942a5ca084eabd17a3c4deb 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -444,29 +444,29 @@ mod tests { // indent between braces buffer.set_text("fn a() {}", cx); let ix = buffer.len() - 1; - buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::Independent), cx); + buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n \n}"); // indent between braces, even after empty lines buffer.set_text("fn a() {\n\n\n}", cx); let ix = buffer.len() - 2; - buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::Independent), cx); + buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n\n\n \n}"); // indent a line that continues a field expression buffer.set_text("fn a() {\n \n}", cx); let ix = buffer.len() - 2; - buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::Independent), cx); + buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n b\n .c\n}"); // indent further lines that continue the field expression, even after empty lines let ix = buffer.len() - 2; - buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::Independent), cx); + buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n b\n .c\n \n .d\n}"); // dedent the line after the field expression let ix = buffer.len() - 2; - buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::Independent), cx); + buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx); assert_eq!( buffer.text(), "fn a() {\n b\n .c\n \n .d;\n e\n}" @@ -475,21 +475,17 @@ mod tests { // indent inside a struct within a call buffer.set_text("const a: B = c(D {});", cx); let ix = buffer.len() - 3; - buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::Independent), cx); + buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "const a: B = c(D {\n \n});"); // indent further inside a nested call let ix = buffer.len() - 4; - buffer.edit( - [(ix..ix, "e: f(\n\n)")], - Some(AutoindentMode::Independent), - cx, - ); + buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "const a: B = c(D {\n e: f(\n \n )\n});"); // keep that indent after an empty line let ix = buffer.len() - 8; - buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::Independent), cx); + buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx); assert_eq!( buffer.text(), "const a: B = c(D {\n e: f(\n \n \n )\n});"