diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index af1e1be89a8a50a8ba66633ba9f7188015809b31..a30bc817540b8c8c6467e314dc1bc661c03e4f81 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4321,10 +4321,14 @@ impl Editor { let snapshot = self.buffer.read(cx).read(cx); let mut clear_linked_edit_ranges = false; let mut all_selections_read_only = true; + let mut has_adjacent_edits = false; + let mut in_adjacent_group = false; - for (selection, autoclose_region) in - self.selections_with_autoclose_regions(selections, &snapshot) - { + let mut regions = self + .selections_with_autoclose_regions(selections, &snapshot) + .peekable(); + + while let Some((selection, autoclose_region)) = regions.next() { if snapshot .point_to_buffer_point(selection.head()) .is_none_or(|(snapshot, ..)| !snapshot.capability.editable()) @@ -4585,10 +4589,21 @@ impl Editor { continue; } + let next_is_adjacent = regions + .peek() + .is_some_and(|(next, _)| selection.end == next.start); + // If not handling any auto-close operation, then just replace the selected // text with the given input and move the selection to the end of the // newly inserted text. - let anchor = snapshot.anchor_after(selection.end); + let anchor = if in_adjacent_group || next_is_adjacent { + // After edits the right bias would shift those anchor to the next visible fragment + // but we want to resolve to the previous one + snapshot.anchor_before(selection.end) + } else { + snapshot.anchor_after(selection.end) + }; + if !self.linked_edit_ranges.is_empty() { let start_anchor = snapshot.anchor_before(selection.start); @@ -4617,12 +4632,16 @@ impl Editor { new_selections.push((selection.map(|_| anchor), 0)); edits.push((selection.start..selection.end, text.clone())); + + has_adjacent_edits |= next_is_adjacent; + in_adjacent_group = next_is_adjacent; } if all_selections_read_only { return; } + drop(regions); drop(snapshot); self.transact(window, cx, |this, window, cx| { @@ -4633,7 +4652,11 @@ impl Editor { jsx_tag_auto_close::construct_initial_buffer_versions_map(this, &edits, cx); this.buffer.update(cx, |buffer, cx| { - buffer.edit(edits, this.autoindent_mode.clone(), cx); + if has_adjacent_edits { + buffer.edit_non_coalesce(edits, this.autoindent_mode.clone(), cx); + } else { + buffer.edit(edits, this.autoindent_mode.clone(), cx); + } }); for (buffer, edits) in linked_edits { buffer.update(cx, |buffer, cx| { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 3bfdf13968ff0f1a2612e27b6134f0523a81724e..6b3c90c40a5ce494567d60f1283eca3e72c5ef90 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -520,11 +520,10 @@ struct AutoindentRequest { 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, + /// The row of the edit start in the buffer before the edit was applied. + /// This is stored here because the anchor in range is created after + /// the edit, so it cannot be used with the before_edit snapshot. + old_row: Option, indent_size: IndentSize, original_indent_column: Option, } @@ -1953,8 +1952,7 @@ impl Buffer { let new_end_row = entry.range.end.to_point(&snapshot).row + 1; language_indent_sizes_by_new_row.push((new_row, entry.indent_size)); - if !entry.first_line_is_new { - let old_row = position.to_point(&request.before_edit).row; + if let Some(old_row) = entry.old_row { old_to_new_rows.insert(old_row, new_row); } row_ranges.push((new_row..new_end_row, entry.original_indent_column)); @@ -2569,7 +2567,7 @@ impl Buffer { } /// Applies the given edits to the buffer. Each edit is specified as a range of text to - /// delete, and a string of text to insert at that location. + /// delete, and a string of text to insert at that location. Adjacent edits are coalesced. /// /// If an [`AutoindentMode`] is provided, then the buffer will enqueue an auto-indent /// request for the edited ranges, which will be processed when the buffer finishes @@ -2583,6 +2581,36 @@ impl Buffer { autoindent_mode: Option, cx: &mut Context, ) -> Option + where + I: IntoIterator, T)>, + S: ToOffset, + T: Into>, + { + self.edit_internal(edits_iter, autoindent_mode, true, cx) + } + + /// Like [`edit`](Self::edit), but does not coalesce adjacent edits. + pub fn edit_non_coalesce( + &mut self, + edits_iter: I, + autoindent_mode: Option, + cx: &mut Context, + ) -> Option + where + I: IntoIterator, T)>, + S: ToOffset, + T: Into>, + { + self.edit_internal(edits_iter, autoindent_mode, false, cx) + } + + fn edit_internal( + &mut self, + edits_iter: I, + autoindent_mode: Option, + coalesce_adjacent: bool, + cx: &mut Context, + ) -> Option where I: IntoIterator, T)>, S: ToOffset, @@ -2599,8 +2627,17 @@ impl Buffer { } let new_text = new_text.into(); if !new_text.is_empty() || !range.is_empty() { - if let Some((prev_range, prev_text)) = edits.last_mut() - && prev_range.end >= range.start + let prev_edit = edits.last_mut(); + let should_coalesce = prev_edit.as_ref().is_some_and(|(prev_range, _)| { + if coalesce_adjacent { + prev_range.end >= range.start + } else { + prev_range.end > range.start + } + }); + + if let Some((prev_range, prev_text)) = prev_edit + && should_coalesce { prev_range.end = cmp::max(prev_range.end, range.end); *prev_text = format!("{prev_text}{new_text}").into(); @@ -2708,8 +2745,12 @@ impl Buffer { } AutoindentRequestEntry { - first_line_is_new, original_indent_column, + old_row: if first_line_is_new { + None + } else { + Some(old_start.row) + }, indent_size: before_edit.language_indent_size_at(range.start, cx), range: self.anchor_before(new_start + range_of_insertion_to_indent.start) ..self.anchor_after(new_start + range_of_insertion_to_indent.end), @@ -2757,7 +2798,7 @@ impl Buffer { .into_iter() .map(|range| AutoindentRequestEntry { range: before_edit.anchor_before(range.start)..before_edit.anchor_after(range.end), - first_line_is_new: true, + old_row: None, indent_size: before_edit.language_indent_size_at(range.start, cx), original_indent_column: None, }) diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index bcbd734598df8a92f61951586611a45947960649..cf48af98475190d15e04309b509f09f179886f20 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -1257,6 +1257,33 @@ impl MultiBuffer { I: IntoIterator, T)>, S: ToOffset, T: Into>, + { + self.edit_internal(edits, autoindent_mode, true, cx); + } + + pub fn edit_non_coalesce( + &mut self, + edits: I, + autoindent_mode: Option, + cx: &mut Context, + ) where + I: IntoIterator, T)>, + S: ToOffset, + T: Into>, + { + self.edit_internal(edits, autoindent_mode, false, cx); + } + + fn edit_internal( + &mut self, + edits: I, + autoindent_mode: Option, + coalesce_adjacent: bool, + cx: &mut Context, + ) where + I: IntoIterator, T)>, + S: ToOffset, + T: Into>, { if self.read_only() || self.buffers.is_empty() { return; @@ -1274,13 +1301,14 @@ impl MultiBuffer { }) .collect::>(); - return edit_internal(self, edits, autoindent_mode, cx); + return edit_internal(self, edits, autoindent_mode, coalesce_adjacent, cx); // Non-generic part of edit, hoisted out to avoid blowing up LLVM IR. fn edit_internal( this: &mut MultiBuffer, edits: Vec<(Range, Arc)>, mut autoindent_mode: Option, + coalesce_adjacent: bool, cx: &mut Context, ) { let original_indent_columns = match &mut autoindent_mode { @@ -1322,7 +1350,13 @@ impl MultiBuffer { .. }) = edits.peek() { - if range.end >= next_range.start { + let should_coalesce = if coalesce_adjacent { + range.end >= next_range.start + } else { + range.end > next_range.start + }; + + if should_coalesce { range.end = cmp::max(next_range.end, range.end); is_insertion |= *next_is_insertion; if excerpt_id == *next_excerpt_id { @@ -1365,8 +1399,13 @@ impl MultiBuffer { autoindent_mode.clone() }; - buffer.edit(deletions, deletion_autoindent_mode, cx); - buffer.edit(insertions, insertion_autoindent_mode, cx); + if coalesce_adjacent { + buffer.edit(deletions, deletion_autoindent_mode, cx); + buffer.edit(insertions, insertion_autoindent_mode, cx); + } else { + buffer.edit_non_coalesce(deletions, deletion_autoindent_mode, cx); + buffer.edit_non_coalesce(insertions, insertion_autoindent_mode, cx); + } }) }