editor: Fix columnar selection coordinates (#63723)

Closes #63712

## Summary

- Use tab-expanded grapheme columns for Add Selection Above/Below when
`skip_soft_wrap` is enabled, rather than UTF-8 byte offsets or Unicode
scalar counts.
- Keep mouse rectangles pixel-based and preserve logical selection goals
across short rows, direction changes, and selection/text undo and redo.
- Reconcile columnar groups when selections change, while preserving
committed groups during cancellable mouse previews.
- Batch source and destination queries by row and skip fully replaced
input rows without a persistent row cache.
- Handle folds, inlays, and replacement blocks with small map-query
additions, and fix the tab bitmap boundary at a tab size of 128.

## Validation

- Full editor library suite: 1,109 passed, one ignored.
- Editor Clippy and scoped formatting passed.
- Regression checks failed without the corresponding lifecycle and
batching fixes, then passed with them restored.
- The three related sync/history tests were rerun and passed after the
final callback-variable renames.

Release Notes:

- Fixed columnar selections landing in the wrong column around non-ASCII
text, tabs, soft wraps, and collapsed content.

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-09-16 16:40:28 +00:00
1 parent 9a606bc84c
commit 9f1587c697
12 files changed
+6276 -337

No files matched your search

File diff suppressed because it is too large. Load diff
+28 -30
View File
@@ -127,7 +127,7 @@ use std::{
fmt::Debug,
iter,
num::NonZeroU32,
ops::{self, Add, Range, Sub},
ops::{self, Add, Range, RangeInclusive, Sub},
sync::Arc,
};
@@ -137,7 +137,8 @@ use crate::{
use block_map::{BlockPointCursor, BlockRow, BlockSnapshot};
use fold_map::{FoldPointCursor, FoldSnapshot};
use inlay_map::{BufferOffsetToInlayPointCursor, InlaySnapshot};
use tab_map::{TabPoint, TabPointCursor, TabSnapshot};
pub(crate) use tab_map::TabPoint;
use tab_map::{TabPointCursor, TabSnapshot};
use wrap_map::{WrapMap, WrapPatch, WrapPointCursor};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -1549,34 +1550,6 @@ impl DisplaySnapshot {
&self.block_snapshot.wrap_snapshot.tab_snapshot
}
/// The column `point` sits at once tabs are expanded, which is where it appears
/// on screen when the line isn't soft-wrapped. Unlike a display column this is
/// counted from the start of the buffer row rather than the wrapped segment.
pub(crate) fn tab_expanded_column(&self, point: Point) -> u32 {
self.tab_snapshot()
.point_to_tab_point(point, Bias::Left)
.0
.column
}
/// Inverse of [`Self::tab_expanded_column`], clamped to the end of the row.
pub(crate) fn point_for_tab_expanded_column(
&self,
buffer_row: MultiBufferRow,
column: u32,
) -> Point {
let tab_snapshot = self.tab_snapshot();
let tab_row = tab_snapshot.buffer_row_to_tab_row(buffer_row);
let column = column.min(tab_snapshot.line_len(tab_row));
tab_snapshot.tab_point_to_point(TabPoint(Point::new(tab_row, column)), Bias::Left)
}
/// The length of `buffer_row` once tabs are expanded.
pub(crate) fn tab_expanded_line_len(&self, buffer_row: MultiBufferRow) -> u32 {
let tab_snapshot = self.tab_snapshot();
tab_snapshot.line_len(tab_snapshot.buffer_row_to_tab_row(buffer_row))
}
pub fn fold_snapshot(&self) -> &FoldSnapshot {
&self.block_snapshot.wrap_snapshot.tab_snapshot.fold_snapshot
}
@@ -2500,6 +2473,31 @@ impl DisplaySnapshot {
Bias::Right,
)
}
pub(crate) fn fully_replaced_tab_rows(&self, row: u32) -> Option<RangeInclusive<u32>> {
if !self.block_snapshot.has_replacement_blocks()
|| row > self.tab_snapshot().max_point().row()
{
return None;
}
let wraps = self.wrap_snapshot();
let wrap_point = wraps.tab_point_to_wrap_point(TabPoint::new(row, 0));
let input_range = self
.block_snapshot
.replacement_block_input_range(wrap_point.row())?;
let start = wraps.to_tab_point(WrapPoint::new(input_range.start, 0));
let start_row = start.row().checked_add(u32::from(start.column() > 0))?;
let end_row = if input_range.end > wraps.max_point().row() {
self.tab_snapshot().max_point().row()
} else {
wraps
.to_tab_point(WrapPoint::new(input_range.end, 0))
.row()
.checked_sub(1)?
};
let rows = start_row..=end_row;
rows.contains(&row).then_some(rows)
}
}
fn diagnostic_style(severity: lsp::DiagnosticSeverity, colors: &StatusColors) -> Hsla {
+93 -2
View File
@@ -2628,6 +2628,11 @@ impl BlockSnapshot {
self.wrap_snapshot.max_point()
}
}
pub(super) fn replacement_block_input_range(&self, row: WrapRow) -> Option<Range<WrapRow>> {
let (start, end, item) = self.transforms.find::<WrapRow, _>((), &row, Bias::Right);
item?.block.as_ref()?.is_replacement().then_some(start..end)
}
}
impl BlockChunks<'_> {
@@ -2951,8 +2956,8 @@ mod tests {
use super::*;
use crate::{
display_map::{
Companion, fold_map::FoldMap, fold_map::FoldPlaceholder, inlay_map::InlayMap,
tab_map::TabMap, wrap_map::WrapMap,
Companion, DisplayMap, fold_map::FoldMap, fold_map::FoldPlaceholder,
inlay_map::InlayMap, tab_map::TabMap, wrap_map::WrapMap,
},
test::test_font,
};
@@ -2961,6 +2966,7 @@ mod tests {
use itertools::Itertools;
use language::{Buffer, Capability, Point};
use multi_buffer::{MultiBuffer, PathKey};
use project::project_settings::DiagnosticSeverity;
use rand::prelude::*;
use settings::SettingsStore;
use std::env;
@@ -5618,6 +5624,91 @@ mod tests {
assert_eq!(snapshot.snapshot.text(), "aaa\nbbb\nccc\nddd\neee\n");
}
#[gpui::test]
fn test_replacement_input_ranges_preserve_partial_tab_rows(cx: &mut gpui::TestAppContext) {
cx.update(|cx| {
init_test(cx);
crate::init(cx);
});
let buffer = cx.update(|cx| {
MultiBuffer::build_simple("one two three\nfour five six\nseven eight", cx)
});
let map = cx.new(|cx| {
DisplayMap::new(
buffer,
font("Helvetica"),
px(14.0),
Some(px(90.)),
1,
1,
FoldPlaceholder::test(),
DiagnosticSeverity::Warning,
cx,
)
});
let mut snapshot = map.update(cx, |map, cx| map.snapshot(cx));
let wraps = snapshot.wrap_snapshot().clone();
assert_eq!(
wraps.text(),
"one two \nthree\nfour five \nsix\nseven \neight"
);
for height in [0, 1, 4] {
let block = Block::Custom(Arc::new(CustomBlock {
id: CustomBlockId(0),
placement: BlockPlacement::Replace(Anchor::Min..=Anchor::Max),
height: Some(height),
style: BlockStyle::Fixed,
render: Arc::new(Mutex::new(Arc::new(|_| div().into_any()))),
priority: 0,
}));
for (hidden, expected) in [
(0..6, [Some(0..=2), Some(0..=2), Some(0..=2)]),
(1..5, [None, Some(1..=1), None]),
(0..5, [Some(0..=1), Some(0..=1), None]),
(1..6, [None, Some(1..=2), Some(1..=2)]),
(2..4, [None, Some(1..=1), None]),
(0..2, [Some(0..=0), None, None]),
(4..6, [None, None, Some(2..=2)]),
(0..1, [None, None, None]),
(5..6, [None, None, None]),
] {
let mut transforms = SumTree::new(());
push_isomorphic(&mut transforms, RowDelta(hidden.start), &wraps);
transforms.push(
Transform {
summary: TransformSummary {
input_rows: WrapRow(hidden.end - hidden.start),
output_rows: BlockRow(height),
..TransformSummary::default()
},
block: Some(block.clone()),
},
(),
);
push_isomorphic(&mut transforms, RowDelta(6 - hidden.end), &wraps);
snapshot.block_snapshot.transforms = transforms;
assert_eq!(
(0..3)
.map(|row| snapshot.fully_replaced_tab_rows(row))
.collect::<Vec<_>>(),
expected,
"hidden: {hidden:?}, height: {height}"
);
for row in 0..=6 {
assert_eq!(
snapshot
.block_snapshot
.replacement_block_input_range(WrapRow(row)),
hidden
.contains(&row)
.then_some(WrapRow(hidden.start)..WrapRow(hidden.end)),
"row: {row}, hidden: {hidden:?}, height: {height}"
);
}
}
}
}
fn init_test(cx: &mut gpui::App) {
let settings = SettingsStore::test(cx);
cx.set_global(settings);
@@ -926,6 +926,14 @@ impl FoldSnapshot {
}
}
pub(crate) fn placeholder_range_at(&self, point: FoldPoint) -> Option<Range<FoldPoint>> {
let (start, end, item) = self
.transforms
.find::<FoldPoint, _>((), &point, Bias::Right);
item.filter(|transform| transform.placeholder.is_some())
.map(|_| start..end)
}
#[ztracing::instrument(skip_all)]
pub(crate) fn chunks<'a>(
&'a self,
+94 -3
View File
@@ -321,7 +321,7 @@ impl TabSnapshot {
chunk: Chunk {
text: unsafe { std::str::from_utf8_unchecked(&SPACES[..to_next_stop as usize]) },
is_tab: true,
chars: 1u128.unbounded_shl(to_next_stop) - 1,
chars: u128::MAX.unbounded_shr(u128::BITS - to_next_stop),
..Default::default()
},
inside_leading_tab: to_next_stop > 0,
@@ -643,7 +643,7 @@ impl TabChunks<'_> {
self.chunk = Chunk {
text: unsafe { std::str::from_utf8_unchecked(&SPACES[..to_next_stop as usize]) },
is_tab: true,
chars: 1u128.unbounded_shl(to_next_stop) - 1,
chars: u128::MAX.unbounded_shr(u128::BITS - to_next_stop),
..Default::default()
};
self.inside_leading_tab = to_next_stop > 0;
@@ -692,7 +692,7 @@ impl<'a> Iterator for TabChunks<'a> {
return Some(Chunk {
text: unsafe { std::str::from_utf8_unchecked(&SPACES[..len as usize]) },
is_tab: true,
chars: 1u128.unbounded_shl(len) - 1,
chars: u128::MAX.unbounded_shr(u128::BITS - len),
tabs: 0,
newlines: 0,
..self.chunk.clone()
@@ -1299,6 +1299,75 @@ mod tests {
}
}
#[gpui::test]
fn test_max_tab_size_full_chunks(cx: &mut gpui::App) {
for tab_size in [127, 128, 129] {
let buffer = MultiBuffer::build_simple("\tα\t🏀", cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, tab_snapshot) = TabMap::new(fold_snapshot, NonZeroU32::new(tab_size).unwrap());
let width = tab_size.min(128);
let expected = format!(
"{}α{}🏀",
" ".repeat(width as usize),
" ".repeat(width as usize - 1)
);
let mut chunks = tab_snapshot.chunks(
TabPoint::zero()..tab_snapshot.max_point(),
LanguageAwareStyling {
tree_sitter: false,
diagnostics: false,
},
Highlights::default(),
);
assert_max_tab_chunk_bitmaps(&mut chunks, &expected);
assert_eq!(tab_snapshot.text(), expected);
assert_eq!(
tab_snapshot.point_to_tab_point(Point::new(0, 4), Bias::Left),
TabPoint::new(0, 2 * width + 1),
);
assert_eq!(
tab_snapshot.tab_point_to_point(TabPoint::new(0, 2 * width + 1), Bias::Left),
Point::new(0, 4),
);
}
}
#[gpui::test]
fn test_max_tab_size_partial_chunks_and_seek(cx: &mut gpui::App) {
let buffer = MultiBuffer::build_simple("\t🏀", cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, tab_snapshot) = TabMap::new(fold_snapshot, NonZeroU32::new(128).unwrap());
let expected = format!("{}🏀", " ".repeat(128));
let mut reused = tab_snapshot.chunks(
TabPoint::zero()..tab_snapshot.max_point(),
LanguageAwareStyling {
tree_sitter: false,
diagnostics: false,
},
Highlights::default(),
);
for start in 0..=128 {
for end in [start, (start + 1).min(128), 128, 132] {
let range = TabPoint::new(0, start)..TabPoint::new(0, end);
let mut chunks = tab_snapshot.chunks(
range.clone(),
LanguageAwareStyling {
tree_sitter: false,
diagnostics: false,
},
Highlights::default(),
);
assert_max_tab_chunk_bitmaps(&mut chunks, &expected[start as usize..end as usize]);
reused.seek(range);
assert_max_tab_chunk_bitmaps(&mut reused, &expected[start as usize..end as usize]);
}
}
}
#[gpui::test]
fn test_empty_chunk_after_leading_tab_trim(cx: &mut gpui::App) {
// We fold "hello" (offsets 1..6) so the fold map creates a
@@ -1755,4 +1824,26 @@ mod tests {
}
}
}
fn assert_max_tab_chunk_bitmaps(chunks: &mut TabChunks<'_>, expected_text: &str) {
let initial_mask = chunks
.chunk
.text
.char_indices()
.fold(0u128, |mask, (index, _)| mask | (1u128 << index));
assert_eq!(chunks.chunk.chars, initial_mask);
let text = chunks
.map(|chunk| {
let expected_mask = chunk
.text
.char_indices()
.fold(0u128, |mask, (index, _)| mask | (1u128 << index));
assert_eq!(chunk.chars, expected_mask, "chunk {:?}", chunk.text);
assert_eq!(chunk.tabs, 0);
assert_eq!(chunk.newlines, 0);
chunk.text
})
.collect::<String>();
assert_eq!(text, expected_text);
}
}
+5 -2
View File
@@ -485,8 +485,11 @@ impl Editor {
self.buffer.read(cx).last_transaction_id(cx)
{
if transaction_id_prev != Some(transaction_id_now) {
self.selection_history
.insert_transaction(transaction_id_now, selections);
self.selection_history.insert_transaction(
transaction_id_now,
selections,
self.add_selections_state.clone(),
);
}
}
+76 -18
View File
@@ -63,6 +63,7 @@ pub mod test;
mod clipboard;
mod code_actions;
mod columnar_selection;
mod completions;
mod config;
mod cursor_animation;
@@ -1436,6 +1437,8 @@ struct DeferredSelectionEffectsState {
pub struct TransactionSelections {
pub undo: Arc<[Selection<Anchor>]>,
pub redo: Option<Arc<[Selection<Anchor>]>>,
undo_add_selections_state: Option<AddSelectionsState>,
redo_add_selections_state: Option<AddSelectionsState>,
}
#[derive(Default)]
@@ -1452,6 +1455,7 @@ impl SelectionHistory {
&mut self,
transaction_id: TransactionId,
selections: Arc<[Selection<Anchor>]>,
add_selections_state: Option<AddSelectionsState>,
) {
if selections.is_empty() {
log::error!(
@@ -1465,6 +1469,8 @@ impl SelectionHistory {
TransactionSelections {
undo: selections,
redo: None,
undo_add_selections_state: add_selections_state,
redo_add_selections_state: None,
},
);
}
@@ -1547,12 +1553,14 @@ struct RowHighlight {
#[derive(Clone, Debug)]
struct AddSelectionsState {
groups: Vec<AddSelectionsGroup>,
skip_soft_wrap: bool,
}
#[derive(Clone, Debug)]
struct AddSelectionsGroup {
above: bool,
stack: Vec<usize>,
goal_source: Option<Range<Anchor>>,
}
#[derive(Clone)]
@@ -3530,9 +3538,14 @@ impl Editor {
return;
}
let cancelling_group = self.selections.pending_anchor().is_none()
&& self.selections.disjoint_anchors().len() > 1;
if self.mode.is_full()
&& self.change_selections(Default::default(), window, cx, |s| s.try_cancel())
{
if cancelling_group {
self.add_selections_state = None;
}
cx.notify();
return;
}
@@ -7913,13 +7926,23 @@ impl Editor {
fn restore_selections(
&mut self,
selections: Option<Arc<[Selection<Anchor>]>>,
selections: Option<(Arc<[Selection<Anchor>]>, Option<AddSelectionsState>)>,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(selections) = selections.filter(|selections| !selections.is_empty()) {
self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_anchors(selections.to_vec());
if let Some((selections, add_selections_state)) =
selections.filter(|(selections, _)| !selections.is_empty())
{
self.with_selection_effects_deferred(window, cx, |editor, window, cx| {
editor.change_selections(
SelectionEffects::no_scroll(),
window,
cx,
|selection_collection| {
selection_collection.select_anchors_unexpanded(selections.to_vec());
},
);
editor.add_selections_state = add_selections_state;
});
}
}
@@ -7934,7 +7957,12 @@ impl Editor {
if transaction.is_none() {
log::error!("No selection history for undone transaction; selection unchanged");
}
let selections = transaction.map(|transaction| transaction.undo.clone());
let selections = transaction.map(|transaction| {
(
transaction.undo.clone(),
transaction.undo_add_selections_state.clone(),
)
});
self.restore_selections(selections, window, cx);
self.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(window, cx);
@@ -7956,10 +7984,14 @@ impl Editor {
}
if let Some(transaction_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) {
let selections = self
.selection_history
.transaction(transaction_id)
.and_then(|transaction| transaction.redo.clone());
let selections =
self.selection_history
.transaction(transaction_id)
.and_then(|transaction| {
transaction.redo.clone().map(|selections| {
(selections, transaction.redo_add_selections_state.clone())
})
});
self.restore_selections(selections, window, cx);
self.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(window, cx);
@@ -8486,7 +8518,7 @@ impl Editor {
buffers.retain(|buffer| !buffer.read(cx).read_only());
let transaction_id_prev = buffer.read(cx).last_transaction_id(cx);
let selections_prev = transaction_id_prev
let (selections_prev, add_selections_state_prev) = transaction_id_prev
.and_then(|transaction_id_prev| {
// default to selections as they were after the last edit, if we have them,
// instead of how they are now.
@@ -8494,9 +8526,19 @@ impl Editor {
// will take you back to where you made the last edit, instead of staying where you scrolled
self.selection_history
.transaction(transaction_id_prev)
.map(|t| t.undo.clone())
.map(|transaction| {
(
transaction.undo.clone(),
transaction.undo_add_selections_state.clone(),
)
})
})
.unwrap_or_else(|| self.selections.disjoint_anchors_arc());
.unwrap_or_else(|| {
(
self.selections.disjoint_anchors_arc(),
self.add_selections_state.clone(),
)
});
let mut timeout = cx.background_executor().timer(FORMAT_TIMEOUT).fuse();
let format = project.update(cx, |project, cx| {
@@ -8528,9 +8570,11 @@ impl Editor {
if has_new_transaction {
editor
.update(cx, |editor, _| {
editor
.selection_history
.insert_transaction(transaction_id_now, selections_prev);
editor.selection_history.insert_transaction(
transaction_id_now,
selections_prev,
add_selections_state_prev,
);
})
.ok();
}
@@ -8700,8 +8744,11 @@ impl Editor {
.buffer
.update(cx, |buffer, cx| buffer.start_transaction_at(now, cx))
{
self.selection_history
.insert_transaction(tx_id, self.selections.disjoint_anchors_arc());
self.selection_history.insert_transaction(
tx_id,
self.selections.disjoint_anchors_arc(),
self.add_selections_state.clone(),
);
cx.emit(EditorEvent::TransactionBegun {
transaction_id: tx_id,
});
@@ -8722,6 +8769,7 @@ impl Editor {
{
if let Some(transaction) = self.selection_history.transaction_mut(transaction_id) {
transaction.redo = Some(self.selections.disjoint_anchors_arc());
transaction.redo_add_selections_state = self.add_selections_state.clone();
} else {
log::error!("unexpectedly ended a transaction that wasn't started by this editor");
}
@@ -8740,7 +8788,17 @@ impl Editor {
) -> bool {
self.selection_history
.transaction_mut(transaction_id)
.map(modify)
.map(|transaction| {
let undo = transaction.undo.clone();
let redo = transaction.redo.clone();
modify(transaction);
if undo != transaction.undo {
transaction.undo_add_selections_state = None;
}
if redo != transaction.redo {
transaction.redo_add_selections_state = None;
}
})
.is_some()
}
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -14,7 +14,7 @@ use project::DisableAiSettings;
use settings::Settings;
use sum_tree::Bias;
use text::SelectionGoal;
use util::{RangeExt, debug_panic, post_inc};
use util::{RangeExt, debug_panic};
use super::{EditorElement, EditorLayout, LineNumberLayout, PositionMap, SplitSide};
use crate::{
@@ -1090,7 +1090,7 @@ impl EditorElement {
);
if mouse_down_time.elapsed() >= drag_and_drop_delay {
let drop_cursor = Selection {
id: post_inc(&mut editor.selections.next_selection_id()),
id: editor.selections.next_selection_id(),
start: drop_anchor,
end: drop_anchor,
reversed: false,
+5 -1
View File
@@ -640,11 +640,15 @@ impl Editor {
});
let snapshot = self.display_snapshot(cx);
self.selections.change_with(&snapshot, |selections| {
let previous = self.selections.disjoint_anchors_arc();
let (changed, ()) = self.selections.change_with(&snapshot, |selections| {
for buffer_id in ids_to_fold.iter().copied() {
selections.remove_selections_from_buffer(buffer_id);
}
});
if changed {
self.invalidate_add_selection_goals_after_change(Some(&previous));
}
cx.emit(EditorEvent::BufferFoldToggled {
ids: ids_to_fold,
+452 -169
View File
@@ -1,4 +1,5 @@
use super::*;
use crate::columnar_selection::ColumnarSelectionRows;
impl Editor {
pub fn sync_selections(
@@ -6,45 +7,30 @@ impl Editor {
other: Entity<Editor>,
cx: &mut Context<Self>,
) -> gpui::Subscription {
let other_selections = other.read(cx).selections.disjoint_anchors().to_vec();
if !other_selections.is_empty() {
self.selections
.change_with(&self.display_snapshot(cx), |selections| {
selections.select_anchors(other_selections);
});
}
self.sync_selections_from(other.read(cx).selections.disjoint_anchors().to_vec(), cx);
let other_subscription = cx.subscribe(&other, |this, other, other_evt, cx| {
if let EditorEvent::SelectionsChanged { local: true } = other_evt {
let other_selections = other.read(cx).selections.disjoint_anchors().to_vec();
if other_selections.is_empty() {
return;
}
let snapshot = this.display_snapshot(cx);
this.selections.change_with(&snapshot, |selections| {
selections.select_anchors(other_selections);
});
let other_subscription = cx.subscribe(&other, |editor, other, event, cx| {
if let EditorEvent::SelectionsChanged { local: true } = event {
editor.sync_selections_from(
other.read(cx).selections.disjoint_anchors().to_vec(),
cx,
);
}
});
let this_subscription = cx.subscribe_self::<EditorEvent>(move |this, this_evt, cx| {
if let EditorEvent::SelectionsChanged { local: true } = this_evt {
let these_selections = this.selections.disjoint_anchors().to_vec();
if these_selections.is_empty() {
let editor_subscription = cx.subscribe_self::<EditorEvent>(move |editor, event, cx| {
if let EditorEvent::SelectionsChanged { local: true } = event {
if editor.selections.disjoint_anchors().is_empty() {
return;
}
let selections = editor.selections.disjoint_anchors().to_vec();
other.update(cx, |other_editor, cx| {
let snapshot = other_editor.display_snapshot(cx);
other_editor
.selections
.change_with(&snapshot, |selections| {
selections.select_anchors(these_selections);
})
other_editor.sync_selections_from(selections, cx);
});
}
});
Subscription::join(other_subscription, this_subscription)
Subscription::join(other_subscription, editor_subscription)
}
/// Changes selections using the provided mutation function. Changes to `self.selections` occur
@@ -57,20 +43,7 @@ impl Editor {
cx: &mut Context<Self>,
change: impl FnOnce(&mut MutableSelectionsCollection<'_, '_>) -> R,
) -> R {
let snapshot = self.display_snapshot(cx);
if let Some(state) = &mut self.deferred_selection_effects_state {
state.effects.scroll = effects.scroll.or(state.effects.scroll);
state.effects.completions = effects.completions;
state.effects.nav_history = effects.nav_history.or(state.effects.nav_history);
let (changed, result) = self.selections.change_with(&snapshot, change);
state.changed |= changed;
return result;
}
let mut state = self.prepare_selection_effects(effects);
let (changed, result) = self.selections.change_with(&snapshot, change);
state.changed = state.changed || changed;
self.apply_selection_effects(state, window, cx);
result
self.change_selections_with_history(effects, None, window, cx, change)
}
/// Re-resolves selection anchors so they reference live buffer fragments.
@@ -86,10 +59,9 @@ impl Editor {
pub fn refresh_selection_anchors(&mut self, cx: &mut Context<Self>) {
let snapshot = self.display_snapshot(cx);
let selections = self.selections.all::<MultiBufferOffset>(&snapshot);
self.selections
.change_with(&snapshot, |selection_collection| {
selection_collection.select(selections);
});
self.change_selections_without_effects(&snapshot, |selection_collection| {
selection_collection.select(selections);
});
}
/// Defers the effects of selection change, so that the effects of multiple calls to
@@ -163,15 +135,14 @@ impl Editor {
cx: &mut Context<Self>,
) {
let old_cursor_position = self.selections.newest_anchor().head();
self.selections
.change_with(&self.display_snapshot(cx), |s| {
s.select_anchors(selections);
if let Some(pending_selection) = pending_selection {
s.set_pending(pending_selection, SelectMode::Character);
} else {
s.clear_pending();
}
});
self.change_selections_without_effects(&self.display_snapshot(cx), |s| {
s.select_anchors(selections);
if let Some(pending_selection) = pending_selection {
s.set_pending(pending_selection, SelectMode::Character);
} else {
s.clear_pending();
}
});
self.selections_did_change(
false,
&old_cursor_position,
@@ -414,11 +385,11 @@ impl Editor {
if !select_prev_state.done {
let first_selection = selections
.iter()
.min_by_key(|s| s.id)
.min_by_key(|selection| selection.id)
.context("missing selection for select previous action")?;
let last_selection = selections
.iter()
.max_by_key(|s| s.id)
.max_by_key(|selection| selection.id)
.context("missing selection for select previous action")?;
let mut next_selected_range = None;
// When we're iterating matches backwards, the oldest match will actually be the furthest one in the buffer.
@@ -1099,7 +1070,7 @@ impl Editor {
SelectionEffects::scroll(Autoscroll::newest()),
window,
cx,
|s| s.select_anchors(entry.selections.to_vec()),
|s| s.select_anchors_unexpanded(entry.selections.to_vec()),
);
});
self.selection_history.mode = SelectionHistoryMode::Normal;
@@ -1124,7 +1095,7 @@ impl Editor {
SelectionEffects::scroll(Autoscroll::newest()),
window,
cx,
|s| s.select_anchors(entry.selections.to_vec()),
|s| s.select_anchors_unexpanded(entry.selections.to_vec()),
);
});
self.selection_history.mode = SelectionHistoryMode::Normal;
@@ -1606,9 +1577,6 @@ impl Editor {
.display_map
.update(cx, |display_map, cx| display_map.snapshot(cx));
let buffer = display_map.buffer_snapshot();
if self.selections.count() == 1 {
self.add_selections_state = None;
}
self.select_next_state = None;
self.select_prev_state = None;
self.select_syntax_node_history.try_clear();
@@ -1761,17 +1729,13 @@ impl Editor {
fn prepare_selection_effects(
&self,
effects: SelectionEffects,
history_entry: Option<SelectionHistoryEntry>,
) -> DeferredSelectionEffectsState {
DeferredSelectionEffectsState {
changed: false,
effects,
old_cursor_position: self.selections.newest_anchor().head(),
history_entry: SelectionHistoryEntry {
selections: self.selections.disjoint_anchors_arc(),
select_next_state: self.select_next_state.clone(),
select_prev_state: self.select_prev_state.clone(),
add_selections_state: self.add_selections_state.clone(),
},
history_entry: history_entry.unwrap_or_else(|| self.selection_history_entry()),
}
}
@@ -1952,6 +1916,180 @@ impl Editor {
))
}
pub(super) fn invalidate_add_selection_goals_after_change(
&mut self,
previous: Option<&[Selection<Anchor>]>,
) {
if self.add_selections_state.is_none() {
return;
}
let Some(previous) = previous else {
self.add_selections_state = None;
return;
};
let current = self
.selections
.disjoint_anchors()
.iter()
.map(|selection| (selection.id, selection))
.collect::<HashMap<_, _>>();
let unchanged = previous
.iter()
.filter(|selection| current.get(&selection.id) == Some(selection))
.map(|selection| selection.id)
.collect::<HashSet<_>>();
let removed = previous
.iter()
.filter(|selection| !current.contains_key(&selection.id))
.map(|selection| selection.id)
.collect::<HashSet<_>>();
if let Some(state) = &mut self.add_selections_state {
Self::retain_add_selection_goals(state, &unchanged, &removed, &current);
if state.groups.is_empty() {
self.add_selections_state = None;
}
}
}
fn sync_selections_from(&mut self, selections: Vec<Selection<Anchor>>, cx: &mut Context<Self>) {
if !selections.is_empty() {
self.change_selections_without_effects(&self.display_snapshot(cx), |collection| {
collection.select_anchors(selections);
});
}
}
fn retain_add_selection_goals<T>(
state: &mut AddSelectionsState,
unchanged: &HashSet<usize>,
removed: &HashSet<usize>,
current: &HashMap<usize, &Selection<T>>,
) {
let skip_soft_wrap = state.skip_soft_wrap;
state.groups.retain_mut(|group| {
if group.stack.first().is_none_or(|id| !unchanged.contains(id))
|| group
.stack
.iter()
.any(|id| !unchanged.contains(id) && !removed.contains(id))
{
group.goal_source = None;
}
group.stack.retain(|id| current.contains_key(id));
!group.stack.is_empty() && (skip_soft_wrap || group.stack.len() > 1)
});
}
fn reconcile_add_selection_groups(
state: &mut AddSelectionsState,
previous: &[Selection<Point>],
current: &[Selection<Point>],
) {
let current_by_id = current
.iter()
.map(|selection| (selection.id, selection))
.collect::<HashMap<_, _>>();
let mut unchanged = HashSet::default();
let mut removed = HashSet::default();
for selection in previous {
if let Some(current) = current_by_id.get(&selection.id) {
if **current == *selection {
unchanged.insert(selection.id);
}
} else {
let index = current.partition_point(|current| current.end < selection.start);
let overlaps = current
.iter()
.skip(index)
.take_while(|current| current.start <= selection.end)
.any(|current| {
if current.is_empty() || selection.is_empty() {
current.start <= selection.end && selection.start <= current.end
} else {
current.start < selection.end && selection.start < current.end
}
});
if !overlaps {
removed.insert(selection.id);
}
}
}
Self::retain_add_selection_goals(state, &unchanged, &removed, &current_by_id);
}
fn resolve_add_selection_anchors(
selections: &[Selection<Anchor>],
snapshot: &DisplaySnapshot,
) -> Vec<Selection<Point>> {
selections
.iter()
.filter(|selection| {
snapshot.can_resolve(&selection.start) && snapshot.can_resolve(&selection.end)
})
.map(|selection| selection.map(|anchor| anchor.to_point(snapshot.buffer_snapshot())))
.collect()
}
fn change_selections_without_effects<R>(
&mut self,
snapshot: &DisplaySnapshot,
change: impl FnOnce(&mut MutableSelectionsCollection<'_, '_>) -> R,
) -> (bool, R) {
let previous = self
.add_selections_state
.as_ref()
.map(|_| self.selections.disjoint_anchors_arc());
let (changed, result) = self.selections.change_with(snapshot, change);
if changed
&& let Some(previous) = previous
&& let Some(state) = &mut self.add_selections_state
{
let previous = Self::resolve_add_selection_anchors(&previous, snapshot);
let current =
Self::resolve_add_selection_anchors(self.selections.disjoint_anchors(), snapshot);
Self::reconcile_add_selection_groups(state, &previous, &current);
if state.groups.is_empty() {
self.add_selections_state = None;
}
}
(changed, result)
}
fn selection_history_entry(&self) -> SelectionHistoryEntry {
SelectionHistoryEntry {
selections: self.selections.disjoint_anchors_arc(),
select_next_state: self.select_next_state.clone(),
select_prev_state: self.select_prev_state.clone(),
add_selections_state: self.add_selections_state.clone(),
}
}
fn change_selections_with_history<R>(
&mut self,
effects: SelectionEffects,
history_entry: Option<SelectionHistoryEntry>,
window: &mut Window,
cx: &mut Context<Self>,
change: impl FnOnce(&mut MutableSelectionsCollection<'_, '_>) -> R,
) -> R {
let snapshot = self.display_snapshot(cx);
if self.deferred_selection_effects_state.is_some() {
let (changed, result) = self.change_selections_without_effects(&snapshot, change);
if let Some(state) = &mut self.deferred_selection_effects_state {
state.effects.scroll = effects.scroll.or(state.effects.scroll);
state.effects.completions = effects.completions;
state.effects.nav_history = effects.nav_history.or(state.effects.nav_history);
state.changed |= changed;
}
return result;
}
let mut state = self.prepare_selection_effects(effects, history_entry);
let (changed, result) = self.change_selections_without_effects(&snapshot, change);
state.changed = state.changed || changed;
self.apply_selection_effects(state, window, cx);
result
}
fn add_selection(
&mut self,
above: bool,
@@ -1960,54 +2098,208 @@ impl Editor {
cx: &mut Context<Self>,
) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let all_selections = self.selections.all::<Point>(&display_map);
let text_layout_details = self.text_layout_details(window, cx);
let (mut columnar_selections, new_selections_to_columnarize) = {
if let Some(state) = self.add_selections_state.as_ref() {
let columnar_selection_ids: HashSet<_> = state
.groups
.iter()
.flat_map(|group| group.stack.iter())
.copied()
.collect();
all_selections
.into_iter()
.partition(|s| columnar_selection_ids.contains(&s.id))
let buffer = display_map.buffer_snapshot();
let all_selections = self.selections.all_unexpanded::<Point>(&display_map);
let goal_source_for_selection = |selection: &Selection<Point>| {
let end_bias = if selection.is_empty() {
Bias::Right
} else {
(Vec::new(), all_selections)
}
Bias::Left
};
buffer.anchor_after(selection.start)..buffer.anchor_at(selection.end, end_bias)
};
let history_entry = self
.deferred_selection_effects_state
.is_none()
.then(|| self.selection_history_entry());
if self
.add_selections_state
.as_ref()
.is_some_and(|state| state.skip_soft_wrap != skip_soft_wrap)
{
self.add_selections_state = None;
}
let mut state = self
.add_selections_state
.take()
.unwrap_or_else(|| AddSelectionsState { groups: Vec::new() });
.unwrap_or_else(|| AddSelectionsState {
groups: Vec::new(),
skip_soft_wrap,
});
if !state.groups.is_empty() {
let previous = Self::resolve_add_selection_anchors(
self.selections.disjoint_anchors(),
&display_map,
);
Self::reconcile_add_selection_groups(&mut state, &previous, &all_selections);
}
let columnar_selection_ids = state
.groups
.iter()
.flat_map(|group| group.stack.iter())
.copied()
.collect::<HashSet<_>>();
let mut columnar_rows = ColumnarSelectionRows::new(&display_map);
let (mut columnar_selections, new_selections_to_columnarize) =
all_selections
.into_iter()
.partition::<Vec<_>, _>(|selection| columnar_selection_ids.contains(&selection.id));
for selection in new_selections_to_columnarize {
let mut goal_columns_by_selection_id = HashMap::default();
let mut projected_selections = Vec::new();
let mut projection_counts = vec![None; new_selections_to_columnarize.len()];
if skip_soft_wrap {
let selections_by_id = columnar_selections
.iter()
.map(|selection| (selection.id, selection))
.collect::<HashMap<_, _>>();
let mut sources = Vec::new();
for group in &mut state.groups {
let Some(oldest_selection) =
group.stack.first().and_then(|id| selections_by_id.get(id))
else {
continue;
};
if group.goal_source.as_ref().is_some_and(|source| {
!buffer.can_resolve(&source.start) || !buffer.can_resolve(&source.end)
}) {
group.goal_source = None;
}
let source = group
.goal_source
.get_or_insert_with(|| goal_source_for_selection(oldest_selection));
if let Some(last_id) = group.stack.last() {
sources.push((*last_id, source.to_point(buffer)));
}
}
sources.extend(
new_selections_to_columnarize
.iter()
.map(|selection| (selection.id, selection.range())),
);
let ranges = sources
.iter()
.map(|(_, range)| range.clone())
.collect::<Vec<_>>();
goal_columns_by_selection_id.extend(
sources
.iter()
.map(|(id, _)| *id)
.zip(columnar_rows.columns_for_ranges(&ranges)),
);
let tabs = display_map.tab_snapshot();
let mut queries = Vec::new();
for (selection, projection_count) in new_selections_to_columnarize
.iter()
.zip(&mut projection_counts)
{
let start = tabs.point_to_tab_point(selection.start, Bias::Left);
let end = tabs.point_to_tab_point(selection.end, Bias::Left);
if start.row() != end.row()
|| display_map
.is_block_line(selection.start.to_display_point(&display_map).row())
{
let columns = &goal_columns_by_selection_id[&selection.id];
let query_start = queries.len();
let mut row = start.row();
loop {
let last_row =
if let Some(hidden) = display_map.fully_replaced_tab_rows(row) {
*hidden.end()
} else {
queries.push((row, columns.clone(), selection.reversed));
row
};
if last_row >= end.row() {
break;
}
row = last_row + 1;
}
*projection_count = Some(queries.len() - query_start);
}
}
projected_selections = self
.selections
.build_columnar_selections_from_tab_expanded_columns(&mut columnar_rows, &queries);
}
let mut projected_selections = projected_selections.into_iter();
for (selection, projection_count) in new_selections_to_columnarize
.into_iter()
.zip(projection_counts)
{
if skip_soft_wrap {
let goal_source = Some(goal_source_for_selection(&selection));
let mut stack = Vec::new();
if let Some(count) = projection_count {
for selection in projected_selections.by_ref().take(count).flatten() {
stack.push(selection.id);
columnar_selections.push(selection);
}
} else {
stack.push(selection.id);
columnar_selections.push(selection);
}
if !stack.is_empty() {
if above {
stack.reverse();
}
if let Some(columns) = goal_columns_by_selection_id.remove(&selection.id)
&& let Some(last_id) = stack.last()
{
goal_columns_by_selection_id.insert(*last_id, columns);
}
state.groups.push(AddSelectionsGroup {
above,
stack,
goal_source,
});
} else {
columnar_selections.push(selection);
}
continue;
}
let range = selection.display_range(&display_map).sorted();
let start_x = display_map.x_for_display_point(range.start, &text_layout_details);
let end_x = display_map.x_for_display_point(range.end, &text_layout_details);
let positions = start_x.min(end_x)..start_x.max(end_x);
let mut stack = Vec::new();
for row in range.start.row().0..=range.end.row().0 {
if let Some(selection) = self.selections.build_columnar_selection(
&display_map,
DisplayRow(row),
&positions,
selection.reversed,
&text_layout_details,
) {
stack.push(selection.id);
columnar_selections.push(selection);
if range.start.row() == range.end.row() && !display_map.is_block_line(range.start.row())
{
stack.push(selection.id);
columnar_selections.push(Selection {
goal: SelectionGoal::HorizontalRange {
start: positions.start.into(),
end: positions.end.into(),
},
..selection
});
} else {
for row in range.start.row().0..=range.end.row().0 {
if let Some(selection) = self.selections.build_columnar_selection(
&display_map,
DisplayRow(row),
&positions,
selection.reversed,
&text_layout_details,
) {
stack.push(selection.id);
columnar_selections.push(selection);
}
}
}
if !stack.is_empty() {
if above {
stack.reverse();
}
state.groups.push(AddSelectionsGroup { above, stack });
state.groups.push(AddSelectionsGroup {
above,
stack,
goal_source: None,
});
} else {
columnar_selections.push(selection);
}
}
@@ -2018,31 +2310,11 @@ impl Editor {
display_map.max_point().row()
};
// When `skip_soft_wrap` is true, we place new selections by column rather than
// pixel position, so we need to keep track of the column range of the oldest
// selection in each group, because intermediate selections may have been
// clamped to shorter lines. Columns are counted with tabs expanded so that
// tab-aligned code lines up the way it renders.
let mut goal_columns_by_selection_id = if skip_soft_wrap {
let mut map = HashMap::default();
for group in state.groups.iter() {
if let Some(oldest_id) = group.stack.first() {
if let Some(oldest_selection) =
columnar_selections.iter().find(|s| s.id == *oldest_id)
{
let start_col = display_map.tab_expanded_column(oldest_selection.start);
let end_col = display_map.tab_expanded_column(oldest_selection.end);
let goal_columns = start_col.min(end_col)..start_col.max(end_col);
for id in &group.stack {
map.insert(*id, goal_columns.clone());
}
}
}
if skip_soft_wrap {
for selection in &mut columnar_selections {
selection.goal = SelectionGoal::None;
}
map
} else {
HashMap::default()
};
}
let mut last_added_item_per_group = HashMap::default();
for group in state.groups.iter_mut() {
@@ -2051,41 +2323,58 @@ impl Editor {
}
}
let mut next_logical_selections = HashMap::default();
if skip_soft_wrap {
let queries = columnar_selections
.iter()
.filter_map(|selection| {
let group = last_added_item_per_group.get_mut(&selection.id)?;
if group.stack.len() == 1 {
group.above = above;
}
(group.above == above).then(|| {
(
*selection,
goal_columns_by_selection_id[&selection.id].clone(),
)
})
})
.collect::<Vec<_>>();
next_logical_selections.extend(queries.iter().map(|(selection, _)| selection.id).zip(
self.selections.find_next_columnar_selections_by_buffer_row(
&display_map,
&mut columnar_rows,
&queries,
above,
),
));
}
for selection in columnar_selections {
if let Some(group) = last_added_item_per_group.get_mut(&selection.id) {
if skip_soft_wrap && group.stack.len() == 1 {
group.above = above;
}
if above == group.above {
let range = selection.display_range(&display_map).sorted();
debug_assert_eq!(range.start.row(), range.end.row());
let row = range.start.row();
let positions =
if let SelectionGoal::HorizontalRange { start, end } = selection.goal {
Pixels::from(start)..Pixels::from(end)
} else {
let start_x =
display_map.x_for_display_point(range.start, &text_layout_details);
let end_x =
display_map.x_for_display_point(range.end, &text_layout_details);
start_x.min(end_x)..start_x.max(end_x)
};
let maybe_new_selection = if skip_soft_wrap {
let goal_columns = goal_columns_by_selection_id
.remove(&selection.id)
.unwrap_or_else(|| {
let start_col = display_map.tab_expanded_column(selection.start);
let end_col = display_map.tab_expanded_column(selection.end);
start_col.min(end_col)..start_col.max(end_col)
});
self.selections.find_next_columnar_selection_by_buffer_row(
&display_map,
row,
end_row,
above,
&goal_columns,
selection.reversed,
&text_layout_details,
)
next_logical_selections.remove(&selection.id).flatten()
} else {
let range = selection.display_range(&display_map).sorted();
let row = if above {
range.start.row()
} else {
range.end.row()
};
let positions =
if let SelectionGoal::HorizontalRange { start, end } = selection.goal {
Pixels::from(start)..Pixels::from(end)
} else {
let start_x = display_map
.x_for_display_point(range.start, &text_layout_details);
let end_x = display_map
.x_for_display_point(range.end, &text_layout_details);
start_x.min(end_x)..start_x.max(end_x)
};
self.selections.find_next_columnar_selection_by_display_row(
&display_map,
row,
@@ -2117,23 +2406,17 @@ impl Editor {
}
}
self.change_selections(Default::default(), window, cx, |s| {
s.select(final_selections);
});
let unmerged_selections = final_selections.clone();
self.change_selections_with_history(
SelectionEffects::default(),
history_entry,
window,
cx,
|selections| selections.select(final_selections),
);
let final_selection_ids: HashSet<_> = self
.selections
.all::<Point>(&display_map)
.iter()
.map(|s| s.id)
.collect();
state.groups.retain_mut(|group| {
// selections might get merged above so we remove invalid items from stacks
group.stack.retain(|id| final_selection_ids.contains(id));
// single selection in stack can be treated as initial state
group.stack.len() > 1
});
let current = self.selections.all_unexpanded::<Point>(&display_map);
Self::reconcile_add_selection_groups(&mut state, &unmerged_selections, &current);
if !state.groups.is_empty() {
self.add_selections_state = Some(state);
@@ -2187,11 +2470,11 @@ impl Editor {
if !select_next_state.done {
let first_selection = selections
.iter()
.min_by_key(|s| s.id)
.min_by_key(|selection| selection.id)
.context("missing selection for select next action")?;
let last_selection = selections
.iter()
.max_by_key(|s| s.id)
.max_by_key(|selection| selection.id)
.context("missing selection for select next action")?;
let mut next_selected_range = None;
+562 -110
View File
@@ -1,5 +1,7 @@
use std::{
cmp, fmt, iter, mem,
cmp,
collections::BTreeMap,
fmt, iter, mem,
ops::{AddAssign, Deref, DerefMut, Range, Sub},
sync::Arc,
};
@@ -7,11 +9,11 @@ use std::{
use gpui::Pixels;
use itertools::{Either, Itertools as _};
use language::{Bias, Point, Selection, SelectionGoal};
use multi_buffer::{MultiBufferDimension, MultiBufferOffset, MultiBufferRow, ToPoint};
use util::post_inc;
use multi_buffer::{MultiBufferDimension, MultiBufferOffset, ToPoint};
use crate::{
Anchor, DisplayPoint, DisplayRow, MultiBufferSnapshot, SelectMode, ToOffset,
columnar_selection::ColumnarSelectionRows,
display_map::{DisplaySnapshot, ToDisplayPoint},
movement::TextLayoutDetails,
};
@@ -115,6 +117,13 @@ impl SelectionsCollection {
resolve_selections_wrapping_blocks(self.pending_anchor(), &snapshot).next()
}
pub(crate) fn all_unexpanded<D>(&self, snapshot: &DisplaySnapshot) -> Vec<Selection<D>>
where
D: MultiBufferDimension + Sub + AddAssign<<D as Sub>::Output> + Ord,
{
self.all_iter(snapshot, false).collect()
}
pub(crate) fn pending_mode(&self) -> Option<SelectMode> {
self.pending.as_ref().map(|pending| pending.mode.clone())
}
@@ -123,19 +132,21 @@ impl SelectionsCollection {
where
D: MultiBufferDimension + Sub + AddAssign<<D as Sub>::Output> + Ord,
{
self.all_iter(snapshot).collect()
self.all_iter(snapshot, true).collect()
}
fn all_iter<'a, D>(
&'a self,
snapshot: &'a DisplaySnapshot,
expand_blocks: bool,
) -> 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);
resolve_selections::<D, _>(self.disjoint.iter(), snapshot, expand_blocks).peekable();
let mut pending_opt =
resolve_selections::<D, _>(self.pending_anchor(), snapshot, expand_blocks).next();
iter::from_fn(move || {
if let Some(pending) = pending_opt.as_mut() {
while let Some(next_selection) = disjoint.peek() {
@@ -347,7 +358,7 @@ impl SelectionsCollection {
.next()
.unwrap();
}
self.all_iter(snapshot).next().unwrap()
self.all_iter(snapshot, true).next().unwrap()
}
pub fn last<D>(&self, snapshot: &DisplaySnapshot) -> Selection<D>
@@ -361,7 +372,7 @@ impl SelectionsCollection {
.next()
.unwrap();
}
self.all_iter(snapshot).last().unwrap()
self.all_iter(snapshot, true).last().unwrap()
}
/// Returns a list of (potentially backwards!) ranges representing the selections.
@@ -413,6 +424,9 @@ impl SelectionsCollection {
reversed: bool,
text_layout_details: &TextLayoutDetails,
) -> Option<Selection<Point>> {
if display_map.is_block_line(row) {
return None;
}
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);
@@ -432,7 +446,7 @@ impl SelectionsCollection {
};
Some(Selection {
id: post_inc(&mut self.next_selection_id),
id: self.allocate_selection_id(),
start: start.to_point(display_map),
end: end.to_point(display_map),
reversed,
@@ -443,52 +457,28 @@ impl SelectionsCollection {
})
}
/// Attempts to build a selection in the provided buffer row using the
/// same tab-expanded 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_tab_expanded_columns(
pub(crate) fn build_columnar_selections_from_tab_expanded_columns(
&mut self,
display_map: &DisplaySnapshot,
multi_buffer_row: MultiBufferRow,
goal_columns: &Range<u32>,
reversed: bool,
text_layout_details: &TextLayoutDetails,
) -> Option<Selection<Point>> {
let is_empty = goal_columns.start == goal_columns.end;
let line_len = display_map.tab_expanded_line_len(multi_buffer_row);
let (start, end) = if is_empty {
let point =
display_map.point_for_tab_expanded_column(multi_buffer_row, goal_columns.start);
(point, point)
} else {
if goal_columns.start >= line_len {
return None;
}
let start =
display_map.point_for_tab_expanded_column(multi_buffer_row, goal_columns.start);
let end = display_map.point_for_tab_expanded_column(multi_buffer_row, goal_columns.end);
(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(),
},
})
rows: &mut ColumnarSelectionRows<'_>,
queries: &[(u32, Range<u32>, bool)],
) -> Vec<Option<Selection<Point>>> {
let row_queries = queries
.iter()
.map(|(row, columns, _)| (*row, columns.clone()))
.collect::<Vec<_>>();
rows.points_for_rows(&row_queries)
.into_iter()
.zip(queries)
.map(|(points, &(_, _, reversed))| {
points.map(|(start, end)| Selection {
id: self.allocate_selection_id(),
start,
end,
reversed,
goal: SelectionGoal::None,
})
})
.collect()
}
/// Finds the next columnar selection by walking display rows one at a time
@@ -524,37 +514,82 @@ impl SelectionsCollection {
None
}
/// Finds the next columnar selection by skipping to the next buffer row,
/// ignoring soft-wrapped lines.
pub(crate) fn find_next_columnar_selection_by_buffer_row(
pub(crate) fn find_next_columnar_selections_by_buffer_row(
&mut self,
display_map: &DisplaySnapshot,
start_row: DisplayRow,
end_row: DisplayRow,
rows: &mut ColumnarSelectionRows<'_>,
queries: &[(Selection<Point>, Range<u32>)],
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 = MultiBufferRow(new_row.to_point(display_map).row);
if let Some(selection) = self.build_columnar_selection_from_tab_expanded_columns(
display_map,
buffer_row,
goal_columns,
reversed,
text_layout_details,
) {
return Some(selection);
) -> Vec<Option<Selection<Point>>> {
let tabs = display_map.tab_snapshot();
let max_row = tabs.max_point().row();
let next_row = |row: u32| {
if above {
row.checked_sub(1)
} else {
row.checked_add(1).filter(|row| *row <= max_row)
}
};
let mut pending = BTreeMap::<_, Vec<_>>::new();
let mut answers = vec![None; queries.len()];
for (index, (selection, columns)) in queries.iter().enumerate() {
if columns.start > columns.end {
continue;
}
let point = if above {
selection.start
} else {
selection.end
};
if let Some(row) = next_row(tabs.point_to_tab_point(point, Bias::Left).row()) {
pending.entry(row).or_default().push(index);
}
}
None
while let Some((row, query_indices)) = if above {
pending.pop_last()
} else {
pending.pop_first()
} {
if let Some(hidden_rows) = display_map.fully_replaced_tab_rows(row) {
let row = if above {
*hidden_rows.start()
} else {
*hidden_rows.end()
};
if let Some(row) = next_row(row) {
pending.entry(row).or_default().extend(query_indices);
}
continue;
}
let row_queries = query_indices
.iter()
.map(|&index| (row, queries[index].1.clone()))
.collect::<Vec<_>>();
let next_row = next_row(row);
for (index, points) in query_indices
.into_iter()
.zip(rows.points_for_rows(&row_queries))
{
if let Some(points) = points {
answers[index] = Some(points);
} else if let Some(row) = next_row {
pending.entry(row).or_default().push(index);
}
}
}
answers
.into_iter()
.zip(queries)
.map(|(points, (selection, _))| {
points.map(|(start, end)| Selection {
id: self.allocate_selection_id(),
start,
end,
reversed: selection.reversed,
goal: SelectionGoal::None,
})
})
.collect()
}
pub fn change_with<R>(
@@ -643,6 +678,12 @@ impl SelectionsCollection {
pub fn set_is_extending(&mut self, is_extending: bool) {
self.is_extending = is_extending;
}
fn allocate_selection_id(&mut self) -> usize {
let id = self.next_selection_id;
self.next_selection_id = id.wrapping_add(1);
id
}
}
pub struct MutableSelectionsCollection<'snap, 'a> {
@@ -714,7 +755,7 @@ impl<'snap, 'a> MutableSelectionsCollection<'snap, 'a> {
.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),
id: self.collection.allocate_selection_id(),
start: anchor,
end: anchor,
reversed: false,
@@ -746,7 +787,7 @@ impl<'snap, 'a> MutableSelectionsCollection<'snap, 'a> {
false
};
Selection {
id: post_inc(&mut self.collection.next_selection_id),
id: self.collection.allocate_selection_id(),
start,
end,
reversed,
@@ -806,7 +847,7 @@ impl<'snap, 'a> MutableSelectionsCollection<'snap, 'a> {
false
};
selections.push(Selection {
id: post_inc(&mut self.collection.next_selection_id),
id: self.collection.allocate_selection_id(),
start,
end,
reversed,
@@ -895,7 +936,7 @@ impl<'snap, 'a> MutableSelectionsCollection<'snap, 'a> {
false
};
Selection {
id: post_inc(&mut self.collection.next_selection_id),
id: self.collection.allocate_selection_id(),
start,
end,
reversed,
@@ -923,7 +964,7 @@ impl<'snap, 'a> MutableSelectionsCollection<'snap, 'a> {
false
};
Selection {
id: post_inc(&mut self.collection.next_selection_id),
id: self.collection.allocate_selection_id(),
start,
end,
reversed,
@@ -935,7 +976,7 @@ impl<'snap, 'a> MutableSelectionsCollection<'snap, 'a> {
}
pub fn new_selection_id(&mut self) -> usize {
post_inc(&mut self.next_selection_id)
self.collection.allocate_selection_id()
}
pub fn select_display_ranges<T>(&mut self, ranges: T)
@@ -954,7 +995,7 @@ impl<'snap, 'a> MutableSelectionsCollection<'snap, 'a> {
false
};
Selection {
id: post_inc(&mut self.collection.next_selection_id),
id: self.collection.allocate_selection_id(),
start: start.to_point(self.snapshot),
end: end.to_point(self.snapshot),
reversed,
@@ -1088,7 +1129,7 @@ impl<'snap, 'a> MutableSelectionsCollection<'snap, 'a> {
.map(|cursor| {
let cursor_point = cursor.to_point(self.snapshot);
Selection {
id: post_inc(&mut self.collection.next_selection_id),
id: self.collection.allocate_selection_id(),
start: cursor_point,
end: cursor_point,
reversed: false,
@@ -1103,6 +1144,10 @@ impl<'snap, 'a> MutableSelectionsCollection<'snap, 'a> {
self.selections_changed = true;
self.pending.as_mut().map(|pending| &mut pending.selection)
}
pub(crate) fn select_anchors_unexpanded(&mut self, selections: Vec<Selection<Anchor>>) {
self.select(selections);
}
}
impl Deref for MutableSelectionsCollection<'_, '_> {
@@ -1209,13 +1254,25 @@ 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>>,
{
resolve_selections(selections, map, true)
}
fn resolve_selections<'a, D, I>(
selections: I,
map: &'a DisplaySnapshot,
expand_blocks: bool,
) -> 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() {
if !expand_blocks || !map.has_collapsed_content() {
Either::Left(resolve_selections_without_display_round_trip(
selections, map,
))
@@ -1356,7 +1413,9 @@ mod tests {
use super::*;
use crate::{
MultiBuffer,
display_map::{BlockPlacement, BlockProperties, BlockStyle, DisplayMap, FoldPlaceholder},
display_map::{
BlockPlacement, BlockProperties, BlockStyle, Crease, DisplayMap, FoldPlaceholder,
},
test::test_font,
};
use gpui::{AppContext as _, IntoElement as _, div, px};
@@ -1366,26 +1425,7 @@ mod tests {
use std::sync::Arc;
fn row_range_snapshot(cx: &mut gpui::TestAppContext, text: &str) -> DisplaySnapshot {
cx.update(|cx| {
let settings = SettingsStore::test(cx);
cx.set_global(settings);
crate::init(cx);
});
let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
let display_map = cx.new(|cx| {
DisplayMap::new(
buffer,
test_font(),
px(14.),
None,
1,
1,
FoldPlaceholder::test(),
DiagnosticSeverity::Warning,
cx,
)
});
display_map.update(cx, |map, cx| map.snapshot(cx))
selection_test_display_map(cx, text).update(cx, |map, cx| map.snapshot(cx))
}
fn row_range_collection(
@@ -1552,6 +1592,418 @@ mod tests {
assert_eq!(fast_offsets, slow_offsets);
}
#[gpui::test]
fn select_anchors_unexpanded_preserves_hidden_buffer_ranges(cx: &mut gpui::TestAppContext) {
let map = selection_test_display_map(cx, "aaaa\nbbbb\ncccc\ndddd\neeee\nffff");
let snapshot = map.update(cx, |map, cx| {
map.fold(
vec![Crease::simple(
Point::new(1, 0)..Point::new(2, 4),
FoldPlaceholder::test(),
)],
cx,
);
let buffer = map.snapshot(cx).buffer_snapshot().clone();
map.insert_blocks(
[BlockProperties {
placement: BlockPlacement::Replace(
buffer.anchor_after(Point::new(4, 0))
..=buffer.anchor_before(Point::new(5, 4)),
),
height: Some(1),
style: BlockStyle::Fixed,
render: Arc::new(|_| div().into_any_element()),
priority: 0,
}],
cx,
);
map.snapshot(cx)
});
let saved = [1, 2, 4, 5]
.into_iter()
.map(|row| Selection {
id: row as usize + 10,
start: Point::new(row, 1),
end: Point::new(row, 2),
reversed: row % 2 == 0,
goal: SelectionGoal::None,
})
.collect::<Vec<_>>();
let anchors = saved
.iter()
.map(|selection| {
selection_to_anchor_selection(
selection.map(|point| point.to_offset(&snapshot)),
&snapshot,
)
})
.collect::<Vec<_>>();
let mut collection = SelectionsCollection::new();
collection.change_with(&snapshot, |selections| {
selections.select_anchors_unexpanded(anchors);
});
assert_eq!(collection.all_unexpanded::<Point>(&snapshot), saved);
assert_eq!(collection.pending_anchor(), None);
let mut overlapping = saved[0];
overlapping.id = 30;
overlapping.end.column = 3;
collection.change_with(&snapshot, |selections| {
selections.select_anchors_unexpanded(
[saved[0], overlapping]
.into_iter()
.map(|selection| {
selection_to_anchor_selection(
selection.map(|point| point.to_offset(&snapshot)),
&snapshot,
)
})
.collect(),
);
});
assert_eq!(
collection.all_unexpanded::<Point>(&snapshot),
vec![Selection {
end: overlapping.end,
..saved[0]
}]
);
}
#[gpui::test]
fn columnar_selection_batch_preserves_request_order(cx: &mut gpui::TestAppContext) {
let snapshot = row_range_snapshot(cx, "abcd\n\né🦀z\n\txyz");
assert_eq!(snapshot.tab_snapshot().text(), "abcd\n\né🦀z\n xyz");
let mut rows = ColumnarSelectionRows::new(&snapshot);
let mut collection = SelectionsCollection::new();
let actual = collection.build_columnar_selections_from_tab_expanded_columns(
&mut rows,
&[
(2, 1..2, true),
(1, 0..1, false),
(0, 2..u32::MAX, false),
(2, 0..3, false),
(u32::MAX, 0..0, true),
(1, u32::MAX..u32::MAX, true),
(0, 4..5, true),
(0, u32::MAX..u32::MAX, false),
(3, 1..3, true),
(3, 0..5, false),
(2, 3..4, false),
(0, Range { start: 3, end: 2 }, false),
],
);
assert_eq!(
actual,
vec![
Some(columnar_test_selection(1, 2, 2..6, true)),
None,
Some(columnar_test_selection(2, 0, 2..4, false)),
Some(columnar_test_selection(3, 2, 0..7, false)),
None,
Some(columnar_test_selection(4, 1, 0..0, true)),
None,
Some(columnar_test_selection(5, 0, 4..4, false)),
Some(columnar_test_selection(6, 3, 0..0, true)),
Some(columnar_test_selection(7, 3, 0..2, false)),
None,
None,
]
);
assert_eq!(collection.next_selection_id(), 8);
assert_eq!(
collection.build_columnar_selections_from_tab_expanded_columns(&mut rows, &[]),
Vec::new()
);
assert_eq!(collection.next_selection_id(), 8);
}
#[gpui::test]
fn columnar_selection_batch_joins_different_starts_and_skips(cx: &mut gpui::TestAppContext) {
for above in [false, true] {
let mut lines = [
"abcdefgh", "x", "", "abcdefgh", "x", "", "abcdefgh", "x", "abcdefgh",
];
if above {
lines.reverse();
}
let snapshot = row_range_snapshot(cx, &lines.join("\n"));
let mut rows = ColumnarSelectionRows::new(&snapshot);
let mut collection = SelectionsCollection::new();
let row = |row| if above { 8 - row } else { row };
let queries = [
(5, 2..5, false),
(0, 3..7, true),
(2, 2..4, false),
(1, 1..2, true),
(8, 0..1, false),
(0, 8..9, true),
(3, 0..0, false),
(6, 0..2, true),
(6, u32::MAX..u32::MAX, false),
(5, Range { start: 1, end: 0 }, true),
(7, 0..8, true),
]
.into_iter()
.enumerate()
.map(|(index, (source_row, columns, reversed))| {
(
columnar_test_selection(index + 20, row(source_row), 0..0, reversed),
columns,
)
})
.collect::<Vec<_>>();
assert_eq!(
collection.find_next_columnar_selections_by_buffer_row(
&snapshot, &mut rows, &queries, above,
),
vec![
Some(columnar_test_selection(1, row(6), 2..5, false)),
Some(columnar_test_selection(2, row(3), 3..7, true)),
Some(columnar_test_selection(3, row(3), 2..4, false)),
Some(columnar_test_selection(4, row(3), 1..2, true)),
None,
None,
Some(columnar_test_selection(5, row(4), 0..0, false)),
Some(columnar_test_selection(6, row(7), 0..1, true)),
Some(columnar_test_selection(7, row(7), 1..1, false)),
None,
Some(columnar_test_selection(8, row(8), 0..8, true)),
],
"above: {above}"
);
assert_eq!(collection.next_selection_id(), 9);
}
}
#[gpui::test]
fn columnar_selection_batch_uses_directional_endpoints(cx: &mut gpui::TestAppContext) {
let snapshot = row_range_snapshot(cx, "abcd\nabcd\nabcd\nabcd\nabcd");
let mut rows = ColumnarSelectionRows::new(&snapshot);
for above in [false, true] {
let mut collection = SelectionsCollection::new();
let queries = [false, true].map(|reversed| {
(
Selection {
id: 20,
start: Point::new(1, 1),
end: Point::new(3, 2),
reversed,
goal: SelectionGoal::None,
},
1..3,
)
});
let destination = if above { 0 } else { 4 };
assert_eq!(
collection.find_next_columnar_selections_by_buffer_row(
&snapshot, &mut rows, &queries, above,
),
vec![
Some(columnar_test_selection(1, destination, 1..3, false)),
Some(columnar_test_selection(2, destination, 1..3, true)),
]
);
assert_eq!(collection.next_selection_id(), 3);
}
}
#[gpui::test]
fn columnar_selection_batch_unavailable_does_not_allocate(cx: &mut gpui::TestAppContext) {
for text in ["", "abcd"] {
let snapshot = row_range_snapshot(cx, text);
let mut rows = ColumnarSelectionRows::new(&snapshot);
let mut collection = SelectionsCollection::new();
for above in [false, true] {
let queries = [
(columnar_test_selection(0, 0, 0..0, false), 0..0),
(columnar_test_selection(0, 0, 0..0, true), 0..1),
];
assert_eq!(
collection.find_next_columnar_selections_by_buffer_row(
&snapshot, &mut rows, &queries, above,
),
vec![None, None]
);
assert_eq!(
collection.find_next_columnar_selections_by_buffer_row(
&snapshot,
&mut rows,
&[],
above,
),
Vec::new()
);
assert_eq!(collection.next_selection_id(), 1);
}
}
}
#[gpui::test]
fn columnar_selection_batch_joins_replacement_skips(cx: &mut gpui::TestAppContext) {
let map = selection_test_display_map(
cx,
&iter::repeat_n("abcdefgh", 9).collect::<Vec<_>>().join("\n"),
);
let snapshot = map.update(cx, |map, cx| {
let buffer = map.snapshot(cx).buffer_snapshot().clone();
map.insert_blocks(
[BlockProperties {
placement: BlockPlacement::Replace(
buffer.anchor_after(Point::new(3, 0))
..=buffer.anchor_before(Point::new(5, 8)),
),
height: Some(1),
style: BlockStyle::Fixed,
render: Arc::new(|_| div().into_any_element()),
priority: 0,
}],
cx,
);
map.snapshot(cx)
});
for above in [false, true] {
let mut rows = ColumnarSelectionRows::new(&snapshot);
let mut collection = SelectionsCollection::new();
let row = |row| if above { 8 - row } else { row };
let queries = [
(6, 1..3, false),
(0, 2..6, true),
(2, 1..2, false),
(4, 0..0, true),
(5, 3..7, true),
(8, 0..0, false),
(1, 8..9, false),
]
.into_iter()
.enumerate()
.map(|(index, (source_row, columns, reversed))| {
(
columnar_test_selection(index + 20, row(source_row), 0..0, reversed),
columns,
)
})
.collect::<Vec<_>>();
assert_eq!(
collection.find_next_columnar_selections_by_buffer_row(
&snapshot, &mut rows, &queries, above,
),
vec![
Some(columnar_test_selection(1, row(7), 1..3, false)),
Some(columnar_test_selection(2, row(1), 2..6, true)),
Some(columnar_test_selection(3, row(6), 1..2, false)),
Some(columnar_test_selection(4, row(6), 0..0, true)),
Some(columnar_test_selection(5, row(6), 3..7, true)),
None,
None,
],
"above: {above}"
);
assert_eq!(collection.next_selection_id(), 6);
}
}
#[gpui::test]
fn columnar_selection_skips_replacement_input_rows(cx: &mut gpui::TestAppContext) {
let text = iter::repeat_n("abcd", 1002).collect::<Vec<_>>().join("\n");
for hidden in [1..=1000, 0..=1000, 1..=1001, 0..=1001] {
let map = selection_test_display_map(cx, &text);
let snapshot = map.update(cx, |map, cx| {
let buffer = map.snapshot(cx).buffer_snapshot().clone();
map.insert_blocks(
[BlockProperties {
placement: BlockPlacement::Replace(
buffer.anchor_after(Point::new(*hidden.start(), 0))
..=buffer.anchor_before(Point::new(*hidden.end(), 4)),
),
height: Some(1),
style: BlockStyle::Fixed,
render: Arc::new(|_| div().into_any_element()),
priority: 0,
}],
cx,
);
map.snapshot(cx)
});
for above in [false, true] {
let source_row = if above { 1001 } else { 0 };
let source = Selection {
id: 0,
start: Point::new(source_row, 0),
end: Point::new(source_row, 1),
reversed: above,
goal: SelectionGoal::None,
};
let expected_row = if above {
hidden.start().checked_sub(1)
} else if *hidden.end() == 1001 {
None
} else {
Some(hidden.end() + 1)
};
let mut collection = SelectionsCollection::new();
let mut rows = ColumnarSelectionRows::new(&snapshot);
let actual = collection.find_next_columnar_selections_by_buffer_row(
&snapshot,
&mut rows,
&[(source, 0..1)],
above,
);
assert_eq!(
actual,
vec![expected_row.map(|row| Selection {
id: 1,
start: Point::new(row, 0),
end: Point::new(row, 1),
reversed: above,
goal: SelectionGoal::None,
})],
"hidden: {hidden:?}, above: {above}"
);
}
}
}
fn columnar_test_selection(
id: usize,
row: u32,
columns: Range<u32>,
reversed: bool,
) -> Selection<Point> {
Selection {
id,
start: Point::new(row, columns.start),
end: Point::new(row, columns.end),
reversed,
goal: SelectionGoal::None,
}
}
fn selection_test_display_map(
cx: &mut gpui::TestAppContext,
text: &str,
) -> gpui::Entity<DisplayMap> {
cx.update(|cx| {
let settings = SettingsStore::test(cx);
cx.set_global(settings);
crate::init(cx);
});
let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
cx.new(|cx| {
DisplayMap::new(
buffer,
test_font(),
px(14.),
None,
1,
1,
FoldPlaceholder::test(),
DiagnosticSeverity::Warning,
cx,
)
})
}
fn random_text(rng: &mut StdRng) -> String {
let mut text = String::new();
for line in 0..rng.random_range(8..32) {