Early draft for #32051 (multi-cursor editing is very slow, and basically hangs at high cursor counts). Opening it early like @Anthony-Eid suggested so we can agree on direction before I go further. ## Root cause Typing with a cursor on every line is mostly O(N)-per-keystroke work spread across a few places. I profiled it with `sample` and per-phase timers around `handle_input`. At 1000 cursors (~24ms before this PR): the post-edit display-map sync is ~11.8ms, the CRDT edit (`apply_local_edit`) ~3.7ms, resolving all N selections plus the per-cursor input loop ~3.5ms, the post-edit selection round-trip ~2.6ms, and `change_selections` plus transact machinery ~2.4ms. The display sync re-runs per-edit `SumTree` work through every layer even when nothing transforms (the plain-text case), and selections get re-resolved through the full display round-trip several times per keystroke. ## What this does - Display fast paths: `InlayMap::sync` (no inlays) and `WrapMap::interpolate` (no soft-wrap) skip the O(edits) transform-tree rebuild and return the passthrough snapshot, gated so a pending inlay splice or existing wraps still take the slow path. - Selection fast path: when nothing collapses buffer content (`!has_folds() && !has_replacement_blocks()`), resolve `Anchor` to `Point` to `Offset` batched and skip the per-selection display round-trip (the `todo(lw)`). - Render and autoscroll resolve only the first/last/newest selections instead of all N per frame. - A parameterized `Multi-cursor input/cursors/{1000,10000,100000}` benchmark. ## Results - Typing (`handle_input`): ~2.1x faster (24ms to 11.7ms at 1k). - Type plus two word-deletes (criterion): 29% faster (89.9 to 63.8ms) at 1k, 37% (959 to 606ms) at 10k. - Post-edit display sync alone: 11.8 to 3.8ms at 1k, 133 to 45ms at 10k. All `editor` (759), `display_map` (67), `multi_buffer` (58), and `text` (36) tests pass, and `./script/clippy` is clean. ## Direction This is ~2x, and I can get the current architecture to roughly 3.5-4.5x with a few more safe changes (hoisting the per-cursor language/editability checks, cheaper snapshot clones). Genuine VS Code numbers (~1-2us/cursor) aren't reachable while the buffer is a CRDT rope of fragments with anchor selections and a 5-layer transform stack. That would need a plain-offset cursor model and/or decoupling display layout from the edit path, which is a bigger effort I'd want to design with you. One thing I'd need your call on: `set_active_selections` sends an `UpdateSelections` collab op every keystroke (building N anchors); can that be debounced to transaction end, or do presence/follow-mode features rely on per-keystroke cursor broadcast? Release Notes: - editor: Improved multi cursor editing performance --------- Co-authored-by: Anthony Eid <anthony@zed.dev>
1471 lines
50 KiB
Rust
1471 lines
50 KiB
Rust
use std::{
|
|
cmp, fmt, iter, mem,
|
|
ops::{AddAssign, Deref, DerefMut, Range, Sub},
|
|
sync::Arc,
|
|
};
|
|
|
|
use gpui::Pixels;
|
|
use itertools::{Either, Itertools as _};
|
|
use language::{Bias, Point, PointUtf16, Selection, SelectionGoal};
|
|
use multi_buffer::{MultiBufferDimension, MultiBufferOffset};
|
|
use util::post_inc;
|
|
|
|
use crate::{
|
|
Anchor, DisplayPoint, DisplayRow, MultiBufferSnapshot, SelectMode, ToOffset,
|
|
display_map::{DisplaySnapshot, ToDisplayPoint},
|
|
movement::TextLayoutDetails,
|
|
};
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct PendingSelection {
|
|
selection: Selection<Anchor>,
|
|
mode: SelectMode,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct SelectionsCollection {
|
|
next_selection_id: usize,
|
|
line_mode: bool,
|
|
/// The non-pending, non-overlapping selections.
|
|
/// The [SelectionsCollection::pending] selection could possibly overlap these
|
|
disjoint: Arc<[Selection<Anchor>]>,
|
|
/// A pending selection, such as when the mouse is being dragged
|
|
pending: Option<PendingSelection>,
|
|
select_mode: SelectMode,
|
|
is_extending: bool,
|
|
}
|
|
|
|
impl SelectionsCollection {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
next_selection_id: 1,
|
|
line_mode: false,
|
|
disjoint: Arc::default(),
|
|
pending: Some(PendingSelection {
|
|
selection: Selection {
|
|
id: 0,
|
|
start: Anchor::Min,
|
|
end: Anchor::Min,
|
|
reversed: false,
|
|
goal: SelectionGoal::None,
|
|
},
|
|
mode: SelectMode::Character,
|
|
}),
|
|
select_mode: SelectMode::Character,
|
|
is_extending: false,
|
|
}
|
|
}
|
|
|
|
pub fn clone_state(&mut self, other: &SelectionsCollection) {
|
|
self.next_selection_id = other.next_selection_id;
|
|
self.line_mode = other.line_mode;
|
|
self.disjoint = other.disjoint.clone();
|
|
self.pending.clone_from(&other.pending);
|
|
}
|
|
|
|
pub fn count(&self) -> usize {
|
|
let mut count = self.disjoint.len();
|
|
if self.pending.is_some() {
|
|
count += 1;
|
|
}
|
|
count
|
|
}
|
|
|
|
/// The non-pending, non-overlapping selections. There could be a pending selection that
|
|
/// overlaps these if the mouse is being dragged, etc. This could also be empty if there is a
|
|
/// pending selection. Returned as selections over Anchors.
|
|
pub fn disjoint_anchors_arc(&self) -> Arc<[Selection<Anchor>]> {
|
|
self.disjoint.clone()
|
|
}
|
|
|
|
/// The non-pending, non-overlapping selections. There could be a pending selection that
|
|
/// overlaps these if the mouse is being dragged, etc. This could also be empty if there is a
|
|
/// pending selection. Returned as selections over Anchors.
|
|
pub fn disjoint_anchors(&self) -> &[Selection<Anchor>] {
|
|
&self.disjoint
|
|
}
|
|
|
|
pub fn disjoint_anchor_ranges(&self) -> impl Iterator<Item = Range<Anchor>> {
|
|
// Mapping the Arc slice would borrow it, whereas indexing captures it.
|
|
let disjoint = self.disjoint_anchors_arc();
|
|
(0..disjoint.len()).map(move |ix| disjoint[ix].range())
|
|
}
|
|
|
|
/// Non-overlapping selections using anchors, including the pending selection.
|
|
pub fn all_anchors(&self, snapshot: &DisplaySnapshot) -> Arc<[Selection<Anchor>]> {
|
|
if self.pending.is_none() {
|
|
self.disjoint_anchors_arc()
|
|
} else {
|
|
let all_offset_selections = self.all::<MultiBufferOffset>(snapshot);
|
|
all_offset_selections
|
|
.into_iter()
|
|
.map(|selection| selection_to_anchor_selection(selection, snapshot))
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
pub fn pending_anchor(&self) -> Option<&Selection<Anchor>> {
|
|
self.pending.as_ref().map(|pending| &pending.selection)
|
|
}
|
|
|
|
pub fn pending<D>(&self, snapshot: &DisplaySnapshot) -> Option<Selection<D>>
|
|
where
|
|
D: MultiBufferDimension + Sub + AddAssign<<D as Sub>::Output> + Ord,
|
|
{
|
|
resolve_selections_wrapping_blocks(self.pending_anchor(), &snapshot).next()
|
|
}
|
|
|
|
pub(crate) fn pending_mode(&self) -> Option<SelectMode> {
|
|
self.pending.as_ref().map(|pending| pending.mode.clone())
|
|
}
|
|
|
|
pub fn all<D>(&self, snapshot: &DisplaySnapshot) -> Vec<Selection<D>>
|
|
where
|
|
D: MultiBufferDimension + Sub + AddAssign<<D as Sub>::Output> + Ord,
|
|
{
|
|
self.all_iter(snapshot).collect()
|
|
}
|
|
|
|
fn all_iter<'a, D>(
|
|
&'a self,
|
|
snapshot: &'a DisplaySnapshot,
|
|
) -> impl 'a + Iterator<Item = Selection<D>>
|
|
where
|
|
D: 'a + MultiBufferDimension + Sub + AddAssign<<D as Sub>::Output> + Ord,
|
|
{
|
|
let mut disjoint =
|
|
resolve_selections_wrapping_blocks::<D, _>(self.disjoint.iter(), snapshot).peekable();
|
|
let mut pending_opt = self.pending::<D>(snapshot);
|
|
iter::from_fn(move || {
|
|
if let Some(pending) = pending_opt.as_mut() {
|
|
while let Some(next_selection) = disjoint.peek() {
|
|
if should_merge(
|
|
pending.start,
|
|
pending.end,
|
|
next_selection.start,
|
|
next_selection.end,
|
|
false,
|
|
) {
|
|
let next_selection = disjoint.next().unwrap();
|
|
if next_selection.start < pending.start {
|
|
pending.start = next_selection.start;
|
|
}
|
|
if next_selection.end > pending.end {
|
|
pending.end = next_selection.end;
|
|
}
|
|
} else if next_selection.end < pending.start {
|
|
return disjoint.next();
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
pending_opt.take()
|
|
} else {
|
|
disjoint.next()
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Returns all of the selections, adjusted to take into account the selection line_mode
|
|
pub fn all_adjusted(&self, snapshot: &DisplaySnapshot) -> Vec<Selection<Point>> {
|
|
let mut selections = self.all::<Point>(&snapshot);
|
|
if self.line_mode {
|
|
for selection in &mut selections {
|
|
let new_range = snapshot.expand_to_line(selection.range());
|
|
selection.start = new_range.start;
|
|
selection.end = new_range.end;
|
|
}
|
|
}
|
|
selections
|
|
}
|
|
|
|
/// Returns the newest selection, adjusted to take into account the selection line_mode
|
|
pub fn newest_adjusted(&self, snapshot: &DisplaySnapshot) -> Selection<Point> {
|
|
let mut selection = self.newest::<Point>(&snapshot);
|
|
if self.line_mode {
|
|
let new_range = snapshot.expand_to_line(selection.range());
|
|
selection.start = new_range.start;
|
|
selection.end = new_range.end;
|
|
}
|
|
selection
|
|
}
|
|
|
|
pub fn all_adjusted_display(
|
|
&self,
|
|
display_map: &DisplaySnapshot,
|
|
) -> Vec<Selection<DisplayPoint>> {
|
|
if self.line_mode {
|
|
let selections = self.all::<Point>(&display_map);
|
|
let result = selections
|
|
.into_iter()
|
|
.map(|mut selection| {
|
|
let new_range = display_map.expand_to_line(selection.range());
|
|
selection.start = new_range.start;
|
|
selection.end = new_range.end;
|
|
selection.map(|point| point.to_display_point(&display_map))
|
|
})
|
|
.collect();
|
|
result
|
|
} else {
|
|
self.all_display(display_map)
|
|
}
|
|
}
|
|
|
|
pub fn disjoint_in_range<D>(
|
|
&self,
|
|
range: Range<Anchor>,
|
|
snapshot: &DisplaySnapshot,
|
|
) -> Vec<Selection<D>>
|
|
where
|
|
D: MultiBufferDimension + Sub + AddAssign<<D as Sub>::Output> + Ord + std::fmt::Debug,
|
|
{
|
|
let start_ix = match self
|
|
.disjoint
|
|
.binary_search_by(|probe| probe.end.cmp(&range.start, snapshot.buffer_snapshot()))
|
|
{
|
|
Ok(ix) | Err(ix) => ix,
|
|
};
|
|
let end_ix = match self
|
|
.disjoint
|
|
.binary_search_by(|probe| probe.start.cmp(&range.end, snapshot.buffer_snapshot()))
|
|
{
|
|
Ok(ix) => ix + 1,
|
|
Err(ix) => ix,
|
|
};
|
|
resolve_selections_wrapping_blocks(&self.disjoint[start_ix..end_ix], snapshot).collect()
|
|
}
|
|
|
|
pub fn all_display(&self, snapshot: &DisplaySnapshot) -> Vec<Selection<DisplayPoint>> {
|
|
let disjoint_anchors = &self.disjoint;
|
|
let mut disjoint =
|
|
resolve_selections_display(disjoint_anchors.iter(), &snapshot).peekable();
|
|
let mut pending_opt = resolve_selections_display(self.pending_anchor(), &snapshot).next();
|
|
iter::from_fn(move || {
|
|
if let Some(pending) = pending_opt.as_mut() {
|
|
while let Some(next_selection) = disjoint.peek() {
|
|
if should_merge(
|
|
pending.start,
|
|
pending.end,
|
|
next_selection.start,
|
|
next_selection.end,
|
|
false,
|
|
) {
|
|
let next_selection = disjoint.next().unwrap();
|
|
if next_selection.start < pending.start {
|
|
pending.start = next_selection.start;
|
|
}
|
|
if next_selection.end > pending.end {
|
|
pending.end = next_selection.end;
|
|
}
|
|
} else if next_selection.end < pending.start {
|
|
return disjoint.next();
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
pending_opt.take()
|
|
} else {
|
|
disjoint.next()
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub fn newest_anchor(&self) -> &Selection<Anchor> {
|
|
self.pending
|
|
.as_ref()
|
|
.map(|s| &s.selection)
|
|
.or_else(|| self.disjoint.iter().max_by_key(|s| s.id))
|
|
.unwrap()
|
|
}
|
|
|
|
pub fn newest<D>(&self, snapshot: &DisplaySnapshot) -> Selection<D>
|
|
where
|
|
D: MultiBufferDimension + Sub + AddAssign<<D as Sub>::Output> + Ord,
|
|
{
|
|
resolve_selections_wrapping_blocks([self.newest_anchor()], &snapshot)
|
|
.next()
|
|
.unwrap()
|
|
}
|
|
|
|
pub fn newest_display(&self, snapshot: &DisplaySnapshot) -> Selection<DisplayPoint> {
|
|
resolve_selections_display([self.newest_anchor()], &snapshot)
|
|
.next()
|
|
.unwrap()
|
|
}
|
|
|
|
pub fn oldest_anchor(&self) -> &Selection<Anchor> {
|
|
self.disjoint
|
|
.iter()
|
|
.min_by_key(|s| s.id)
|
|
.or_else(|| self.pending.as_ref().map(|p| &p.selection))
|
|
.unwrap()
|
|
}
|
|
|
|
pub fn oldest<D>(&self, snapshot: &DisplaySnapshot) -> Selection<D>
|
|
where
|
|
D: MultiBufferDimension + Sub + AddAssign<<D as Sub>::Output> + Ord,
|
|
{
|
|
resolve_selections_wrapping_blocks([self.oldest_anchor()], &snapshot)
|
|
.next()
|
|
.unwrap()
|
|
}
|
|
|
|
pub fn first_anchor(&self) -> Selection<Anchor> {
|
|
self.pending
|
|
.as_ref()
|
|
.map(|pending| pending.selection.clone())
|
|
.unwrap_or_else(|| self.disjoint.first().cloned().unwrap())
|
|
}
|
|
|
|
pub fn first<D>(&self, snapshot: &DisplaySnapshot) -> Selection<D>
|
|
where
|
|
D: MultiBufferDimension + Sub + AddAssign<<D as Sub>::Output> + Ord,
|
|
{
|
|
// Without a pending selection, the first disjoint selection is also the first resolved
|
|
// selection, so avoid resolving every selection.
|
|
if self.pending.is_none()
|
|
&& let Some(first) = self.disjoint.first()
|
|
{
|
|
return resolve_selections_wrapping_blocks([first], snapshot)
|
|
.next()
|
|
.unwrap();
|
|
}
|
|
self.all_iter(snapshot).next().unwrap()
|
|
}
|
|
|
|
pub fn last<D>(&self, snapshot: &DisplaySnapshot) -> Selection<D>
|
|
where
|
|
D: MultiBufferDimension + Sub + AddAssign<<D as Sub>::Output> + Ord,
|
|
{
|
|
if self.pending.is_none()
|
|
&& let Some(last) = self.disjoint.last()
|
|
{
|
|
return resolve_selections_wrapping_blocks([last], snapshot)
|
|
.next()
|
|
.unwrap();
|
|
}
|
|
self.all_iter(snapshot).last().unwrap()
|
|
}
|
|
|
|
/// Returns a list of (potentially backwards!) ranges representing the selections.
|
|
/// Useful for test assertions, but prefer `.all()` instead.
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
pub fn ranges<D>(&self, snapshot: &DisplaySnapshot) -> Vec<Range<D>>
|
|
where
|
|
D: MultiBufferDimension + Sub + AddAssign<<D as Sub>::Output> + Ord,
|
|
{
|
|
self.all::<D>(snapshot)
|
|
.iter()
|
|
.map(|s| {
|
|
if s.reversed {
|
|
s.end..s.start
|
|
} else {
|
|
s.start..s.end
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
pub fn display_ranges(&self, display_snapshot: &DisplaySnapshot) -> Vec<Range<DisplayPoint>> {
|
|
self.disjoint_anchors_arc()
|
|
.iter()
|
|
.chain(self.pending_anchor())
|
|
.map(|s| {
|
|
if s.reversed {
|
|
s.end.to_display_point(display_snapshot)
|
|
..s.start.to_display_point(display_snapshot)
|
|
} else {
|
|
s.start.to_display_point(display_snapshot)
|
|
..s.end.to_display_point(display_snapshot)
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Attempts to build a selection in the provided `DisplayRow` within the
|
|
/// same range as the provided range of `Pixels`.
|
|
/// Returns `None` if the range is not empty but it starts past the line's
|
|
/// length, meaning that the line isn't long enough to be contained within
|
|
/// part of the provided range.
|
|
pub fn build_columnar_selection(
|
|
&mut self,
|
|
display_map: &DisplaySnapshot,
|
|
row: DisplayRow,
|
|
positions: &Range<Pixels>,
|
|
reversed: bool,
|
|
text_layout_details: &TextLayoutDetails,
|
|
) -> Option<Selection<Point>> {
|
|
let is_empty = positions.start == positions.end;
|
|
let line_len = display_map.line_len(row);
|
|
let line = display_map.layout_row(row, text_layout_details);
|
|
let start_col = line.closest_index_for_x(positions.start) as u32;
|
|
|
|
let (start, end) = if is_empty {
|
|
let point = DisplayPoint::new(row, std::cmp::min(start_col, line_len));
|
|
(point, point)
|
|
} else {
|
|
if start_col >= line_len {
|
|
return None;
|
|
}
|
|
let start = DisplayPoint::new(row, start_col);
|
|
let end_col = line.closest_index_for_x(positions.end) as u32;
|
|
let end = DisplayPoint::new(row, end_col);
|
|
(start, end)
|
|
};
|
|
|
|
Some(Selection {
|
|
id: post_inc(&mut self.next_selection_id),
|
|
start: start.to_point(display_map),
|
|
end: end.to_point(display_map),
|
|
reversed,
|
|
goal: SelectionGoal::HorizontalRange {
|
|
start: positions.start.into(),
|
|
end: positions.end.into(),
|
|
},
|
|
})
|
|
}
|
|
|
|
/// Attempts to build a selection in the provided buffer row using the
|
|
/// same UTF-16 column range as specified.
|
|
/// Returns `None` if the range is not empty but it starts past the line's
|
|
/// length, meaning that the line isn't long enough to be contained within
|
|
/// part of the provided range.
|
|
fn build_columnar_selection_from_utf16_columns(
|
|
&mut self,
|
|
display_map: &DisplaySnapshot,
|
|
buffer_row: u32,
|
|
positions: &Range<u32>,
|
|
reversed: bool,
|
|
text_layout_details: &TextLayoutDetails,
|
|
) -> Option<Selection<Point>> {
|
|
let snapshot = display_map.buffer_snapshot();
|
|
let is_empty = positions.start == positions.end;
|
|
let line_len_utf16 = snapshot.line_len_utf16(multi_buffer::MultiBufferRow(buffer_row));
|
|
|
|
let (start, end) = if is_empty {
|
|
let column = std::cmp::min(positions.start, line_len_utf16);
|
|
let point = snapshot.point_utf16_to_point(PointUtf16::new(buffer_row, column));
|
|
(point, point)
|
|
} else {
|
|
if positions.start >= line_len_utf16 {
|
|
return None;
|
|
}
|
|
|
|
let start = snapshot.point_utf16_to_point(PointUtf16::new(buffer_row, positions.start));
|
|
let end_column = std::cmp::min(positions.end, line_len_utf16);
|
|
let end = snapshot.point_utf16_to_point(PointUtf16::new(buffer_row, end_column));
|
|
(start, end)
|
|
};
|
|
|
|
let start_display_point = start.to_display_point(display_map);
|
|
let end_display_point = end.to_display_point(display_map);
|
|
let start_x = display_map.x_for_display_point(start_display_point, text_layout_details);
|
|
let end_x = display_map.x_for_display_point(end_display_point, text_layout_details);
|
|
|
|
Some(Selection {
|
|
id: post_inc(&mut self.next_selection_id),
|
|
start,
|
|
end,
|
|
reversed,
|
|
goal: SelectionGoal::HorizontalRange {
|
|
start: start_x.min(end_x).into(),
|
|
end: start_x.max(end_x).into(),
|
|
},
|
|
})
|
|
}
|
|
|
|
/// Finds the next columnar selection by walking display rows one at a time
|
|
/// so that soft-wrapped lines are considered and not skipped.
|
|
pub fn find_next_columnar_selection_by_display_row(
|
|
&mut self,
|
|
display_map: &DisplaySnapshot,
|
|
start_row: DisplayRow,
|
|
end_row: DisplayRow,
|
|
above: bool,
|
|
positions: &Range<Pixels>,
|
|
reversed: bool,
|
|
text_layout_details: &TextLayoutDetails,
|
|
) -> Option<Selection<Point>> {
|
|
let mut row = start_row;
|
|
while row != end_row {
|
|
if above {
|
|
row.0 -= 1;
|
|
} else {
|
|
row.0 += 1;
|
|
}
|
|
|
|
if let Some(selection) = self.build_columnar_selection(
|
|
display_map,
|
|
row,
|
|
positions,
|
|
reversed,
|
|
text_layout_details,
|
|
) {
|
|
return Some(selection);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Finds the next columnar selection by skipping to the next buffer row,
|
|
/// ignoring soft-wrapped lines.
|
|
pub fn find_next_columnar_selection_by_buffer_row(
|
|
&mut self,
|
|
display_map: &DisplaySnapshot,
|
|
start_row: DisplayRow,
|
|
end_row: DisplayRow,
|
|
above: bool,
|
|
goal_columns: &Range<u32>,
|
|
reversed: bool,
|
|
text_layout_details: &TextLayoutDetails,
|
|
) -> Option<Selection<Point>> {
|
|
let mut row = start_row;
|
|
let direction = if above { -1 } else { 1 };
|
|
while row != end_row {
|
|
let new_row =
|
|
display_map.start_of_relative_buffer_row(DisplayPoint::new(row, 0), direction);
|
|
row = new_row.row();
|
|
let buffer_row = new_row.to_point(display_map).row;
|
|
|
|
if let Some(selection) = self.build_columnar_selection_from_utf16_columns(
|
|
display_map,
|
|
buffer_row,
|
|
goal_columns,
|
|
reversed,
|
|
text_layout_details,
|
|
) {
|
|
return Some(selection);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
pub fn change_with<R>(
|
|
&mut self,
|
|
snapshot: &DisplaySnapshot,
|
|
change: impl FnOnce(&mut MutableSelectionsCollection<'_, '_>) -> R,
|
|
) -> (bool, R) {
|
|
let mut mutable_collection = MutableSelectionsCollection {
|
|
snapshot,
|
|
collection: self,
|
|
selections_changed: false,
|
|
};
|
|
|
|
let result = change(&mut mutable_collection);
|
|
assert!(
|
|
!mutable_collection.disjoint.is_empty() || mutable_collection.pending.is_some(),
|
|
"There must be at least one selection"
|
|
);
|
|
if cfg!(debug_assertions) {
|
|
mutable_collection.disjoint.iter().for_each(|selection| {
|
|
assert!(
|
|
selection.start.cmp(&selection.end, &snapshot).is_le(),
|
|
"disjoint selection has start > end: {:?}",
|
|
mutable_collection.disjoint
|
|
);
|
|
assert!(
|
|
snapshot.can_resolve(&selection.start),
|
|
"disjoint selection start is not resolvable for the given snapshot:\n{selection:?}",
|
|
);
|
|
assert!(
|
|
snapshot.can_resolve(&selection.end),
|
|
"disjoint selection start is not resolvable for the given snapshot:\n{selection:?}",
|
|
);
|
|
});
|
|
assert!(
|
|
mutable_collection
|
|
.disjoint
|
|
.is_sorted_by(|first, second| first.end.cmp(&second.start, &snapshot).is_le()),
|
|
"disjoint selections are not sorted: {:?}",
|
|
mutable_collection.disjoint
|
|
);
|
|
if let Some(pending) = &mutable_collection.pending {
|
|
let selection = &pending.selection;
|
|
assert!(
|
|
selection.start.cmp(&selection.end, &snapshot).is_le(),
|
|
"pending selection has start > end: {:?}",
|
|
selection
|
|
);
|
|
assert!(
|
|
snapshot.can_resolve(&selection.start),
|
|
"pending selection start is not resolvable for the given snapshot: {pending:?}",
|
|
);
|
|
assert!(
|
|
snapshot.can_resolve(&selection.end),
|
|
"pending selection end is not resolvable for the given snapshot: {pending:?}",
|
|
);
|
|
}
|
|
}
|
|
(mutable_collection.selections_changed, result)
|
|
}
|
|
|
|
pub fn next_selection_id(&self) -> usize {
|
|
self.next_selection_id
|
|
}
|
|
|
|
pub fn line_mode(&self) -> bool {
|
|
self.line_mode
|
|
}
|
|
|
|
pub fn set_line_mode(&mut self, line_mode: bool) {
|
|
self.line_mode = line_mode;
|
|
}
|
|
|
|
pub fn select_mode(&self) -> &SelectMode {
|
|
&self.select_mode
|
|
}
|
|
|
|
pub fn set_select_mode(&mut self, select_mode: SelectMode) {
|
|
self.select_mode = select_mode;
|
|
}
|
|
|
|
pub fn is_extending(&self) -> bool {
|
|
self.is_extending
|
|
}
|
|
|
|
pub fn set_is_extending(&mut self, is_extending: bool) {
|
|
self.is_extending = is_extending;
|
|
}
|
|
}
|
|
|
|
pub struct MutableSelectionsCollection<'snap, 'a> {
|
|
collection: &'a mut SelectionsCollection,
|
|
snapshot: &'snap DisplaySnapshot,
|
|
selections_changed: bool,
|
|
}
|
|
|
|
impl<'snap, 'a> fmt::Debug for MutableSelectionsCollection<'snap, 'a> {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.debug_struct("MutableSelectionsCollection")
|
|
.field("collection", &self.collection)
|
|
.field("selections_changed", &self.selections_changed)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl<'snap, 'a> MutableSelectionsCollection<'snap, 'a> {
|
|
pub fn display_snapshot(&self) -> DisplaySnapshot {
|
|
self.snapshot.clone()
|
|
}
|
|
|
|
pub fn clear_disjoint(&mut self) {
|
|
self.collection.disjoint = Arc::default();
|
|
}
|
|
|
|
pub fn delete(&mut self, selection_id: usize) {
|
|
let mut changed = false;
|
|
self.collection.disjoint = self
|
|
.disjoint
|
|
.iter()
|
|
.filter(|selection| {
|
|
let found = selection.id == selection_id;
|
|
changed |= found;
|
|
!found
|
|
})
|
|
.cloned()
|
|
.collect();
|
|
|
|
self.selections_changed |= changed;
|
|
}
|
|
|
|
pub fn remove_selections_from_buffer(&mut self, buffer_id: language::BufferId) {
|
|
let mut changed = false;
|
|
|
|
let filtered_selections: Arc<[Selection<Anchor>]> = {
|
|
self.disjoint
|
|
.iter()
|
|
.filter(|selection| {
|
|
if let Some((selection_buffer_anchor, _)) =
|
|
self.snapshot.anchor_to_buffer_anchor(selection.start)
|
|
{
|
|
let should_remove = selection_buffer_anchor.buffer_id == buffer_id;
|
|
changed |= should_remove;
|
|
!should_remove
|
|
} else {
|
|
true
|
|
}
|
|
})
|
|
.cloned()
|
|
.collect()
|
|
};
|
|
|
|
if filtered_selections.is_empty() {
|
|
let buffer_snapshot = self.snapshot.buffer_snapshot();
|
|
let anchor = buffer_snapshot
|
|
.excerpts()
|
|
.find(|excerpt| excerpt.context.start.buffer_id == buffer_id)
|
|
.and_then(|excerpt| buffer_snapshot.anchor_in_excerpt(excerpt.context.start))
|
|
.unwrap_or_else(|| self.snapshot.anchor_before(MultiBufferOffset(0)));
|
|
self.collection.disjoint = Arc::from([Selection {
|
|
id: post_inc(&mut self.collection.next_selection_id),
|
|
start: anchor,
|
|
end: anchor,
|
|
reversed: false,
|
|
goal: SelectionGoal::None,
|
|
}]);
|
|
} else {
|
|
self.collection.disjoint = filtered_selections;
|
|
}
|
|
|
|
self.selections_changed |= changed;
|
|
}
|
|
|
|
pub fn clear_pending(&mut self) {
|
|
if self.collection.pending.is_some() {
|
|
self.collection.pending = None;
|
|
self.selections_changed = true;
|
|
}
|
|
}
|
|
|
|
pub(crate) fn set_pending_anchor_range(&mut self, range: Range<Anchor>, mode: SelectMode) {
|
|
self.collection.pending = Some(PendingSelection {
|
|
selection: {
|
|
let mut start = range.start;
|
|
let mut end = range.end;
|
|
let reversed = if start.cmp(&end, self.snapshot).is_gt() {
|
|
mem::swap(&mut start, &mut end);
|
|
true
|
|
} else {
|
|
false
|
|
};
|
|
Selection {
|
|
id: post_inc(&mut self.collection.next_selection_id),
|
|
start,
|
|
end,
|
|
reversed,
|
|
goal: SelectionGoal::None,
|
|
}
|
|
},
|
|
mode,
|
|
});
|
|
self.selections_changed = true;
|
|
}
|
|
|
|
pub(crate) fn set_pending(&mut self, selection: Selection<Anchor>, mode: SelectMode) {
|
|
self.collection.pending = Some(PendingSelection { selection, mode });
|
|
self.selections_changed = true;
|
|
}
|
|
|
|
pub fn try_cancel(&mut self) -> bool {
|
|
if let Some(pending) = self.collection.pending.take() {
|
|
if self.disjoint.is_empty() {
|
|
self.collection.disjoint = Arc::from([pending.selection]);
|
|
}
|
|
self.selections_changed = true;
|
|
return true;
|
|
}
|
|
|
|
let mut oldest = self.oldest_anchor().clone();
|
|
if self.count() > 1 {
|
|
self.collection.disjoint = Arc::from([oldest]);
|
|
self.selections_changed = true;
|
|
return true;
|
|
}
|
|
|
|
if !oldest.start.cmp(&oldest.end, self.snapshot).is_eq() {
|
|
let head = oldest.head();
|
|
oldest.start = head;
|
|
oldest.end = head;
|
|
self.collection.disjoint = Arc::from([oldest]);
|
|
self.selections_changed = true;
|
|
return true;
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
pub fn insert_range<T>(&mut self, range: Range<T>)
|
|
where
|
|
T: ToOffset,
|
|
{
|
|
let display_map = self.display_snapshot();
|
|
let mut selections = self.collection.all(&display_map);
|
|
let mut start = range.start.to_offset(self.snapshot);
|
|
let mut end = range.end.to_offset(self.snapshot);
|
|
let reversed = if start > end {
|
|
mem::swap(&mut start, &mut end);
|
|
true
|
|
} else {
|
|
false
|
|
};
|
|
selections.push(Selection {
|
|
id: post_inc(&mut self.collection.next_selection_id),
|
|
start,
|
|
end,
|
|
reversed,
|
|
goal: SelectionGoal::None,
|
|
});
|
|
self.select(selections);
|
|
}
|
|
|
|
pub fn select<T>(&mut self, selections: Vec<Selection<T>>)
|
|
where
|
|
T: ToOffset + std::marker::Copy + std::fmt::Debug,
|
|
{
|
|
let mut selections = selections
|
|
.into_iter()
|
|
.map(|selection| selection.map(|it| it.to_offset(self.snapshot)))
|
|
.map(|mut selection| {
|
|
if selection.start > selection.end {
|
|
mem::swap(&mut selection.start, &mut selection.end);
|
|
selection.reversed = true
|
|
}
|
|
selection
|
|
})
|
|
.collect::<Vec<_>>();
|
|
selections.sort_unstable_by_key(|s| s.start);
|
|
|
|
let mut i = 1;
|
|
while i < selections.len() {
|
|
let prev = &selections[i - 1];
|
|
let current = &selections[i];
|
|
|
|
if should_merge(prev.start, prev.end, current.start, current.end, true) {
|
|
let removed = selections.remove(i);
|
|
if removed.start < selections[i - 1].start {
|
|
selections[i - 1].start = removed.start;
|
|
}
|
|
if selections[i - 1].end < removed.end {
|
|
selections[i - 1].end = removed.end;
|
|
}
|
|
} else {
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
self.collection.disjoint = Arc::from_iter(
|
|
selections
|
|
.into_iter()
|
|
.map(|selection| selection_to_anchor_selection(selection, self.snapshot)),
|
|
);
|
|
self.collection.pending = None;
|
|
self.selections_changed = true;
|
|
}
|
|
|
|
pub fn select_anchors(&mut self, selections: Vec<Selection<Anchor>>) {
|
|
let map = self.display_snapshot();
|
|
let resolved_selections =
|
|
resolve_selections_wrapping_blocks::<MultiBufferOffset, _>(&selections, &map)
|
|
.collect::<Vec<_>>();
|
|
self.select(resolved_selections);
|
|
}
|
|
|
|
pub fn select_ranges<I, T>(&mut self, ranges: I)
|
|
where
|
|
I: IntoIterator<Item = Range<T>>,
|
|
T: ToOffset,
|
|
{
|
|
let ranges = ranges
|
|
.into_iter()
|
|
.map(|range| range.start.to_offset(self.snapshot)..range.end.to_offset(self.snapshot));
|
|
self.select_offset_ranges(ranges);
|
|
}
|
|
|
|
fn select_offset_ranges<I>(&mut self, ranges: I)
|
|
where
|
|
I: IntoIterator<Item = Range<MultiBufferOffset>>,
|
|
{
|
|
let selections = ranges
|
|
.into_iter()
|
|
.map(|range| {
|
|
let mut start = range.start;
|
|
let mut end = range.end;
|
|
let reversed = if start > end {
|
|
mem::swap(&mut start, &mut end);
|
|
true
|
|
} else {
|
|
false
|
|
};
|
|
Selection {
|
|
id: post_inc(&mut self.collection.next_selection_id),
|
|
start,
|
|
end,
|
|
reversed,
|
|
goal: SelectionGoal::None,
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
self.select(selections)
|
|
}
|
|
|
|
pub fn select_anchor_ranges<I>(&mut self, ranges: I)
|
|
where
|
|
I: IntoIterator<Item = Range<Anchor>>,
|
|
{
|
|
let selections = ranges
|
|
.into_iter()
|
|
.map(|range| {
|
|
let mut start = range.start;
|
|
let mut end = range.end;
|
|
let reversed = if start.cmp(&end, self.snapshot).is_gt() {
|
|
mem::swap(&mut start, &mut end);
|
|
true
|
|
} else {
|
|
false
|
|
};
|
|
Selection {
|
|
id: post_inc(&mut self.collection.next_selection_id),
|
|
start,
|
|
end,
|
|
reversed,
|
|
goal: SelectionGoal::None,
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
self.select_anchors(selections)
|
|
}
|
|
|
|
pub fn new_selection_id(&mut self) -> usize {
|
|
post_inc(&mut self.next_selection_id)
|
|
}
|
|
|
|
pub fn select_display_ranges<T>(&mut self, ranges: T)
|
|
where
|
|
T: IntoIterator<Item = Range<DisplayPoint>>,
|
|
{
|
|
let selections = ranges
|
|
.into_iter()
|
|
.map(|range| {
|
|
let mut start = range.start;
|
|
let mut end = range.end;
|
|
let reversed = if start > end {
|
|
mem::swap(&mut start, &mut end);
|
|
true
|
|
} else {
|
|
false
|
|
};
|
|
Selection {
|
|
id: post_inc(&mut self.collection.next_selection_id),
|
|
start: start.to_point(self.snapshot),
|
|
end: end.to_point(self.snapshot),
|
|
reversed,
|
|
goal: SelectionGoal::None,
|
|
}
|
|
})
|
|
.collect();
|
|
self.select(selections);
|
|
}
|
|
|
|
pub fn reverse_selections(&mut self) {
|
|
let mut new_selections: Vec<Selection<Point>> = Vec::new();
|
|
let disjoint = self.disjoint.clone();
|
|
for selection in disjoint
|
|
.iter()
|
|
.sorted_by(|first, second| Ord::cmp(&second.id, &first.id))
|
|
{
|
|
new_selections.push(Selection {
|
|
id: self.new_selection_id(),
|
|
start: selection
|
|
.start
|
|
.to_display_point(self.snapshot)
|
|
.to_point(self.snapshot),
|
|
end: selection
|
|
.end
|
|
.to_display_point(self.snapshot)
|
|
.to_point(self.snapshot),
|
|
reversed: selection.reversed,
|
|
goal: selection.goal,
|
|
});
|
|
}
|
|
self.select(new_selections);
|
|
}
|
|
|
|
pub fn move_with(
|
|
&mut self,
|
|
move_selection: &mut dyn FnMut(&DisplaySnapshot, &mut Selection<DisplayPoint>),
|
|
) {
|
|
let mut changed = false;
|
|
let display_map = self.display_snapshot();
|
|
let selections = self.collection.all_display(&display_map);
|
|
let selections = selections
|
|
.into_iter()
|
|
.map(|selection| {
|
|
let mut moved_selection = selection.clone();
|
|
move_selection(&display_map, &mut moved_selection);
|
|
if selection != moved_selection {
|
|
changed = true;
|
|
}
|
|
moved_selection.map(|display_point| display_point.to_point(&display_map))
|
|
})
|
|
.collect();
|
|
|
|
if changed {
|
|
self.select(selections)
|
|
}
|
|
}
|
|
|
|
pub fn move_offsets_with(
|
|
&mut self,
|
|
move_selection: &mut dyn FnMut(&MultiBufferSnapshot, &mut Selection<MultiBufferOffset>),
|
|
) {
|
|
let mut changed = false;
|
|
let display_map = self.display_snapshot();
|
|
let selections = self
|
|
.collection
|
|
.all::<MultiBufferOffset>(&display_map)
|
|
.into_iter()
|
|
.map(|selection| {
|
|
let mut moved_selection = selection.clone();
|
|
move_selection(self.snapshot, &mut moved_selection);
|
|
if selection != moved_selection {
|
|
changed = true;
|
|
}
|
|
moved_selection
|
|
})
|
|
.collect();
|
|
|
|
if changed {
|
|
self.select(selections)
|
|
}
|
|
}
|
|
|
|
pub fn move_heads_with(
|
|
&mut self,
|
|
update_head: &mut dyn FnMut(
|
|
&DisplaySnapshot,
|
|
DisplayPoint,
|
|
SelectionGoal,
|
|
) -> (DisplayPoint, SelectionGoal),
|
|
) {
|
|
self.move_with(&mut |map, selection| {
|
|
let (new_head, new_goal) = update_head(map, selection.head(), selection.goal);
|
|
selection.set_head(new_head, new_goal);
|
|
});
|
|
}
|
|
|
|
pub fn move_cursors_with(
|
|
&mut self,
|
|
update_cursor_position: &mut dyn FnMut(
|
|
&DisplaySnapshot,
|
|
DisplayPoint,
|
|
SelectionGoal,
|
|
) -> (DisplayPoint, SelectionGoal),
|
|
) {
|
|
self.move_with(&mut |map, selection| {
|
|
let (cursor, new_goal) = update_cursor_position(map, selection.head(), selection.goal);
|
|
selection.collapse_to(cursor, new_goal)
|
|
});
|
|
}
|
|
|
|
pub fn maybe_move_cursors_with(
|
|
&mut self,
|
|
update_cursor_position: &mut dyn FnMut(
|
|
&DisplaySnapshot,
|
|
DisplayPoint,
|
|
SelectionGoal,
|
|
) -> Option<(DisplayPoint, SelectionGoal)>,
|
|
) {
|
|
self.move_cursors_with(&mut |map, point, goal| {
|
|
update_cursor_position(map, point, goal).unwrap_or((point, goal))
|
|
})
|
|
}
|
|
|
|
pub fn replace_cursors_with(
|
|
&mut self,
|
|
find_replacement_cursors: impl FnOnce(&DisplaySnapshot) -> Vec<DisplayPoint>,
|
|
) {
|
|
let new_selections = find_replacement_cursors(self.snapshot)
|
|
.into_iter()
|
|
.map(|cursor| {
|
|
let cursor_point = cursor.to_point(self.snapshot);
|
|
Selection {
|
|
id: post_inc(&mut self.collection.next_selection_id),
|
|
start: cursor_point,
|
|
end: cursor_point,
|
|
reversed: false,
|
|
goal: SelectionGoal::None,
|
|
}
|
|
})
|
|
.collect();
|
|
self.select(new_selections);
|
|
}
|
|
|
|
pub fn pending_anchor_mut(&mut self) -> Option<&mut Selection<Anchor>> {
|
|
self.selections_changed = true;
|
|
self.pending.as_mut().map(|pending| &mut pending.selection)
|
|
}
|
|
}
|
|
|
|
impl Deref for MutableSelectionsCollection<'_, '_> {
|
|
type Target = SelectionsCollection;
|
|
fn deref(&self) -> &Self::Target {
|
|
self.collection
|
|
}
|
|
}
|
|
|
|
impl DerefMut for MutableSelectionsCollection<'_, '_> {
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
self.collection
|
|
}
|
|
}
|
|
|
|
fn selection_to_anchor_selection(
|
|
selection: Selection<MultiBufferOffset>,
|
|
buffer: &MultiBufferSnapshot,
|
|
) -> Selection<Anchor> {
|
|
let end_bias = if selection.start == selection.end {
|
|
Bias::Right
|
|
} else {
|
|
Bias::Left
|
|
};
|
|
Selection {
|
|
id: selection.id,
|
|
start: buffer.anchor_after(selection.start),
|
|
end: buffer.anchor_at(selection.end, end_bias),
|
|
reversed: selection.reversed,
|
|
goal: selection.goal,
|
|
}
|
|
}
|
|
|
|
fn resolve_selections_point<'a>(
|
|
selections: impl 'a + IntoIterator<Item = &'a Selection<Anchor>>,
|
|
map: &'a DisplaySnapshot,
|
|
) -> impl 'a + Iterator<Item = Selection<Point>> {
|
|
let (to_summarize, selections) = selections.into_iter().tee();
|
|
let mut summaries = map
|
|
.buffer_snapshot()
|
|
.summaries_for_anchors::<Point, _>(to_summarize.flat_map(|s| [&s.start, &s.end]))
|
|
.into_iter();
|
|
selections.map(move |s| {
|
|
let start = summaries.next().unwrap();
|
|
let end = summaries.next().unwrap();
|
|
assert!(
|
|
start <= end,
|
|
"anchors: start: {:?}, end: {:?}; resolved to: start: {:?}, end: {:?}",
|
|
s.start,
|
|
s.end,
|
|
start,
|
|
end
|
|
);
|
|
Selection {
|
|
id: s.id,
|
|
start,
|
|
end,
|
|
reversed: s.reversed,
|
|
goal: s.goal,
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Panics if passed selections are not in order
|
|
/// Resolves the anchors to display positions
|
|
fn resolve_selections_display<'a>(
|
|
selections: impl 'a + IntoIterator<Item = &'a Selection<Anchor>>,
|
|
map: &'a DisplaySnapshot,
|
|
) -> impl 'a + Iterator<Item = Selection<DisplayPoint>> {
|
|
let selections = resolve_selections_point(selections, map).map(move |s| {
|
|
let display_start = map.point_to_display_point(s.start, Bias::Left);
|
|
let display_end = map.point_to_display_point(
|
|
s.end,
|
|
if s.start == s.end {
|
|
Bias::Right
|
|
} else {
|
|
Bias::Left
|
|
},
|
|
);
|
|
assert!(
|
|
display_start <= display_end,
|
|
"display_start: {:?}, display_end: {:?}",
|
|
display_start,
|
|
display_end
|
|
);
|
|
Selection {
|
|
id: s.id,
|
|
start: display_start,
|
|
end: display_end,
|
|
reversed: s.reversed,
|
|
goal: s.goal,
|
|
}
|
|
});
|
|
coalesce_selections(selections)
|
|
}
|
|
|
|
/// Resolves the passed in anchors to [`MultiBufferDimension`]s `D`
|
|
/// wrapping around blocks inbetween.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if passed selections are not in order
|
|
pub(crate) fn resolve_selections_wrapping_blocks<'a, D, I>(
|
|
selections: I,
|
|
map: &'a DisplaySnapshot,
|
|
) -> impl 'a + Iterator<Item = Selection<D>>
|
|
where
|
|
D: MultiBufferDimension + Sub + AddAssign<<D as Sub>::Output> + Ord,
|
|
I: 'a + IntoIterator<Item = &'a Selection<Anchor>>,
|
|
{
|
|
// Without collapsed content, coalescing in buffer point space is equivalent to coalescing in
|
|
// display point space, so skip the per-selection display-coordinate round trip.
|
|
if !map.has_collapsed_content() {
|
|
Either::Left(resolve_selections_without_display_round_trip(
|
|
selections, map,
|
|
))
|
|
} else {
|
|
Either::Right(resolve_selections_via_display_round_trip(selections, map))
|
|
}
|
|
}
|
|
|
|
fn resolve_selections_without_display_round_trip<'a, D, I>(
|
|
selections: I,
|
|
map: &'a DisplaySnapshot,
|
|
) -> impl 'a + Iterator<Item = Selection<D>>
|
|
where
|
|
D: MultiBufferDimension + Sub + AddAssign<<D as Sub>::Output> + Ord,
|
|
I: 'a + IntoIterator<Item = &'a Selection<Anchor>>,
|
|
{
|
|
let (to_convert, selections) =
|
|
coalesce_selections(resolve_selections_point(selections, map)).tee();
|
|
let mut converted_endpoints = map
|
|
.buffer_snapshot()
|
|
.dimensions_from_points::<D>(to_convert.flat_map(|s| [s.start, s.end]));
|
|
selections.map(move |s| {
|
|
let start = converted_endpoints.next().unwrap();
|
|
let end = converted_endpoints.next().unwrap();
|
|
Selection {
|
|
id: s.id,
|
|
start,
|
|
end,
|
|
reversed: s.reversed,
|
|
goal: s.goal,
|
|
}
|
|
})
|
|
}
|
|
|
|
fn resolve_selections_via_display_round_trip<'a, D, I>(
|
|
selections: I,
|
|
map: &'a DisplaySnapshot,
|
|
) -> impl 'a + Iterator<Item = Selection<D>>
|
|
where
|
|
D: MultiBufferDimension + Sub + AddAssign<<D as Sub>::Output> + Ord,
|
|
I: 'a + IntoIterator<Item = &'a Selection<Anchor>>,
|
|
{
|
|
// Transforms `Anchor -> DisplayPoint -> Point -> DisplayPoint -> D`
|
|
// todo(lw): We should be able to short circuit the `Anchor -> DisplayPoint -> Point` to `Anchor -> Point`
|
|
let (to_convert, selections) = resolve_selections_display(selections, map).tee();
|
|
let mut converted_endpoints =
|
|
map.buffer_snapshot()
|
|
.dimensions_from_points::<D>(to_convert.flat_map(|s| {
|
|
let start = map.display_point_to_point(s.start, Bias::Left);
|
|
let end = map.display_point_to_point(s.end, Bias::Right);
|
|
assert!(start <= end, "start: {:?}, end: {:?}", start, end);
|
|
[start, end]
|
|
}));
|
|
selections.map(move |s| {
|
|
let start = converted_endpoints.next().unwrap();
|
|
let end = converted_endpoints.next().unwrap();
|
|
assert!(start <= end, "start: {:?}, end: {:?}", start, end);
|
|
Selection {
|
|
id: s.id,
|
|
start,
|
|
end,
|
|
reversed: s.reversed,
|
|
goal: s.goal,
|
|
}
|
|
})
|
|
}
|
|
|
|
fn coalesce_selections<D: Ord + fmt::Debug + Copy>(
|
|
selections: impl Iterator<Item = Selection<D>>,
|
|
) -> impl Iterator<Item = Selection<D>> {
|
|
let mut selections = selections.peekable();
|
|
iter::from_fn(move || {
|
|
let mut selection = selections.next()?;
|
|
while let Some(next_selection) = selections.peek() {
|
|
if should_merge(
|
|
selection.start,
|
|
selection.end,
|
|
next_selection.start,
|
|
next_selection.end,
|
|
true,
|
|
) {
|
|
if selection.reversed == next_selection.reversed {
|
|
selection.end = cmp::max(selection.end, next_selection.end);
|
|
selections.next();
|
|
} else {
|
|
selection.end = cmp::max(selection.start, next_selection.start);
|
|
break;
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
assert!(
|
|
selection.start <= selection.end,
|
|
"selection.start: {:?}, selection.end: {:?}, selection.reversed: {:?}",
|
|
selection.start,
|
|
selection.end,
|
|
selection.reversed
|
|
);
|
|
Some(selection)
|
|
})
|
|
}
|
|
|
|
/// Determines whether two selections should be merged into one.
|
|
///
|
|
/// Two selections should be merged when:
|
|
/// 1. They overlap: the selections share at least one position
|
|
/// 2. They have the same start position: one contains or equals the other
|
|
/// 3. A cursor touches a selection boundary: a zero-width selection (cursor) at the
|
|
/// start or end of another selection should be absorbed into it
|
|
///
|
|
/// Note: two selections that merely touch (one ends exactly where the other begins)
|
|
/// but don't share any positions remain separate, see: https://github.com/zed-industries/zed/issues/24748
|
|
fn should_merge<T: Ord + Copy>(a_start: T, a_end: T, b_start: T, b_end: T, sorted: bool) -> bool {
|
|
let is_overlapping = if sorted {
|
|
// When sorted, `a` starts before or at `b`, so overlap means `b` starts before `a` ends
|
|
b_start < a_end
|
|
} else {
|
|
a_start < b_end && b_start < a_end
|
|
};
|
|
|
|
// Selections starting at the same position should always merge (one contains the other)
|
|
let same_start = a_start == b_start;
|
|
|
|
// A cursor (zero-width selection) touching another selection's boundary should merge.
|
|
// This handles cases like a cursor at position X merging with a selection that
|
|
// starts or ends at X.
|
|
let is_cursor_a = a_start == a_end;
|
|
let is_cursor_b = b_start == b_end;
|
|
let cursor_at_boundary = (is_cursor_a && (a_start == b_start || a_end == b_end))
|
|
|| (is_cursor_b && (b_start == a_start || b_end == a_end));
|
|
|
|
is_overlapping || same_start || cursor_at_boundary
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::{
|
|
MultiBuffer,
|
|
display_map::{BlockPlacement, BlockProperties, BlockStyle, DisplayMap, FoldPlaceholder},
|
|
test::test_font,
|
|
};
|
|
use gpui::{AppContext as _, IntoElement as _, div, px};
|
|
use project::project_settings::DiagnosticSeverity;
|
|
use rand::{Rng as _, rngs::StdRng};
|
|
use settings::SettingsStore;
|
|
use std::sync::Arc;
|
|
|
|
#[gpui::test(iterations = 20)]
|
|
fn fast_and_slow_selection_resolution_match_without_collapsed_content(
|
|
cx: &mut gpui::TestAppContext,
|
|
mut rng: StdRng,
|
|
) {
|
|
cx.update(|cx| {
|
|
let settings = SettingsStore::test(cx);
|
|
cx.set_global(settings);
|
|
crate::init(cx);
|
|
});
|
|
|
|
let text = random_text(&mut rng);
|
|
let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
|
|
let display_map = cx.new(|cx| {
|
|
DisplayMap::new(
|
|
buffer.clone(),
|
|
test_font(),
|
|
px(14.),
|
|
Some(px(rng.random_range(80.0..=180.0))),
|
|
1,
|
|
1,
|
|
FoldPlaceholder::test(),
|
|
DiagnosticSeverity::Warning,
|
|
cx,
|
|
)
|
|
});
|
|
|
|
let snapshot = display_map.update(cx, |map, cx| {
|
|
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
|
map.insert_blocks(
|
|
[BlockProperties {
|
|
placement: BlockPlacement::Above(
|
|
buffer_snapshot.anchor_after(MultiBufferOffset(text.len() / 2)),
|
|
),
|
|
height: Some(2),
|
|
style: BlockStyle::Fixed,
|
|
render: Arc::new(|_| div().into_any_element()),
|
|
priority: 0,
|
|
}],
|
|
cx,
|
|
);
|
|
map.snapshot(cx)
|
|
});
|
|
assert!(!snapshot.has_collapsed_content());
|
|
|
|
let selections = random_anchor_selections(&snapshot, text.len(), &mut rng);
|
|
|
|
let fast_points =
|
|
resolve_selections_without_display_round_trip::<Point, _>(selections.iter(), &snapshot)
|
|
.collect::<Vec<_>>();
|
|
let slow_points =
|
|
resolve_selections_via_display_round_trip::<Point, _>(selections.iter(), &snapshot)
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(fast_points, slow_points);
|
|
|
|
let fast_offsets = resolve_selections_without_display_round_trip::<MultiBufferOffset, _>(
|
|
selections.iter(),
|
|
&snapshot,
|
|
)
|
|
.collect::<Vec<_>>();
|
|
let slow_offsets = resolve_selections_via_display_round_trip::<MultiBufferOffset, _>(
|
|
selections.iter(),
|
|
&snapshot,
|
|
)
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(fast_offsets, slow_offsets);
|
|
}
|
|
|
|
fn random_text(rng: &mut StdRng) -> String {
|
|
let mut text = String::new();
|
|
for line in 0..rng.random_range(8..32) {
|
|
if line > 0 {
|
|
text.push('\n');
|
|
}
|
|
for _ in 0..rng.random_range(8..48) {
|
|
let ch = match rng.random_range(0..12) {
|
|
0 => '\t',
|
|
1 => ' ',
|
|
_ => rng.random_range(b'a'..=b'z') as char,
|
|
};
|
|
text.push(ch);
|
|
}
|
|
}
|
|
text
|
|
}
|
|
|
|
fn random_anchor_selections(
|
|
snapshot: &DisplaySnapshot,
|
|
text_len: usize,
|
|
rng: &mut StdRng,
|
|
) -> Vec<Selection<Anchor>> {
|
|
let buffer = snapshot.buffer_snapshot();
|
|
let mut selections = Vec::new();
|
|
let mut offset = 0;
|
|
for id in 0..rng.random_range(1..32) {
|
|
if offset > text_len {
|
|
break;
|
|
}
|
|
|
|
let start = rng.random_range(offset..=text_len);
|
|
let end = rng.random_range(start..=text_len);
|
|
selections.push(Selection {
|
|
id,
|
|
start: MultiBufferOffset(start),
|
|
end: MultiBufferOffset(end),
|
|
reversed: rng.random(),
|
|
goal: SelectionGoal::None,
|
|
});
|
|
offset = end.saturating_add(rng.random_range(0..=4));
|
|
}
|
|
|
|
selections
|
|
.into_iter()
|
|
.map(|selection| selection_to_anchor_selection(selection, buffer))
|
|
.collect()
|
|
}
|
|
}
|