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:
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
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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, ¤t);
|
||||
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, ¤t_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, ¤t);
|
||||
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, ¤t);
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user