Files
zed/crates/editor/src/columnar_selection.rs
T
9f1587c697 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>
2026-09-16 16:40:28 +00:00

2853 lines
111 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use language::{Bias, LanguageAwareStyling, Point};
use multi_buffer::{MBTextSummary, MultiBufferRow};
use std::ops::Range;
use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
use crate::{
DisplayPoint,
display_map::{DisplaySnapshot, FoldPoint, Highlights, TabPoint, ToDisplayPoint as _},
};
pub(crate) struct ColumnarSelectionRows<'a> {
snapshot: &'a DisplaySnapshot,
}
impl<'a> ColumnarSelectionRows<'a> {
pub(crate) fn new(snapshot: &'a DisplaySnapshot) -> Self {
Self { snapshot }
}
pub(crate) fn columns_for_ranges(&mut self, ranges: &[Range<Point>]) -> Vec<Range<u32>> {
let tabs = self.snapshot.tab_snapshot();
let mut endpoints = ranges
.iter()
.enumerate()
.flat_map(|(index, range)| {
[(range.start, false), (range.end, true)]
.map(|(point, end)| (tabs.point_to_tab_point(point, Bias::Left), index, end))
})
.collect::<Vec<_>>();
endpoints.sort_unstable();
let mut answers = vec![0..0; ranges.len()];
for endpoints in endpoints.chunk_by(|left, right| left.0.row() == right.0.row()) {
let mut text = self.row(endpoints[0].0.row());
let mut cursor = text.forward_cursor();
for &(point, index, end) in endpoints {
text.floor_forward(&mut cursor, point.column());
if end {
answers[index].end = cursor.column;
} else {
answers[index].start = cursor.column;
}
}
}
for answer in &mut answers {
*answer = answer.start.min(answer.end)..answer.start.max(answer.end);
}
answers
}
pub(crate) fn points_for_rows(
&mut self,
queries: &[(u32, Range<u32>)],
) -> Vec<Option<(Point, Point)>> {
let tabs = self.snapshot.tab_snapshot();
let max_row = tabs.max_point().row();
let mut endpoints = Vec::new();
for (index, (row, columns)) in queries.iter().enumerate() {
if *row <= max_row && columns.start <= columns.end {
endpoints.push((*row, columns.start, false, index));
if columns.start != columns.end {
endpoints.push((*row, columns.end, true, index));
}
}
}
endpoints.sort_unstable();
let mut answers = vec![None::<(Point, Point)>; queries.len()];
for endpoints in endpoints.chunk_by(|left, right| left.0 == right.0) {
let row = endpoints[0].0;
let length = tabs.line_len(row);
let mut text = self.row(row);
let mut cursor = text.cursor(0);
let mut column = 0;
let mut preceding = None;
let mut buffer_cursor = None;
for &(_, goal, end, index) in endpoints {
let columns = &queries[index].1;
if (end && answers[index].is_none())
|| (!end && columns.start != columns.end && goal >= length)
{
continue;
}
text.advance_columns(&mut cursor, goal - column);
column = goal;
if !end && columns.start != columns.end && cursor.byte() == length {
continue;
}
let point = self.point_for_cursor(
row,
&mut text,
cursor.clone(),
preceding,
&mut buffer_cursor,
);
preceding = Some((cursor.byte(), point));
answers[index] = if end {
answers[index]
.zip(point)
.map(|((start, _), end)| (start.min(end), start.max(end)))
} else {
point.map(|point| (point, point))
};
}
}
answers
}
#[cfg(test)]
pub(crate) fn columns_for_range(&mut self, range: Range<Point>) -> Range<u32> {
self.columns_for_ranges(&[range]).pop().expect("one range")
}
#[cfg(test)]
pub(crate) fn points_for_row(
&mut self,
row: u32,
columns: &Range<u32>,
) -> Option<(Point, Point)> {
self.points_for_rows(&[(row, columns.clone())])
.pop()
.flatten()
}
fn point_for_cursor(
&self,
row: u32,
text: &mut RowText<'a>,
mut cursor: BoundaryCursor,
preceding: Option<(u32, Option<Point>)>,
buffer_cursor: &mut Option<BufferRowCursor<'a>>,
) -> Option<Point> {
let frontier = preceding.map_or(0, |(byte, _)| byte);
let mut backward_buffer_cursor = None;
if let Some(buffer_cursor) = buffer_cursor {
buffer_cursor.reverse = None;
}
loop {
if let Some((byte, point)) = preceding
&& cursor.byte() <= byte
{
return point;
}
let tab_point = TabPoint::new(row, cursor.byte());
let tabs = self.snapshot.tab_snapshot();
let fold_point = tabs.tab_point_to_fold_point(tab_point, Bias::Left).0;
if let Some(range) = self
.snapshot
.fold_snapshot()
.placeholder_range_at(fold_point)
&& range.start < fold_point
{
let start = tabs.fold_point_to_tab_point(range.start);
text.retreat_to(&mut cursor, start.column().max(frontier));
continue;
}
let point = tabs.tab_point_to_point(tab_point, Bias::Left);
let display_point = point.to_display_point(self.snapshot);
let hidden = self.snapshot.is_block_line(display_point.row());
let canonical = tabs.point_to_tab_point(point, Bias::Left);
if !hidden
&& canonical == tab_point
&& self.floor_buffer_point(buffer_cursor, &mut backward_buffer_cursor, point)
== point
{
return Some(point);
}
let preceding = if hidden && canonical == tab_point {
let hidden_start = self.snapshot.display_point_to_fold_point(
DisplayPoint::new(display_point.row(), 0),
Bias::Left,
);
let hidden_start = tabs.fold_point_to_tab_point(hidden_start);
if hidden_start.row() != row {
return None;
}
if hidden_start < tab_point {
hidden_start
} else {
TabPoint::new(row, hidden_start.column().checked_sub(1)?)
}
} else if canonical < tab_point {
canonical
} else {
let previous = if let Some(byte) = point.column.checked_sub(1) {
self.floor_buffer_point(
buffer_cursor,
&mut backward_buffer_cursor,
Point::new(point.row, byte),
)
} else {
let row = point.row.checked_sub(1)?;
Point::new(
row,
self.snapshot
.buffer_snapshot()
.line_len(MultiBufferRow(row)),
)
};
tabs.point_to_tab_point(previous, Bias::Left)
};
if preceding.row() != row {
return None;
}
if preceding < tab_point {
text.retreat_to(&mut cursor, preceding.column().max(frontier));
} else {
text.previous_boundary(&mut cursor)?;
}
}
}
fn floor_buffer_point(
&self,
forward: &mut Option<BufferRowCursor<'a>>,
backward: &mut Option<BufferRowCursor<'a>>,
point: Point,
) -> Point {
if forward.as_ref().is_none_or(|cursor| cursor.row < point.row) {
*backward = forward.replace(self.buffer_row_cursor(point.row));
}
let cursor = if let Some(cursor) = forward.as_mut().filter(|cursor| cursor.row == point.row)
{
cursor
} else {
if backward
.as_ref()
.is_none_or(|cursor| cursor.row != point.row)
{
let cursor = backward.insert(self.buffer_row_cursor(point.row));
cursor.reverse = Some(cursor.text.floor_at(point.column));
}
backward.as_mut().expect("backward buffer row initialized")
};
if cursor.reverse.is_none() {
cursor.text.floor_forward(&mut cursor.forward, point.column);
cursor.reverse = Some(cursor.forward.boundary.clone());
}
let boundary = cursor
.reverse
.as_mut()
.expect("buffer boundary initialized");
cursor.text.retreat_to(boundary, point.column);
Point::new(point.row, boundary.byte())
}
fn buffer_row_cursor(&self, row: u32) -> BufferRowCursor<'a> {
let text = self.buffer_row(row);
let forward = text.forward_cursor();
BufferRowCursor {
row,
text,
forward,
reverse: None,
}
}
fn buffer_row(&self, row: u32) -> RowText<'a> {
let buffer = self.snapshot.buffer_snapshot();
let length = buffer.line_len(MultiBufferRow(row));
let range = Point::new(row, 0)..Point::new(row, length);
let summary = buffer.text_summary_for_range::<MBTextSummary, _>(range.clone());
if summary.len.0 == summary.chars {
RowText::Ascii(length)
} else {
RowText::new(length, buffer.text_for_range(range))
}
}
fn row(&self, row: u32) -> RowText<'a> {
let tabs = self.snapshot.tab_snapshot();
let folds = self.snapshot.fold_snapshot();
let summary = folds.text_summary_for_range(
FoldPoint::new(row, 0)..FoldPoint::new(row, folds.line_len(row)),
);
let length = tabs.line_len(row);
if summary.len.0 == summary.chars {
RowText::Ascii(length)
} else {
let chunks = tabs
.chunks(
TabPoint::new(row, 0)..TabPoint::new(row, length),
LanguageAwareStyling {
tree_sitter: false,
diagnostics: false,
},
Highlights::default(),
)
.map(|chunk| (chunk.text, chunk.is_tab));
RowText::with_tabs(length, chunks)
}
}
}
enum RowText<'a> {
Ascii(u32),
Unicode(RowChunks<'a>),
}
struct RowChunks<'a> {
length: u32,
chunks: Vec<TextChunk<'a>>,
remaining: Box<dyn Iterator<Item = (&'a str, bool)> + 'a>,
#[cfg(test)]
context_bytes: usize,
}
#[derive(Clone, Copy)]
struct TextChunk<'a> {
start: usize,
text: &'a str,
}
#[derive(Clone)]
struct BoundaryCursor {
graphemes: GraphemeCursor,
chunk_index: usize,
}
struct ForwardCursor {
boundary: BoundaryCursor,
next: BoundaryCursor,
column: u32,
}
struct BufferRowCursor<'a> {
row: u32,
text: RowText<'a>,
forward: ForwardCursor,
reverse: Option<BoundaryCursor>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum BoundaryQuery {
Next,
Previous,
Check,
}
impl<'a> RowText<'a> {
fn new(length: u32, chunks: impl Iterator<Item = &'a str> + 'a) -> Self {
Self::with_tabs(length, chunks.map(|text| (text, false)))
}
fn with_tabs(length: u32, chunks: impl Iterator<Item = (&'a str, bool)> + 'a) -> Self {
Self::Unicode(RowChunks {
length,
chunks: Vec::new(),
remaining: Box::new(chunks),
#[cfg(test)]
context_bytes: 0,
})
}
fn cursor(&self, byte: u32) -> BoundaryCursor {
let length = match self {
Self::Ascii(length) => *length,
Self::Unicode(chunks) => chunks.length,
};
BoundaryCursor {
graphemes: GraphemeCursor::new(byte.min(length) as usize, length as usize, true),
chunk_index: 0,
}
}
fn forward_cursor(&self) -> ForwardCursor {
let boundary = self.cursor(0);
ForwardCursor {
next: boundary.clone(),
boundary,
column: 0,
}
}
fn floor_forward(&mut self, cursor: &mut ForwardCursor, byte: u32) {
debug_assert!(byte >= cursor.boundary.byte());
if let Self::Ascii(length) = self {
cursor.column = byte.min(*length);
cursor.boundary.graphemes.set_cursor(cursor.column as usize);
return;
}
while cursor.boundary.byte() < byte {
if cursor.next.byte() == cursor.boundary.byte()
&& self.next_boundary(&mut cursor.next).is_none()
{
break;
}
if cursor.next.byte() > byte {
break;
}
cursor.boundary = cursor.next.clone();
cursor.column += 1;
}
}
#[cfg(test)]
fn columns_for_bytes(&mut self, bytes: Range<u32>) -> Range<u32> {
let mut cursor = self.forward_cursor();
self.floor_forward(&mut cursor, bytes.start);
let start = cursor.column;
self.floor_forward(&mut cursor, bytes.end);
start..cursor.column
}
fn advance_columns(&mut self, cursor: &mut BoundaryCursor, count: u32) {
if let Self::Ascii(length) = self {
cursor
.graphemes
.set_cursor(cursor.byte().saturating_add(count).min(*length) as usize);
} else {
for _ in 0..count {
if self.next_boundary(cursor).is_none() {
break;
}
}
}
}
fn floor_at(&mut self, byte: u32) -> BoundaryCursor {
let mut cursor = self.cursor(byte);
if let Self::Unicode(chunks) = self
&& cursor.byte() > 0
&& cursor.byte() < chunks.length
{
cursor.chunk_index = chunks.locate(cursor.byte() as usize);
let chunk = chunks.chunk(cursor.chunk_index);
let mut offset = cursor.byte() as usize - chunk.start;
while !chunk.text.is_char_boundary(offset) {
offset -= 1;
}
cursor.graphemes.set_cursor(chunk.start + offset);
if chunks.resolve(&mut cursor, BoundaryQuery::Check).is_none() {
chunks
.resolve(&mut cursor, BoundaryQuery::Previous)
.expect("non-boundary has a preceding boundary");
}
}
cursor
}
fn retreat_to(&mut self, cursor: &mut BoundaryCursor, byte: u32) {
if let Self::Ascii(_) = self {
cursor
.graphemes
.set_cursor(cursor.byte().min(byte) as usize);
} else {
while cursor.byte() > byte {
if self.previous_boundary(cursor).is_none() {
break;
}
}
}
}
fn next_boundary(&mut self, cursor: &mut BoundaryCursor) -> Option<u32> {
match self {
Self::Ascii(length) => {
if cursor.byte() >= *length {
return None;
}
cursor.graphemes.set_cursor(cursor.byte() as usize + 1);
Some(cursor.byte())
}
Self::Unicode(chunks) => chunks.resolve(cursor, BoundaryQuery::Next),
}
}
fn previous_boundary(&mut self, cursor: &mut BoundaryCursor) -> Option<u32> {
match self {
Self::Ascii(_) => {
let byte = cursor.byte().checked_sub(1)?;
cursor.graphemes.set_cursor(byte as usize);
Some(byte)
}
Self::Unicode(chunks) => chunks.resolve(cursor, BoundaryQuery::Previous),
}
}
}
impl<'a> RowChunks<'a> {
fn chunk(&mut self, index: usize) -> TextChunk<'a> {
const TAB_CHARACTERS: [u8; 128] = [b'\t'; 128];
while self.chunks.len() <= index {
let (text, is_tab) = self
.remaining
.find(|(text, _)| !text.is_empty())
.expect("row chunks must cover the row");
let text = if is_tab {
std::str::from_utf8(&TAB_CHARACTERS[..text.len()]).expect("tab controls are UTF-8")
} else {
text
};
let start = self
.chunks
.last()
.map_or(0, |chunk| chunk.start + chunk.text.len());
self.chunks.push(TextChunk { start, text });
#[cfg(test)]
tests::ROW_WORK.with(|work| {
let (boundaries, chunks) = work.get();
work.set((boundaries, chunks + 1));
});
}
self.chunks[index]
}
fn locate(&mut self, byte: usize) -> usize {
while self
.chunks
.last()
.is_none_or(|chunk| chunk.start + chunk.text.len() <= byte)
{
self.chunk(self.chunks.len());
}
self.chunks.partition_point(|chunk| chunk.start <= byte) - 1
}
fn resolve(&mut self, cursor: &mut BoundaryCursor, query: BoundaryQuery) -> Option<u32> {
#[cfg(test)]
tests::ROW_WORK.with(|work| {
let (boundaries, chunks) = work.get();
work.set((boundaries + 1, chunks));
});
loop {
let offset = cursor.graphemes.cur_cursor();
if query == BoundaryQuery::Check && (offset == 0 || offset == self.length as usize) {
return Some(offset as u32);
}
if (query == BoundaryQuery::Next && offset == self.length as usize)
|| (query == BoundaryQuery::Previous && offset == 0)
{
return None;
}
let chunk = loop {
let chunk = self.chunk(cursor.chunk_index);
if offset < chunk.start
|| (query == BoundaryQuery::Previous && offset == chunk.start)
{
cursor.chunk_index -= 1;
} else if offset > chunk.start + chunk.text.len()
|| (query != BoundaryQuery::Previous
&& offset == chunk.start + chunk.text.len())
{
cursor.chunk_index += 1;
} else {
break chunk;
}
};
let mut bridge = [0; 8];
let (text, start) =
if query != BoundaryQuery::Previous && offset == chunk.start && offset > 0 {
let previous = self.chunk(cursor.chunk_index - 1);
let previous = previous.text.chars().next_back().expect("nonempty chunk");
let following = chunk.text.chars().next().expect("nonempty chunk");
let previous_length = previous.encode_utf8(&mut bridge).len();
let following_length =
following.encode_utf8(&mut bridge[previous_length..]).len();
(
std::str::from_utf8(&bridge[..previous_length + following_length])
.expect("encoded scalars are UTF-8"),
offset - previous_length,
)
} else {
(chunk.text, chunk.start)
};
loop {
let result = match query {
BoundaryQuery::Next => cursor.graphemes.next_boundary(text, start),
BoundaryQuery::Previous => cursor.graphemes.prev_boundary(text, start),
BoundaryQuery::Check => cursor
.graphemes
.is_boundary(text, start)
.map(|boundary| boundary.then_some(cursor.graphemes.cur_cursor())),
};
match result {
Ok(boundary) => return boundary.map(|byte| byte as u32),
Err(GraphemeIncomplete::PreContext(end)) => {
let index = self.locate(end - 1);
let context = self.chunk(index);
let context_text = &context.text[..end - context.start];
#[cfg(test)]
{
self.context_bytes += context_text.len();
}
cursor
.graphemes
.provide_context(context_text, context.start);
}
Err(GraphemeIncomplete::NextChunk | GraphemeIncomplete::PrevChunk) => break,
Err(error) => unreachable!("row grapheme cursor: {error:?}"),
}
}
}
}
}
impl BoundaryCursor {
fn byte(&self) -> u32 {
self.graphemes.cur_cursor() as u32
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
AddSelectionAbove, AddSelectionBelow, SelectionEffects,
display_map::{
BlockPlacement, BlockProperties, BlockStyle, Crease, DisplayMap, FoldPlaceholder,
},
inlays::Inlay,
test::{editor_test_context::EditorTestContext, test_font},
};
use collections::HashSet;
use gpui::{
AppContext as _, BorrowAppContext as _, Element as _, Entity, Pixels, SharedString,
TestAppContext, div, px,
};
use language::{Buffer, Capability};
use multi_buffer::{Anchor, ExcerptRange, MultiBuffer, MultiBufferOffset, PathKey};
use project::project_settings::DiagnosticSeverity;
use rand::prelude::*;
use settings::SettingsStore;
use std::{cell::Cell, env, iter, num::NonZeroU32, sync::Arc};
use theme::LoadThemes;
use unicode_segmentation::UnicodeSegmentation as _;
thread_local! {
pub(super) static ROW_WORK: Cell<(usize, usize)> = const { Cell::new((0, 0)) };
}
#[gpui::test]
async fn test_columnar_selection_actions_batch_work(cx: &mut TestAppContext) {
crate::editor_tests::init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let rows = [("é.", 3, 2), ("e\u{301}.", 4, 3), ("🦀.", 5, 4)];
for count in [32, 128] {
let text = rows
.iter()
.map(|(unit, _, _)| unit.repeat(count))
.collect::<Vec<_>>()
.join("\n");
for above in [false, true] {
for (nonempty, reversed) in [(false, false), (true, false), (true, true)] {
let indices = if nonempty { 0..count } else { 1..count + 1 };
let expected = |added| {
rows.iter()
.enumerate()
.filter(|(row, _)| {
if above {
*row >= 2 - added
} else {
*row <= added
}
})
.flat_map(|(row, &(_, stride, width))| {
separated_columnar_ranges(
row as u32,
indices.clone(),
stride,
if nonempty { width } else { 0 },
reversed,
)
})
.collect::<Vec<_>>()
};
set_columnar_action_state(&mut cx, &text, &expected(0));
for (direction, added) in [
(above, 1),
(above, 2),
(above, 2),
(!above, 1),
(!above, 0),
(above, 1),
] {
let expected = expected(added);
assert_eq!(expected.len(), (added + 1) * count);
assert_columnar_action_work(&mut cx, direction, &expected, 4 * (2 * count));
}
}
}
}
}
#[gpui::test]
async fn test_columnar_selection_actions_join_skipped_rows_work(cx: &mut TestAppContext) {
crate::editor_tests::init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
for count in [32, 128] {
for above in [false, true] {
let row = |row| if above { 6 - row } else { row };
let mut lines = [
"é.".repeat(count + 1),
"é".to_owned(),
"e\u{301}.".repeat(count / 2 + 1),
String::new(),
"🦀.".to_owned(),
"🦀.".repeat(count + 1),
"e\u{301}.".repeat(count + 1),
];
if above {
lines.reverse();
}
let text = lines.join("\n");
for reversed in [false, true] {
let sources =
separated_columnar_ranges(row(0), count / 2 + 1..count + 1, 3, 2, reversed)
.into_iter()
.chain(separated_columnar_ranges(
row(2),
1..count / 2 + 1,
4,
3,
reversed,
))
.collect::<Vec<_>>();
let expected = |added| {
let mut expected = sources.clone();
for (target, stride, width) in
[(5, 5, 4), (6, 4, 3)].into_iter().take(added)
{
expected.extend(separated_columnar_ranges(
row(target),
1..count + 1,
stride,
width,
reversed,
));
}
expected.sort_unstable_by_key(|range| range.start.min(range.end));
expected
};
set_columnar_action_state(&mut cx, &text, &expected(0));
for (direction, added) in [
(above, 1),
(above, 2),
(above, 2),
(!above, 1),
(!above, 0),
(above, 1),
] {
let expected = expected(added);
assert_eq!(expected.len(), (added + 1) * count);
assert_columnar_action_work(
&mut cx,
direction,
&expected,
5 * (2 * (count + 1)),
);
}
}
}
}
}
#[gpui::test]
async fn test_columnar_selection_actions_skip_replaced_source_rows(cx: &mut TestAppContext) {
crate::editor_tests::init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
for hidden_count in [1, 128] {
let text = iter::repeat_n("éab", hidden_count as usize + 3)
.collect::<Vec<_>>()
.join("\n");
for above in [false, true] {
let row = |row| if above { hidden_count + 2 - row } else { row };
for ends_inside_replacement in [false, true] {
let end_row = hidden_count + u32::from(!ends_inside_replacement);
let source = Point::new(row(0).min(row(end_row)), 2)
..Point::new(row(0).max(row(end_row)), 3);
set_columnar_action_state(&mut cx, &text, &[source]);
let blocks = cx.update_editor(|editor, _, cx| {
let buffer = editor.buffer.read(cx).snapshot(cx);
editor.insert_blocks(
[BlockProperties {
placement: BlockPlacement::Replace(
buffer
.anchor_after(Point::new(row(1).min(row(hidden_count)), 0))
..=buffer.anchor_before(Point::new(
row(1).max(row(hidden_count)),
4,
)),
),
height: Some(1),
style: BlockStyle::Fixed,
render: Arc::new(|_| div().into_any()),
priority: 0,
}],
None,
cx,
)
});
let mut expected = [0, hidden_count + 1, hidden_count + 2]
.into_iter()
.take(if ends_inside_replacement { 2 } else { 3 })
.map(|source_row| {
Point::new(row(source_row), 2)..Point::new(row(source_row), 3)
})
.collect::<Vec<_>>();
expected.sort_unstable_by_key(|range| range.start);
assert_columnar_action_work(&mut cx, above, &expected, 32);
cx.update_editor(|editor, _, cx| {
editor.remove_blocks(blocks.into_iter().collect(), None, cx);
});
}
}
}
}
#[gpui::test]
fn test_columnar_selection_batch_work(cx: &mut TestAppContext) {
init_test(cx);
let length = 4096;
let line = "é".repeat(length);
let snapshot = plain_state(&format!("{line}\n{line}"), cx)
.build_map(cx)
.update(cx, |map, cx| map.snapshot(cx));
let mut query = ColumnarSelectionRows::new(&snapshot);
let mut ranges = (0..length as u32)
.flat_map(|column| {
(0..2)
.map(move |row| Point::new(row, column * 2)..Point::new(row, length as u32 * 2))
})
.collect::<Vec<_>>();
let mut expected_columns = (0..length as u32)
.flat_map(|column| [column..length as u32, column..length as u32])
.collect::<Vec<_>>();
for _ in 0..2 {
ROW_WORK.set((0, 0));
let answers = query.columns_for_ranges(&ranges);
assert_eq!(answers.len(), expected_columns.len());
for (index, (answer, expected)) in answers.iter().zip(&expected_columns).enumerate() {
assert_eq!(answer, expected, "range {index}");
}
let (boundaries, chunks) = ROW_WORK.get();
assert_eq!(boundaries, 2 * length);
assert!(chunks <= 2 * length);
ranges.reverse();
expected_columns.reverse();
}
let requests = (0..length as u32)
.flat_map(|column| (0..2).map(move |row| (row, column..length as u32)))
.collect::<Vec<_>>();
let expected_points = (0..length as u32)
.flat_map(|column| {
(0..2).map(move |row| {
Some((
Point::new(row, column * 2),
Point::new(row, length as u32 * 2),
))
})
})
.collect::<Vec<_>>();
assert_batch_points(&mut query, requests, expected_points, 4 * length);
assert_eq!(query.columns_for_ranges(&[]), Vec::new());
assert_eq!(query.points_for_rows(&[]), Vec::new());
}
#[gpui::test]
fn test_columnar_selection_batch_fold_projection(cx: &mut TestAppContext) {
init_test(cx);
let length = 128;
let prefix = "é".repeat(length);
let indicators = "🇦".repeat(length);
let mut state = plain_state(&format!("{prefix}\n{indicators}\n{indicators}"), cx);
let buffer = state.buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
state.folds = vec![
buffer.anchor_after(Point::new(0, length as u32 * 2))
..buffer.anchor_before(Point::new(1, 4)),
buffer.anchor_after(Point::new(1, length as u32 * 4))
..buffer.anchor_before(Point::new(2, 4)),
];
let snapshot = state.build_map(cx).update(cx, |map, cx| map.snapshot(cx));
let visible_indicators = "🇦".repeat(length - 1);
assert_eq!(
snapshot.tab_snapshot().text(),
format!("{prefix}⋯{visible_indicators}⋯{visible_indicators}")
);
let end_column = length as u32 * 2 + 2;
let end = Point::new(2, length as u32 * 4);
let requests = (0..=end_column)
.flat_map(|column| [(0, column..column), (0, column..end_column)])
.collect::<Vec<_>>();
let expected = (0..=end_column)
.flat_map(|column| {
let point = if column <= length as u32 {
Point::new(0, column * 2)
} else if column <= length as u32 * 3 / 2 {
Point::new(0, length as u32 * 2)
} else if column < end_column {
Point::new(1, length as u32 * 4)
} else {
end
};
[Some((point, point)), Some((point, end))]
})
.collect::<Vec<_>>();
let mut query = ColumnarSelectionRows::new(&snapshot);
assert_batch_points(&mut query, requests, expected, 16 * length);
}
#[gpui::test]
fn test_columnar_selection_batch_rejects_before_end(cx: &mut TestAppContext) {
init_test(cx);
let mut state = plain_state("x\né\n", cx);
let buffer = state.buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
state.inlays.push(Inlay::mock_hint(
0,
buffer.anchor_before(Point::new(1, 0)),
&format!("é{}\n", "a".repeat(1_048_576)),
));
let snapshot = state.build_map(cx).update(cx, |map, cx| map.snapshot(cx));
let mut query = ColumnarSelectionRows::new(&snapshot);
let mut requests = vec![(1, 0..u32::MAX); 4096];
requests.extend([(2, 1..u32::MAX), (0, 2..u32::MAX), (u32::MAX, 0..0)]);
for _ in 0..2 {
ROW_WORK.set((0, 0));
assert_eq!(query.points_for_rows(&requests), vec![None; requests.len()]);
assert_eq!(ROW_WORK.get(), (1, 1));
requests.reverse();
}
}
#[gpui::test]
fn test_columnar_selection_batch_virtual_frontier_work(cx: &mut TestAppContext) {
init_test(cx);
for (folded, length, prefix_length) in [(false, 4096, 1), (true, 64, 1), (true, 4096, 256)]
{
let prefix = "a".repeat(prefix_length);
let placeholder = "é".repeat(length);
let point = Point::new(0, prefix_length as u32);
let mut state = plain_state(&format!("{prefix}xb"), cx);
if !folded {
let buffer = state.buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
state.inlays.push(Inlay::mock_hint(
0,
buffer.anchor_after(point),
&placeholder,
));
}
let map = state.build_map(cx);
if folded {
map.update(cx, |map, cx| {
map.fold(
vec![Crease::simple(
point..Point::new(0, point.column + 1),
FoldPlaceholder {
collapsed_text: Some(SharedString::from(placeholder.clone())),
..FoldPlaceholder::test()
},
)],
cx,
)
});
}
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(
snapshot.tab_snapshot().text(),
if folded {
format!("{prefix}{placeholder}b")
} else {
format!("{prefix}{placeholder}xb")
}
);
let end = point.column + length as u32 - 1;
let requests = (point.column..=end)
.flat_map(|column| [(0, column..column), (0, column..end)])
.collect::<Vec<_>>();
let expected = vec![Some((point, point)); requests.len()];
assert_batch_points(
&mut ColumnarSelectionRows::new(&snapshot),
requests,
expected,
8 * length,
);
}
}
#[gpui::test]
fn test_columnar_selection_batch_frontier_across_original_rows(
cx: &mut TestAppContext,
mut rng: StdRng,
) {
init_test(cx);
let lines = [
"ééé",
"🇦🇶🇦🇶🇦🇶🇦",
"🇦🇶🇦🇶🇦🇶🇦🇶",
"\t\u{301}界",
"\u{600}e\u{301}",
"👩🏽\u{200d}💻",
"क्\u{200d}ष",
"\u{1100}\u{1161}\u{11a8}",
"\u{301}\u{301}",
"a\0\u{200b}b",
"é界🏀",
"✈\u{fe0f}1\u{fe0f}\u{20e3}",
"का",
"",
];
for tab_size in [1, 128] {
set_tab_size(tab_size, cx);
for bias in [None, Some(Bias::Left), Some(Bias::Right)] {
let mut state = plain_state(&lines.join("\n"), cx);
let buffer = state.buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
for (row, line) in lines.iter().enumerate().skip(1) {
let Some(first) = line.chars().next() else {
continue;
};
let point = Point::new(row as u32, first.len_utf8() as u32);
if row % 4 != 0 {
state.folds.push(
buffer.anchor_after(Point::new(
row as u32 - 1,
lines[row - 1].len() as u32,
))..buffer.anchor_before(point),
);
}
if let Some(bias) = bias {
state.inlays.push(Inlay::mock_hint(
row,
buffer.anchor_at(point, bias),
["🇶", "\u{301}", "\t\n\u{600}"][row % 3],
));
}
}
let snapshot = state.build_map(cx).update(cx, |map, cx| map.snapshot(cx));
let expanded = snapshot
.fold_snapshot()
.text()
.split('\n')
.map(|line| plain_row(line, tab_size))
.collect::<Vec<_>>();
assert_eq!(
snapshot.tab_snapshot().text(),
expanded
.iter()
.map(|row| row.text.as_str())
.collect::<Vec<_>>()
.join("\n")
);
let raw = snapshot.buffer_snapshot().text();
let candidates = expanded
.iter()
.enumerate()
.map(|(row, expanded)| {
enumerated_points(&snapshot, &raw, row as u32, &expanded.boundaries)
})
.collect::<Vec<_>>();
let mut requests = expanded
.iter()
.enumerate()
.flat_map(|(row, expanded)| {
(0..=expanded.len + 1)
.chain(iter::once(u32::MAX))
.flat_map(move |start| {
[0, start, start.saturating_add(1), expanded.len, u32::MAX]
.map(|end| (row as u32, start..end))
})
})
.collect::<Vec<_>>();
requests.extend([(u32::MAX, 0..0), (expanded.len() as u32, 0..1)]);
requests.extend_from_within(..);
requests.shuffle(&mut rng);
let mut query = ColumnarSelectionRows::new(&snapshot);
for _ in 0..4 {
let answers = query.points_for_rows(&requests);
assert_eq!(answers.len(), requests.len());
for ((row, columns), answer) in requests.iter().zip(answers) {
let expected = expanded.get(*row as usize).and_then(|expanded| {
nearest_points(columns, expanded.len, &candidates[*row as usize])
});
assert_eq!(
answer, expected,
"tab size {tab_size}, bias {bias:?}, row {row}, columns {columns:?}"
);
}
requests.reverse();
requests = requests.into_iter().step_by(7).collect();
}
let points = lines
.iter()
.enumerate()
.flat_map(|(row, line)| {
scalar_boundaries(line)
.into_iter()
.map(move |byte| Point::new(row as u32, byte as u32))
})
.collect::<Vec<_>>();
let mut ranges = points
.iter()
.flat_map(|start| points.iter().map(move |end| *start..*end))
.collect::<Vec<_>>();
ranges.extend_from_within(..);
ranges.shuffle(&mut rng);
let answers = query.columns_for_ranges(&ranges);
assert_eq!(answers.len(), ranges.len());
for (range, answer) in ranges.iter().zip(answers) {
let column = |point| {
let tab = snapshot
.tab_snapshot()
.point_to_tab_point(point, Bias::Left);
expanded[tab.row() as usize]
.boundaries
.partition_point(|byte| *byte <= tab.column() as usize)
as u32
- 1
};
let start = column(range.start);
let end = column(range.end);
assert_eq!(
answer,
start.min(end)..start.max(end),
"tab size {tab_size}, bias {bias:?}, source {range:?}"
);
}
}
}
}
#[test]
fn test_column_boundaries_across_chunks() {
for (text, expected) in [
("", vec![0]),
("abc", vec![0, 1, 2, 3]),
("e\u{301}x", vec![0, 3, 4]),
("\u{301}\u{301}x", vec![0, 4, 5]),
("👩\u{200d}💻x", vec![0, 11, 12]),
("🇦🇶🇦🇶🇦x", vec![0, 8, 16, 20, 21]),
("क्\u{200d}षx", vec![0, 12, 13]),
("\r\nx", vec![0, 2, 3]),
("\u{600}🇦🇶🇦x", vec![0, 10, 14, 15]),
("🇦\u{301}🇶🇦x", vec![0, 6, 14, 15]),
] {
for split in scalar_boundaries(text) {
let mut query = RowText::new(
text.len() as u32,
["", &text[..split], "", &text[split..], ""].into_iter(),
);
assert_row_boundaries(&mut query, &expected);
}
let boundaries = scalar_boundaries(text);
let mut query = RowText::new(
text.len() as u32,
boundaries.windows(2).map(|range| &text[range[0]..range[1]]),
);
assert_row_boundaries(&mut query, &expected);
for start in 0..=text.len() {
for end in start..=text.len() {
assert_eq!(
query.columns_for_bytes(start as u32..end as u32),
(expected.partition_point(|boundary| *boundary <= start) - 1) as u32
..(expected.partition_point(|boundary| *boundary <= end) - 1) as u32,
"{text:?}, range {start}..{end}"
);
}
}
}
}
#[test]
fn test_column_boundaries_tab_chunks() {
let prefix = "\u{600}";
let suffix = "\u{301}e\u{301}👩\u{200d}💻🇦🇶\0\u{200b}x";
for width in [1, 4, 128] {
let spaces = " ".repeat(width);
let suffix_start = prefix.len() + width;
let len = suffix_start + suffix.len();
let expected = iter::once(0)
.chain(prefix.len()..=suffix_start)
.chain([2, 5, 16, 24, 25, 28, 29].map(|offset| suffix_start + offset))
.collect::<Vec<_>>();
for tab_split in 0..=width {
for split in scalar_boundaries(suffix) {
let mut query = RowText::with_tabs(
len as u32,
[
(prefix, false),
(&spaces[..tab_split], true),
("", false),
(&spaces[tab_split..], true),
(&suffix[..split], false),
("", true),
(&suffix[split..], false),
]
.into_iter(),
);
assert_row_boundaries(&mut query, &expected);
}
}
}
}
#[test]
fn test_column_boundaries_tab_before_large_grapheme() {
let suffix = format!("{}z", "\u{301}".repeat(1_048_576));
let mut query = RowText::with_tabs(
4 + suffix.len() as u32,
[(" ", true), (suffix.as_str(), false)].into_iter(),
);
let mut cursor = query.cursor(0);
query.advance_columns(&mut cursor, 4);
assert_eq!(cursor.byte(), 4);
let RowText::Unicode(chunks) = &query else {
panic!("Unicode row")
};
assert_eq!(chunks.chunks.len(), 2);
assert_eq!(chunks.chunks[1].text.as_ptr(), suffix.as_ptr());
assert_eq!(chunks.context_bytes, 0);
assert_eq!(query.next_boundary(&mut cursor), Some(2_097_156));
assert_eq!(query.next_boundary(&mut cursor), Some(2_097_157));
assert_eq!(query.columns_for_bytes(2_097_155..2_097_155), 4..4);
query.retreat_to(&mut cursor, 2_097_155);
assert_eq!(cursor.byte(), 4);
}
#[test]
fn test_row_cursor_forks_and_reverse_floors() {
let text = "🇦🇶🇦🇶🇦e\u{301}👩\u{200d}💻क्\u{200d}षz";
let expected = [0, 8, 16, 20, 23, 34, 46, 47];
let boundaries = scalar_boundaries(text);
let mut query = RowText::new(
text.len() as u32,
boundaries.windows(2).map(|range| &text[range[0]..range[1]]),
);
assert_row_boundaries(&mut query, &expected);
let mut forward = query.cursor(0);
for pair in expected.windows(2) {
assert_eq!(forward.byte(), pair[0] as u32);
let mut backward = forward.clone();
for byte in (0..pair[0]).rev() {
query.retreat_to(&mut backward, byte as u32);
let index = expected.partition_point(|boundary| *boundary <= byte) - 1;
assert_eq!(backward.byte(), expected[index] as u32);
}
assert_eq!(query.next_boundary(&mut forward), Some(pair[1] as u32));
assert_eq!(backward.byte(), 0);
}
}
#[test]
fn test_row_cursor_giant_split_clusters() {
for text in [
format!("e{}z", "\u{301}".repeat(4096)),
format!("👩{}\u{200d}💻z", "\u{301}".repeat(4096)),
format!("क{}षz", "्\u{200d}".repeat(4096)),
] {
let boundaries = scalar_boundaries(&text);
let mut query = RowText::new(
text.len() as u32,
boundaries.windows(2).map(|range| &text[range[0]..range[1]]),
);
let end = text.len() as u32;
let mut cursor = query.cursor(0);
assert_eq!(query.next_boundary(&mut cursor), Some(end - 1));
let mut fork = cursor.clone();
assert_eq!(query.next_boundary(&mut cursor), Some(end));
assert_eq!(query.previous_boundary(&mut fork), Some(0));
for byte in [1, end / 2, end - 2] {
assert_eq!(query.floor_at(byte).byte(), 0);
}
assert_eq!(query.floor_at(end - 1).byte(), end - 1);
assert_eq!(query.previous_boundary(&mut cursor), Some(end - 1));
assert_eq!(query.previous_boundary(&mut cursor), Some(0));
let RowText::Unicode(chunks) = &query else {
panic!("Unicode row")
};
assert_eq!(chunks.chunks.len(), boundaries.len() - 1);
assert_eq!(chunks.chunks[0].text.as_ptr(), text.as_ptr());
}
}
#[test]
fn test_row_cursor_lazy_polling() {
let suffix = format!("{}é", "a".repeat(1_048_576));
let polls = Cell::new(0);
let chunks = ["éabc", suffix.as_str()].into_iter().inspect(|_| {
polls.set(polls.get() + 1);
});
let mut query = RowText::new(5 + suffix.len() as u32, chunks);
assert_eq!(query.columns_for_bytes(0..0), 0..0);
assert_eq!(query.floor_at(0).byte(), 0);
let mut cursor = query.cursor(0);
query.advance_columns(&mut cursor, 0);
assert_eq!(polls.get(), 0);
query.advance_columns(&mut cursor, 2);
assert_eq!(cursor.byte(), 3);
assert_eq!(polls.get(), 1);
let mut fork = cursor.clone();
query.retreat_to(&mut fork, 1);
assert_eq!(fork.byte(), 0);
assert_eq!(query.next_boundary(&mut cursor), Some(4));
assert_eq!(polls.get(), 1);
assert_eq!(query.columns_for_bytes(0..2), 0..1);
assert_eq!(polls.get(), 1);
let RowText::Unicode(chunks) = &query else {
panic!("Unicode row")
};
assert_eq!(chunks.chunks.len(), 1);
assert_eq!(chunks.context_bytes, 0);
}
#[test]
fn test_row_cursor_reverse_regional_context_is_linear() {
for symbols in [127, 128, 1023, 1024, 8191, 8192] {
let text = "🇦".repeat(symbols);
for chunk_symbols in [1, 2, 7, symbols] {
for end in [text.len(), text.len() - 1, text.len() - 4] {
let chunks = text.as_bytes().chunks(4 * chunk_symbols).map(|chunk| {
std::str::from_utf8(chunk).expect("regional indicator chunks")
});
let mut query = RowText::new(text.len() as u32, chunks);
let mut cursor = query.floor_at(end as u32);
assert_eq!(
cursor.byte(),
if end == text.len() { end } else { end / 8 * 8 } as u32
);
let mut expected = cursor.byte();
while expected > 0 {
expected = (expected - 1) / 8 * 8;
assert_eq!(query.previous_boundary(&mut cursor), Some(expected));
}
assert_eq!(query.previous_boundary(&mut cursor), None);
let RowText::Unicode(chunks) = &query else {
panic!("Unicode row")
};
if chunk_symbols == 1 {
assert!(chunks.context_bytes > 0);
}
assert!(
chunks.context_bytes <= text.len() * 2,
"symbols {symbols}, chunk symbols {chunk_symbols}, end {end}, context bytes {}",
chunks.context_bytes
);
}
}
}
}
#[gpui::test]
fn test_columnar_selection_right_inlay_shifts_regional_parity(cx: &mut TestAppContext) {
init_test(cx);
for symbols in [4, 64, 8192] {
let text = format!("{}z", "🇦".repeat(symbols));
let mut state = plain_state(&text, cx);
let buffer = state.buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
state
.inlays
.push(Inlay::mock_hint(0, buffer.anchor_after(Point::zero()), "🇶"));
let snapshot = state.build_map(cx).update(cx, |map, cx| map.snapshot(cx));
assert_eq!(snapshot.tab_snapshot().text(), format!("🇶{text}"));
let mut query = ColumnarSelectionRows::new(&snapshot);
let last_flag = symbols as u32 / 2;
let requests = (1..=last_flag)
.flat_map(|column| [(0, column..column), (0, column..last_flag + 1)])
.collect::<Vec<_>>();
let end = Point::new(0, symbols as u32 * 4);
let expected = (1..=last_flag)
.flat_map(|_| {
[
Some((Point::zero(), Point::zero())),
Some((Point::zero(), end)),
]
})
.collect::<Vec<_>>();
assert_batch_points(&mut query, requests, expected, 8 * symbols);
assert_eq!(
query.points_for_row(0, &(last_flag..last_flag + 1)),
Some((Point::zero(), end))
);
assert_eq!(
query.points_for_row(0, &(last_flag + 1..last_flag + 1)),
Some((end, end))
);
}
}
#[gpui::test]
fn test_columnar_selection_tab_control_boundaries(cx: &mut TestAppContext) {
init_test(cx);
let expected = plain_row("\t\u{301}x", 4);
assert_eq!(expected.text, " \u{301}x");
assert_eq!(expected.len, 6);
assert_eq!(expected.columns, [(0, 0), (1, 4), (3, 5), (4, 6)]);
assert_eq!(expected.points, [(0, 0), (4, 1), (5, 3), (6, 4)]);
assert_eq!(expected.boundaries, [0, 1, 2, 3, 4, 6, 7]);
for tab_size in [1, 2, 4, 8, 127, 128] {
set_tab_size(tab_size, cx);
let after_prepend_tab = 1 + tab_size - 1 % tab_size;
for (line, endpoints) in [
(
"\t\u{301}x",
vec![(0, 0), (1, tab_size), (3, tab_size + 1), (4, tab_size + 2)],
),
(
"\u{600}\t\u{301}x",
vec![
(0, 0),
(2, 1),
(3, after_prepend_tab),
(5, after_prepend_tab + 1),
(6, after_prepend_tab + 2),
],
),
] {
let snapshot = plain_state(&format!("{line}\n{line}"), cx)
.build_map(cx)
.update(cx, |map, cx| map.snapshot(cx));
let mut query = ColumnarSelectionRows::new(&snapshot);
for source_row in [0, 1] {
for (index, &(start, start_column)) in endpoints.iter().enumerate() {
for &(end, end_column) in &endpoints[index..] {
for reversed in [false, true] {
let start_point = Point::new(source_row, start);
let end_point = Point::new(source_row, end);
let columns = query.columns_for_range(if reversed {
end_point..start_point
} else {
start_point..end_point
});
assert_eq!(columns, start_column..end_column);
let target_row = 1 - source_row;
assert_eq!(
query.points_for_row(target_row, &columns),
Some((
Point::new(target_row, start),
Point::new(target_row, end)
)),
"line {line:?}, tab size {tab_size}, source row {source_row}, range {start}..{end}, reversed {reversed}"
);
}
}
}
}
assert_plain_rows(&snapshot, tab_size);
}
}
}
#[gpui::test]
fn test_columnar_selection_tab_control_boundaries_with_virtual_text(cx: &mut TestAppContext) {
init_test(cx);
let mut state = plain_state("\tQx\n\tx", cx);
let buffer = state.buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
state.inlays.push(Inlay::mock_hint(
0,
buffer.anchor_after(Point::new(1, 1)),
"\u{301}",
));
let map = state.build_map(cx);
map.update(cx, |map, cx| {
map.fold(
vec![Crease::simple(
Point::new(0, 1)..Point::new(0, 2),
FoldPlaceholder {
collapsed_text: Some(SharedString::from("\u{301}")),
..FoldPlaceholder::test()
},
)],
cx,
)
});
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(snapshot.tab_snapshot().text(), " \u{301}x\n \u{301}x");
let mut query = ColumnarSelectionRows::new(&snapshot);
for row in [0, 1] {
assert_eq!(
query.columns_for_range(Point::new(row, 1)..Point::new(row, 1)),
4..4
);
assert_eq!(
query.points_for_row(row, &(4..4)),
Some((Point::new(row, 1), Point::new(row, 1)))
);
assert_eq!(
query.points_for_row(row, &(4..5)),
Some((Point::new(row, 1), Point::new(row, 2 - row)))
);
}
}
#[gpui::test]
fn test_columnar_selection_lazy_unicode_prefix(cx: &mut TestAppContext) {
init_test(cx);
let line = format!("{}é", "a".repeat(1_048_576));
let snapshot = plain_state(&format!("{line}\n{line}"), cx)
.build_map(cx)
.update(cx, |map, cx| map.snapshot(cx));
let mut query = ColumnarSelectionRows::new(&snapshot);
assert_eq!(query.columns_for_range(Point::zero()..Point::zero()), 0..0);
assert_eq!(
query.points_for_row(1, &(0..0)),
Some((Point::new(1, 0), Point::new(1, 0)))
);
for row in [query.row(1), query.buffer_row(1)] {
let RowText::Unicode(chunks) = row else {
panic!("Unicode row")
};
assert_eq!(chunks.chunks.len(), 0);
}
assert_eq!(
query.points_for_row(1, &(1..1)),
Some((Point::new(1, 1), Point::new(1, 1)))
);
for mut row in [query.row(1), query.buffer_row(1)] {
let mut cursor = row.cursor(0);
row.advance_columns(&mut cursor, 1);
assert_eq!(cursor.byte(), 1);
let RowText::Unicode(chunks) = row else {
panic!("Unicode row")
};
assert_eq!(chunks.chunks.len(), 1);
}
assert_eq!(
query.points_for_row(1, &(1_048_576..1_048_576)),
Some((Point::new(1, 1_048_576), Point::new(1, 1_048_576)))
);
}
#[gpui::test]
fn test_columnar_selection_skips_long_virtual_row(cx: &mut TestAppContext) {
init_test(cx);
let mut state = plain_state(&format!("x\n{}", "a".repeat(1_048_576)), cx);
let buffer = state.buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
state.inlays.push(Inlay::mock_hint(
0,
buffer.anchor_before(Point::new(1, 0)),
&format!("{}\n", "a".repeat(1_048_576)),
));
let snapshot = state.build_map(cx).update(cx, |map, cx| map.snapshot(cx));
let mut query = ColumnarSelectionRows::new(&snapshot);
assert_eq!(query.points_for_row(1, &(1_048_576..1_048_576)), None);
assert_eq!(
query.points_for_row(2, &(1_048_576..1_048_576)),
Some((Point::new(1, 1_048_576), Point::new(1, 1_048_576)))
);
}
#[gpui::test]
fn test_columnar_selection_graphemes_and_edges(cx: &mut TestAppContext) {
init_test(cx);
let state = plain_state("e\u{301}\néé\n\n\t\u{301}x\n👩\u{200d}💻z\n", cx);
let map = state.build_map(cx);
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
let mut query = ColumnarSelectionRows::new(&snapshot);
assert_eq!(
query.columns_for_range(Point::new(0, 0)..Point::new(0, 3)),
0..1
);
assert_eq!(
query.columns_for_range(Point::new(1, 0)..Point::new(1, 4)),
0..2
);
assert_eq!(
query.columns_for_range(Point::new(1, 4)..Point::new(0, 0)),
0..2
);
assert_eq!(
query.points_for_row(0, &(0..1)),
Some((Point::new(0, 0), Point::new(0, 3)))
);
assert_eq!(
query.points_for_row(1, &(0..1)),
Some((Point::new(1, 0), Point::new(1, 2)))
);
assert_eq!(query.points_for_row(0, &(1..2)), None);
assert_eq!(
query.points_for_row(0, &(u32::MAX..u32::MAX)),
Some((Point::new(0, 3), Point::new(0, 3)))
);
assert_eq!(
query.points_for_row(2, &(0..0)),
Some((Point::new(2, 0), Point::new(2, 0)))
);
assert_eq!(query.points_for_row(2, &(0..1)), None);
assert_eq!(
query.points_for_row(3, &(3..4)),
Some((Point::new(3, 0), Point::new(3, 1)))
);
assert_eq!(
query.points_for_row(4, &(0..1)),
Some((Point::new(4, 0), Point::new(4, 11)))
);
assert_eq!(
query.points_for_row(5, &(u32::MAX..u32::MAX)),
Some((Point::new(5, 0), Point::new(5, 0)))
);
assert_eq!(query.points_for_row(6, &(0..0)), None);
assert_eq!(query.points_for_row(u32::MAX, &(0..0)), None);
assert_plain_rows(&snapshot, 4);
let empty = plain_state("", cx)
.build_map(cx)
.update(cx, |map, cx| map.snapshot(cx));
assert_plain_rows(&empty, 4);
}
#[gpui::test]
fn test_columnar_selection_ascii_fast_path(cx: &mut TestAppContext) {
init_test(cx);
let state = plain_state(&format!("{}\nx", "a".repeat(1_048_576)), cx);
let map = state.build_map(cx);
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
let mut query = ColumnarSelectionRows::new(&snapshot);
assert_eq!(query.points_for_row(1, &(2..3)), None);
assert_eq!(size_of_val(&query), size_of::<&DisplaySnapshot>());
assert_eq!(
query.columns_for_range(Point::zero()..Point::new(0, 1_048_576)),
0..1_048_576
);
let RowText::Ascii(len) = query.row(0) else {
panic!("ASCII row allocated Unicode boundaries")
};
assert_eq!(len, 1_048_576);
assert_eq!(
query.points_for_row(0, &(1_048_575..u32::MAX)),
Some((Point::new(0, 1_048_575), Point::new(0, 1_048_576)))
);
let RowText::Ascii(len) = query.buffer_row(0) else {
panic!("ASCII buffer row allocated Unicode boundaries")
};
assert_eq!(len, 1_048_576);
}
#[gpui::test]
fn test_columnar_selection_tab_expansion_boundary(cx: &mut TestAppContext) {
init_test(cx);
let prefixes = (255..=257)
.map(|column| format!("{}{}", "α".repeat(127), "a".repeat(column - 254)))
.chain([
format!("{}α", "a".repeat(255)),
format!("{}e\u{301}", "a".repeat(254)),
])
.collect::<Vec<_>>();
let text = prefixes
.iter()
.map(|prefix| format!("{prefix}\t\u{301}β"))
.collect::<Vec<_>>()
.join("\n");
let buffer = cx.new(|cx| Buffer::local(text, cx));
let state = MapState::new(cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)));
let map = state.build_map(cx);
for tab_size in [1, 3, 128] {
set_tab_size(tab_size, cx);
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
assert_plain_rows(&snapshot, tab_size);
if tab_size == 128 {
assert_eq!(
snapshot.tab_snapshot().text(),
format!(
"{}{}\u{301}β\n{} \u{301}β\n{} \u{301}β\n{} \u{301}β\n{} \u{301}β",
prefixes[0],
" ".repeat(128),
prefixes[1],
prefixes[2],
prefixes[3],
prefixes[4]
)
);
}
buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "a")], None, cx));
assert_plain_rows(&map.update(cx, |map, cx| map.snapshot(cx)), tab_size);
buffer.update(cx, |buffer, cx| buffer.edit([(0..1, "")], None, cx));
let restored = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(
restored.tab_snapshot().text(),
snapshot.tab_snapshot().text()
);
assert_plain_rows(&restored, tab_size);
}
}
#[gpui::test]
fn test_columnar_selection_folds_and_inlays_without_frames(
cx: &mut TestAppContext,
mut rng: StdRng,
) {
init_test(cx);
let mut state = plain_state("e\u{301}x\nab\ncd", cx);
let map = state.build_map(cx);
map.update(cx, |map, cx| {
map.fold(
vec![Crease::simple(
Point::new(0, 1)..Point::new(0, 3),
FoldPlaceholder::test(),
)],
cx,
)
});
let folded = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(folded.tab_snapshot().text(), "e⋯x\nab\ncd");
assert_eq!(
ColumnarSelectionRows::new(&folded).points_for_row(0, &(1..2)),
Some((Point::new(0, 0), Point::new(0, 3)))
);
state.folds = folded
.folds_in_range(Anchor::Min..Anchor::Max)
.map(|fold| fold.range.0.clone())
.collect();
for (index, bias) in [Bias::Left, Bias::Right].into_iter().enumerate() {
state.inlays.push(Inlay::mock_hint(
index,
folded.buffer_snapshot().anchor_at(Point::new(1, 1), bias),
"\u{301}\n\tR",
));
}
map.update(cx, |map, cx| {
map.splice_inlays(&[], state.inlays.clone(), cx)
});
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(
snapshot.tab_snapshot().text(),
"e⋯x\na\u{301}\n R\u{301}\n Rb\ncd"
);
let mut query = ColumnarSelectionRows::new(&snapshot);
assert_eq!(query.points_for_row(2, &(0..0)), None);
assert_eq!(query.points_for_row(2, &(0..u32::MAX)), None);
let rebuilt = state.build_map(cx).update(cx, |map, cx| map.snapshot(cx));
check_snapshot(&snapshot, &rebuilt, true, None, 4, &mut rng, 0);
}
#[gpui::test]
fn test_columnar_selection_long_fold_placeholders(cx: &mut TestAppContext) {
init_test(cx);
for placeholder in ["...", "long placeholder", "ééé", "👩\u{200d}💻xyz"] {
for edited in [false, true] {
let buffer = cx.new(|cx| Buffer::local(if edited { "a\nxb" } else { "axb" }, cx));
let state = MapState::new(cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)));
let map = state.build_map(cx);
map.update(cx, |map, cx| {
map.fold(
vec![Crease::simple(
Point::new(0, 1)..if edited {
Point::new(1, 1)
} else {
Point::new(0, 2)
},
FoldPlaceholder {
collapsed_text: Some(SharedString::from(placeholder)),
..FoldPlaceholder::test()
},
)],
cx,
);
});
if edited {
buffer.update(cx, |buffer, cx| {
buffer.edit([(Point::new(0, 1)..Point::new(1, 0), "")], None, cx)
});
}
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(snapshot.tab_snapshot().text(), format!("a{placeholder}b"));
let mut query = ColumnarSelectionRows::new(&snapshot);
let end = 1 + placeholder.graphemes(true).count() as u32;
for column in 1..end {
assert_eq!(
query.points_for_row(0, &(column..column)),
Some((Point::new(0, 1), Point::new(0, 1))),
"{placeholder:?}, column {column}, edited {edited}"
);
}
assert_eq!(
query.points_for_row(0, &(end..end)),
Some((Point::new(0, 2), Point::new(0, 2)))
);
}
}
}
#[gpui::test]
fn test_columnar_selection_fold_grapheme_edges(cx: &mut TestAppContext) {
init_test(cx);
for (text, placeholder, column, expected) in
[("axb", "\u{301}..", 1, 0), ("ax\u{301}b", "..", 3, 4)]
{
let state = plain_state(text, cx);
let map = state.build_map(cx);
map.update(cx, |map, cx| {
map.fold(
vec![Crease::simple(
Point::new(0, 1)..Point::new(0, 2),
FoldPlaceholder {
collapsed_text: Some(SharedString::from(placeholder)),
..FoldPlaceholder::test()
},
)],
cx,
)
});
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(
ColumnarSelectionRows::new(&snapshot).points_for_row(0, &(column..column)),
Some((Point::new(0, expected), Point::new(0, expected))),
"{text:?}, {placeholder:?}"
);
}
for merge in [false, true] {
let map = plain_state("axyb", cx).build_map(cx);
map.update(cx, |map, cx| {
map.fold(
[(1, ".."), (2, "ZZ")]
.into_iter()
.map(|(start, placeholder)| {
Crease::simple(
Point::new(0, start)..Point::new(0, start + 1),
FoldPlaceholder {
merge_adjacent: merge,
collapsed_text: Some(SharedString::from(placeholder)),
..FoldPlaceholder::test()
},
)
})
.collect(),
cx,
)
});
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(
snapshot.tab_snapshot().text(),
if merge { "a..b" } else { "a..ZZb" }
);
let expected = if merge {
vec![0, 1, 1, 3, 4]
} else {
vec![0, 1, 1, 2, 2, 3, 4]
};
let mut query = ColumnarSelectionRows::new(&snapshot);
for (column, byte) in expected.into_iter().enumerate() {
let column = column as u32;
assert_eq!(
query.points_for_row(0, &(column..column)),
Some((Point::new(0, byte), Point::new(0, byte))),
"merge={merge}, column={column}"
);
}
}
}
#[gpui::test]
fn test_columnar_selection_fold_ending_inside_grapheme(cx: &mut TestAppContext) {
init_test(cx);
let map = plain_state("aX\u{1100}\u{1161}z", cx).build_map(cx);
map.update(cx, |map, cx| {
map.fold(
vec![Crease::simple(
Point::new(0, 1)..Point::new(0, 5),
FoldPlaceholder::test(),
)],
cx,
)
});
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(snapshot.tab_snapshot().text(), "a⋯\u{1161}z");
let mut query = ColumnarSelectionRows::new(&snapshot);
for (columns, expected) in [
(0..0, (Point::new(0, 0), Point::new(0, 0))),
(1..1, (Point::new(0, 1), Point::new(0, 1))),
(2..2, (Point::new(0, 1), Point::new(0, 1))),
(3..3, (Point::new(0, 8), Point::new(0, 8))),
(4..4, (Point::new(0, 9), Point::new(0, 9))),
(1..3, (Point::new(0, 1), Point::new(0, 8))),
(2..4, (Point::new(0, 1), Point::new(0, 9))),
] {
assert_eq!(
query.points_for_row(0, &columns),
Some(expected),
"columns {columns:?}"
);
}
}
#[test]
fn test_column_boundaries_large_grapheme() {
let text = format!("e{}z", "\u{301}".repeat(1024));
let mut query = RowText::new(text.len() as u32, iter::once(text.as_str()));
let mut cursor = query.cursor(0);
assert_eq!(cursor.byte(), 0);
assert_eq!(query.next_boundary(&mut cursor), Some(2049));
assert_eq!(query.next_boundary(&mut cursor), Some(2050));
assert_eq!(query.columns_for_bytes(2048..2049), 0..1);
assert_eq!(query.floor_at(2048).byte(), 0);
assert_eq!(query.floor_at(2049).byte(), 2049);
}
#[gpui::test(iterations = 20)]
fn test_columnar_selection_during_pending_rewrap(cx: &mut TestAppContext, mut rng: StdRng) {
init_test(cx);
cx.background_executor.set_block_on_ticks(0..=0);
let text = (0..120)
.map(|row| format!("row{row} {}", random_text(&mut rng, 6)))
.collect::<Vec<_>>()
.join("\n");
let mut state = plain_state(&text, cx);
let map = state.build_map(cx);
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
let buffer = snapshot.buffer_snapshot();
let hidden_start = rng.random_range(0..buffer.max_point().row);
let hidden_end = rng.random_range(hidden_start..=buffer.max_point().row);
state.block = Some((
BlockPlacement::Replace(
buffer.anchor_before(Point::new(hidden_start, 0))
..=buffer.anchor_after(Point::new(
hidden_end,
buffer.line_len(MultiBufferRow(hidden_end)),
)),
),
1,
));
map.update(cx, |map, cx| {
map.insert_blocks([state.block_properties().expect("block")], cx)
});
state.wrap_width = Some(px(30.0));
map.update(cx, |map, cx| map.set_wrap_width(state.wrap_width, cx));
assert!(map.update(cx, |map, cx| map.is_rewrapping(cx)));
let interpolated = map.update(cx, |map, cx| map.snapshot(cx));
assert!(map.update(cx, |map, cx| map.is_rewrapping(cx)));
check_snapshot(&interpolated, &interpolated, true, None, 4, &mut rng, 0);
let rebuilt = state.build_map(cx);
cx.run_until_parked();
assert!(!map.update(cx, |map, cx| map.is_rewrapping(cx)));
let rebuilt = rebuilt.update(cx, |map, cx| map.snapshot(cx));
let settled = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(hidden_rows(&settled), hidden_rows(&rebuilt));
check_snapshot(&settled, &rebuilt, true, None, 4, &mut rng, 1);
check_snapshot(
&interpolated,
&settled,
hidden_rows(&interpolated) == hidden_rows(&settled),
None,
4,
&mut rng,
2,
);
}
#[gpui::test(iterations = 100)]
fn test_random_columnar_selections(cx: &mut TestAppContext, mut rng: StdRng) {
init_test(cx);
cx.background_executor.set_block_on_ticks(0..=50);
let operations = env::var("OPERATIONS")
.map(|value| value.parse::<usize>().expect("invalid OPERATIONS"))
.unwrap_or(15);
let mut texts = (0..rng.random_range(1..=2))
.map(|_| format!("a{}z", random_text(&mut rng, 12)))
.collect::<Vec<_>>();
let buffers = texts
.iter()
.map(|text| cx.new(|cx| Buffer::local(text.clone(), cx)))
.collect::<Vec<_>>();
let multibuffer = cx.new(|cx| {
if buffers.len() == 1 {
return MultiBuffer::singleton(buffers[0].clone(), cx);
}
let mut buffer = MultiBuffer::new(Capability::ReadWrite);
for (index, source) in buffers.iter().enumerate() {
let snapshot = source.read(cx).snapshot();
buffer.set_excerpt_ranges_for_path(
PathKey::sorted(index as u64),
source.clone(),
&snapshot,
vec![ExcerptRange::new(Point::zero()..snapshot.max_point())],
cx,
);
}
buffer
});
let mut state = MapState::new(multibuffer);
let map = state.build_map(cx);
let mut block_ids = HashSet::default();
let mut kinds = [0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6];
kinds.shuffle(&mut rng);
let mut snapshot = map.update(cx, |map, cx| map.snapshot(cx));
check_snapshot(
&snapshot,
&snapshot,
true,
Some(&texts.join("\n")),
4,
&mut rng,
0,
);
for operation in 0..operations {
let kind = kinds[operation % kinds.len()];
log::info!(
"operation {operation}: kind {kind}, texts {texts:?}, tabs {}, folds {:?}, inlays {:?}, block {:?}, wrap {:?}",
state.tab_size,
state.folds,
state.inlays,
state.block,
state.wrap_width
);
let replaced_before = state.replaces_rows();
match kind {
0 => {
let index = rng.random_range(0..buffers.len());
let text = &mut texts[index];
let boundaries = text
.char_indices()
.map(|(offset, _)| offset)
.skip(1)
.collect::<Vec<_>>();
let start = rng.random_range(0..boundaries.len());
let mut range =
boundaries[start]..boundaries[rng.random_range(start..boundaries.len())];
let mut replacement = random_text(&mut rng, 4);
if text.get(range.clone()) == Some(replacement.as_str()) {
replacement.push('a');
}
let mut expected = text.clone();
expected.replace_range(range.clone(), &replacement);
if expected.len() > 256
|| expected.matches('\t').count() > 4
|| expected.matches('\n').count() > 12
{
range = 1..text.len() - 1;
replacement.clear();
expected = String::from("az");
}
assert_ne!(expected.as_str(), text.as_str());
log::info!("edit buffer {index}: {range:?} -> {replacement:?}");
buffers[index].update(cx, |buffer, cx| {
buffer.edit([(range, replacement)], None, cx)
});
*text = expected;
if state.replaces_rows() {
map.update(cx, |map, cx| {
map.remove_blocks(std::mem::take(&mut block_ids), cx);
block_ids.extend(
map.insert_blocks([state.block_properties().expect("block")], cx),
);
});
}
}
1 => {
let size = rng.random_range(1..128);
state.tab_size = size + u32::from(size >= state.tab_size);
set_tab_size(state.tab_size, cx);
}
2 => {
if state.folds.is_empty() {
let index = rng.random_range(0..texts.len());
let base = texts
.iter()
.take(index)
.map(|text| text.len() + 1)
.sum::<usize>();
let boundaries = scalar_boundaries(&texts[index]);
let start = rng.random_range(0..boundaries.len() - 1);
let end = rng.random_range(start + 1..boundaries.len());
let buffer = snapshot.buffer_snapshot();
let range = buffer.anchor_after(MultiBufferOffset(base + boundaries[start]))
..buffer.anchor_before(MultiBufferOffset(base + boundaries[end]));
map.update(cx, |map, cx| {
map.fold(vec![Crease::simple(range, FoldPlaceholder::test())], cx)
});
} else {
map.update(cx, |map, cx| {
map.unfold_intersecting([Anchor::Min..Anchor::Max], true, cx)
});
if state.replaces_rows() {
block_ids.clear();
state.block = None;
}
}
}
3 | 6 => {
if state.inlays.is_empty() {
let text = snapshot.buffer_snapshot().text();
let boundaries = scalar_boundaries(&text);
let offset =
MultiBufferOffset(boundaries[rng.random_range(0..boundaries.len())]);
for (index, bias) in [Bias::Left, Bias::Right].into_iter().enumerate() {
let text = format!("\n{}\t", random_text(&mut rng, 3));
state.inlays.push(Inlay::mock_hint(
index,
snapshot.buffer_snapshot().anchor_at(offset, bias),
text.as_str(),
));
}
map.update(cx, |map, cx| {
map.splice_inlays(&[], state.inlays.clone(), cx)
});
} else {
let ids = state
.inlays
.iter()
.map(|inlay| inlay.id)
.collect::<Vec<_>>();
map.update(cx, |map, cx| map.splice_inlays(&ids, Vec::new(), cx));
state.inlays.clear();
}
}
4 => {
state.wrap_width = if state.wrap_width.is_some() {
None
} else {
Some(px([0.0, 12.0, 48.0, 300.0][rng.random_range(0..4)]))
};
map.update(cx, |map, cx| map.set_wrap_width(state.wrap_width, cx));
}
5 => {
if state.block.is_some() {
map.update(cx, |map, cx| {
map.remove_blocks(std::mem::take(&mut block_ids), cx)
});
state.block = None;
} else {
let text = snapshot.buffer_snapshot().text();
let boundaries = scalar_boundaries(&text);
let start = rng.random_range(0..boundaries.len());
let buffer = snapshot.buffer_snapshot();
let position = buffer.anchor_after(MultiBufferOffset(boundaries[start]));
let placement = match rng.random_range(0..3) {
0 => BlockPlacement::Above(position),
1 => BlockPlacement::Below(position),
_ => {
let start = rng.random_range(0..boundaries.len() - 1);
let end = rng.random_range(start + 1..boundaries.len());
BlockPlacement::Replace(
buffer.anchor_before(MultiBufferOffset(boundaries[start]))
..=buffer.anchor_after(MultiBufferOffset(boundaries[end])),
)
}
};
state.block = Some((placement, rng.random_range(1..=3)));
block_ids.extend(map.update(cx, |map, cx| {
map.insert_blocks([state.block_properties().expect("block")], cx)
}));
}
}
_ => unreachable!(),
}
let previous = snapshot;
snapshot = map.update(cx, |map, cx| map.snapshot(cx));
state.folds = snapshot
.folds_in_range(Anchor::Min..Anchor::Max)
.map(|fold| fold.range.0.clone())
.collect();
let text = texts.join("\n");
assert_eq!(
snapshot.buffer_snapshot().text(),
text,
"operation {operation}"
);
let plain = (state.folds.is_empty()
&& state
.inlays
.iter()
.all(|inlay| !inlay.position.is_valid(snapshot.buffer_snapshot()))
&& !state.replaces_rows())
.then_some(text.as_str());
let rewrapping = map.update(cx, |map, cx| map.is_rewrapping(cx));
let rebuilt = state.build_map(cx);
cx.run_until_parked();
let rebuilt = rebuilt.update(cx, |map, cx| map.snapshot(cx));
let settled = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(
hidden_rows(&settled),
hidden_rows(&rebuilt),
"operation {operation}: hidden rows"
);
check_snapshot(
&snapshot,
&rebuilt,
!state.replaces_rows() || !rewrapping,
plain,
state.tab_size,
&mut rng,
operation,
);
if (kind == 4 || kind == 5) && !replaced_before && !state.replaces_rows() {
check_snapshot(
&snapshot,
&previous,
true,
plain,
state.tab_size,
&mut rng,
operation,
);
}
check_snapshot(
&settled,
&rebuilt,
true,
plain,
state.tab_size,
&mut rng,
operation,
);
snapshot = settled;
}
}
struct MapState {
buffer: Entity<MultiBuffer>,
tab_size: u32,
folds: Vec<Range<Anchor>>,
inlays: Vec<Inlay>,
block: Option<(BlockPlacement<Anchor>, u32)>,
wrap_width: Option<Pixels>,
}
impl MapState {
fn new(buffer: Entity<MultiBuffer>) -> Self {
Self {
buffer,
tab_size: 4,
folds: Vec::new(),
inlays: Vec::new(),
block: None,
wrap_width: None,
}
}
fn build_map(&self, cx: &mut TestAppContext) -> Entity<DisplayMap> {
cx.new(|cx| {
let mut map = DisplayMap::new(
self.buffer.clone(),
test_font(),
px(10.0),
self.wrap_width,
1,
1,
FoldPlaceholder::test(),
DiagnosticSeverity::Warning,
cx,
);
map.splice_inlays(&[], self.inlays.clone(), cx);
if !self.folds.is_empty() {
map.fold(
self.folds
.iter()
.cloned()
.map(|range| Crease::simple(range, FoldPlaceholder::test()))
.collect(),
cx,
);
}
if let Some(block) = self.block_properties() {
map.insert_blocks([block], cx);
}
map
})
}
fn replaces_rows(&self) -> bool {
matches!(self.block, Some((BlockPlacement::Replace(_), _)))
}
fn block_properties(&self) -> Option<BlockProperties<Anchor>> {
self.block
.as_ref()
.map(|(placement, height)| BlockProperties {
placement: placement.clone(),
style: BlockStyle::Fixed,
height: Some(*height),
render: Arc::new(|_| div().into_any()),
priority: 0,
})
}
}
struct PlainRow {
text: String,
len: u32,
columns: Vec<(u32, u32)>,
points: Vec<(u32, u32)>,
boundaries: Vec<usize>,
}
fn assert_batch_points(
query: &mut ColumnarSelectionRows<'_>,
mut requests: Vec<(u32, Range<u32>)>,
mut expected: Vec<Option<(Point, Point)>>,
work_bound: usize,
) {
for _ in 0..2 {
ROW_WORK.set((0, 0));
let answers = query.points_for_rows(&requests);
assert_eq!(answers.len(), expected.len());
for (index, (answer, expected)) in answers.iter().zip(&expected).enumerate() {
assert_eq!(answer, expected, "request {index}: {:?}", requests[index]);
}
let (boundaries, chunks) = ROW_WORK.get();
assert!(boundaries <= work_bound, "{boundaries} > {work_bound}");
assert!(chunks <= work_bound, "{chunks} > {work_bound}");
requests.reverse();
expected.reverse();
}
}
fn enumerated_points(
snapshot: &DisplaySnapshot,
raw: &str,
row: u32,
boundaries: &[usize],
) -> Vec<(u32, Point)> {
raw.split('\n')
.enumerate()
.flat_map(|(row, line)| {
grapheme_boundaries(line)
.into_iter()
.map(move |byte| Point::new(row as u32, byte as u32))
})
.filter_map(|point| {
let tab = snapshot
.tab_snapshot()
.point_to_tab_point(point, Bias::Left);
if tab.row() != row
|| snapshot.tab_snapshot().tab_point_to_point(tab, Bias::Left) != point
{
return None;
}
let display = point.to_display_point(snapshot);
if display.to_point(snapshot) != point || snapshot.is_block_line(display.row()) {
return None;
}
boundaries
.binary_search(&(tab.column() as usize))
.ok()
.map(|column| (column as u32, point))
})
.collect()
}
fn nearest_points(
columns: &Range<u32>,
length: u32,
candidates: &[(u32, Point)],
) -> Option<(Point, Point)> {
if columns.start > columns.end || (columns.start != columns.end && columns.start >= length)
{
return None;
}
let point = |column| {
candidates
.iter()
.filter(|(candidate, _)| *candidate <= column)
.max_by_key(|(candidate, _)| *candidate)
.map(|(_, point)| *point)
};
point(columns.start).zip(point(columns.end))
}
fn hidden_rows(snapshot: &DisplaySnapshot) -> Vec<bool> {
(0..=snapshot.buffer_snapshot().max_point().row)
.map(|row| snapshot.is_block_line(Point::new(row, 0).to_display_point(snapshot).row()))
.collect()
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings = SettingsStore::test(cx);
cx.set_global(settings);
crate::init(cx);
theme_settings::init(LoadThemes::JustBase, cx);
});
set_tab_size(4, cx);
}
fn set_tab_size(size: u32, cx: &mut TestAppContext) {
cx.update(|cx| {
cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings(cx, |settings| {
settings.project.all_languages.defaults.tab_size = NonZeroU32::new(size);
})
})
});
}
fn plain_state(text: &str, cx: &mut TestAppContext) -> MapState {
MapState::new(cx.update(|cx| MultiBuffer::build_simple(text, cx)))
}
fn random_text(rng: &mut StdRng, tokens: usize) -> String {
let mut text = String::new();
let mut tabs = 0;
for _ in 0..rng.random_range(0..=tokens) {
let mut token = *[
"a",
" ",
"\t",
"\n",
"α",
"界",
"🏀",
"é",
"e\u{301}",
"o\u{308}\u{301}",
"\u{301}",
"👩\u{200d}💻",
"👍🏽",
"🇦🇶",
"✈\u{fe0f}",
]
.choose(rng)
.expect("tokens");
if token == "\t" {
if tabs == 2 {
token = "a";
} else {
tabs += 1;
}
}
text.push_str(token);
}
text
}
fn assert_row_boundaries(query: &mut RowText<'_>, expected: &[usize]) {
let length = *expected.last().expect("end boundary");
for byte in 0..=length + 1 {
let column = expected.partition_point(|boundary| *boundary <= byte) - 1;
assert_eq!(
query.columns_for_bytes(byte as u32..byte as u32),
column as u32..column as u32,
"byte {byte}, expected {expected:?}"
);
assert_eq!(
query.floor_at(byte as u32).byte(),
expected[column] as u32,
"byte {byte}, expected {expected:?}"
);
}
let mut cursor = query.cursor(0);
for byte in expected.iter().skip(1) {
assert_eq!(query.next_boundary(&mut cursor), Some(*byte as u32));
}
assert_eq!(query.next_boundary(&mut cursor), None);
for byte in expected.iter().rev().skip(1) {
assert_eq!(query.previous_boundary(&mut cursor), Some(*byte as u32));
}
assert_eq!(query.previous_boundary(&mut cursor), None);
let mut cursor = query.floor_at(u32::MAX);
assert_eq!(cursor.byte(), length as u32);
for byte in expected.iter().rev().skip(1) {
assert_eq!(query.previous_boundary(&mut cursor), Some(*byte as u32));
}
query.advance_columns(&mut cursor, u32::MAX);
assert_eq!(cursor.byte(), length as u32);
}
fn scalar_boundaries(text: &str) -> Vec<usize> {
text.char_indices()
.map(|(offset, _)| offset)
.chain(iter::once(text.len()))
.collect()
}
fn grapheme_boundaries(text: &str) -> Vec<usize> {
text.grapheme_indices(true)
.map(|(offset, _)| offset)
.chain(iter::once(text.len()))
.collect()
}
fn plain_row(line: &str, tab_size: u32) -> PlainRow {
let mut text = String::new();
let mut columns = Vec::new();
let mut points = Vec::new();
let mut boundaries = Vec::new();
let mut column = 0;
let mut character_column = 0;
for (byte, grapheme) in line.grapheme_indices(true) {
points.push((column, byte as u32));
columns.extend(
grapheme
.char_indices()
.map(|(offset, _)| ((byte + offset) as u32, column)),
);
if grapheme == "\t" {
let width = if byte < 256 {
tab_size - character_column % tab_size
} else {
1
};
boundaries.extend(text.len()..text.len() + width as usize);
text.extend(iter::repeat_n(' ', width as usize));
character_column += width;
column += width;
} else {
boundaries.push(text.len());
text.push_str(grapheme);
character_column += grapheme.chars().count() as u32;
column += 1;
}
}
columns.push((line.len() as u32, column));
points.push((column, line.len() as u32));
boundaries.push(text.len());
PlainRow {
text,
len: column,
columns,
points,
boundaries,
}
}
fn expected_points(
row: u32,
columns: &Range<u32>,
expected: &PlainRow,
) -> Option<(Point, Point)> {
if columns.start != columns.end && columns.start >= expected.len {
return None;
}
let point = |column| {
Point::new(
row,
expected
.points
.iter()
.rev()
.find(|(candidate, _)| *candidate <= column)
.expect("row start")
.1,
)
};
Some((point(columns.start), point(columns.end)))
}
fn assert_plain_rows(snapshot: &DisplaySnapshot, tab_size: u32) {
let raw = snapshot.buffer_snapshot().text();
let expected = raw
.split('\n')
.map(|line| plain_row(line, tab_size))
.collect::<Vec<_>>();
assert_eq!(
snapshot.tab_snapshot().text(),
expected
.iter()
.map(|row| row.text.as_str())
.collect::<Vec<_>>()
.join("\n")
);
let mut query = ColumnarSelectionRows::new(snapshot);
for (row, expected) in expected.iter().enumerate() {
let row = row as u32;
assert_eq!(
query.row(row).columns_for_bytes(0..u32::MAX).end,
expected.len
);
for &(byte, column) in &expected.columns {
assert_eq!(
query.columns_for_range(Point::new(row, byte)..Point::new(row, byte)),
column..column
);
}
for column in 0..=expected.len + 1 {
for columns in [column..column, column..column + 1, column..u32::MAX] {
assert_eq!(
query.points_for_row(row, &columns),
expected_points(row, &columns, expected),
"row {row}, columns {columns:?}, text {:?}",
expected.text
);
}
}
}
}
fn check_snapshot(
snapshot: &DisplaySnapshot,
rebuilt: &DisplaySnapshot,
blocks_agree: bool,
plain: Option<&str>,
tab_size: u32,
rng: &mut StdRng,
operation: usize,
) {
let text = snapshot.tab_snapshot().text();
assert_eq!(
text,
rebuilt.tab_snapshot().text(),
"operation {operation}: tab text"
);
assert_eq!(
snapshot.fold_snapshot().text(),
rebuilt.fold_snapshot().text(),
"operation {operation}: fold text"
);
assert_eq!(
snapshot.inlay_snapshot().text(),
rebuilt.inlay_snapshot().text(),
"operation {operation}: inlay text"
);
let raw = snapshot.buffer_snapshot().text();
assert_eq!(raw, rebuilt.buffer_snapshot().text());
if let Some(plain) = plain {
assert_eq!(raw, plain);
}
let expected = plain.map(|plain| {
plain
.split('\n')
.map(|line| plain_row(line, tab_size))
.collect::<Vec<_>>()
});
if let Some(expected) = &expected {
assert_eq!(
text,
expected
.iter()
.map(|row| row.text.as_str())
.collect::<Vec<_>>()
.join("\n"),
"operation {operation}: independent expansion"
);
}
let lines = text.split('\n').collect::<Vec<_>>();
let raw_lines = raw.split('\n').collect::<Vec<_>>();
let raw_boundaries = grapheme_boundaries(&raw);
let mut query = ColumnarSelectionRows::new(snapshot);
let mut rebuilt_query = ColumnarSelectionRows::new(rebuilt);
assert_eq!(
snapshot.tab_snapshot().max_point().row() as usize + 1,
lines.len()
);
let sources = scalar_boundaries(&raw);
for (start, end) in [
(0, raw.len()),
(
sources[rng.random_range(0..sources.len())],
sources[rng.random_range(0..sources.len())],
),
] {
let point = |offset| {
Point::new(
raw[..offset].matches('\n').count() as u32,
raw[..offset].rsplit('\n').next().expect("line").len() as u32,
)
};
let range = point(start)..point(end);
let columns = query.columns_for_range(range.clone());
assert_eq!(
columns,
rebuilt_query.columns_for_range(range.clone()),
"operation {operation}: source {range:?}"
);
if let Some(expected) = &expected {
let column = |point: Point| {
expected[point.row as usize]
.columns
.iter()
.find(|(byte, _)| *byte == point.column)
.expect("source byte")
.1
};
let (start, end) = (column(range.start), column(range.end));
assert_eq!(
columns,
start.min(end)..start.max(end),
"operation {operation}: independent source {range:?}"
);
}
}
let folded = snapshot.fold_snapshot().text();
let expanded = folded
.split('\n')
.map(|line| plain_row(line, tab_size))
.collect::<Vec<_>>();
let mut rows = vec![
0,
lines.len() - 1,
rng.random_range(0..lines.len()),
rng.random_range(0..lines.len()),
];
rows.sort_unstable();
rows.dedup();
for row in rows {
assert_eq!(lines[row], expanded[row].text);
let boundaries = &expanded[row].boundaries;
let len = boundaries.len() as u32 - 1;
assert_eq!(
snapshot.tab_snapshot().line_len(row as u32),
lines[row].len() as u32
);
assert_eq!(
query.row(row as u32).columns_for_bytes(0..u32::MAX).end,
len,
"operation {operation}: row {row}"
);
assert_eq!(
rebuilt_query
.row(row as u32)
.columns_for_bytes(0..u32::MAX)
.end,
len
);
let candidates = enumerated_points(snapshot, &raw, row as u32, boundaries);
let middle = rng.random_range(0..=len + 1);
let requests = [
0..0,
0..u32::MAX,
middle..middle,
middle..middle + 1,
middle..rng.random_range(middle..=len + 1),
len..len,
len..len + 1,
u32::MAX..u32::MAX,
]
.map(|columns| (row as u32, columns));
let answers = query.points_for_rows(&requests);
let rebuilt_answers = rebuilt_query.points_for_rows(&requests);
let mut checked_points = HashSet::default();
for (((_, columns), points), rebuilt_points) in
requests.into_iter().zip(answers).zip(rebuilt_answers)
{
if blocks_agree {
assert_eq!(
points, rebuilt_points,
"operation {operation}: row {row}, columns {columns:?}"
);
}
if let Some(expected) = &expected {
assert_eq!(
points,
expected_points(row as u32, &columns, &expected[row]),
"operation {operation}: independent row {row}, columns {columns:?}"
);
}
let expected = nearest_points(&columns, len, &candidates);
assert_eq!(
points, expected,
"operation {operation}: enumerated raw boundaries, row {row}, columns {columns:?}, raw {raw:?}, tab {text:?}"
);
if let Some((start, end)) = points {
assert!(start <= end);
for point in [start, end] {
if !checked_points.insert(point) {
continue;
}
let line = raw_lines
.get(point.row as usize)
.expect("raw row in bounds");
assert!(point.column as usize <= line.len());
let offset = raw_lines
.iter()
.take(point.row as usize)
.map(|line| line.len() + 1)
.sum::<usize>()
+ point.column as usize;
assert_eq!(
raw_boundaries
.get(raw_boundaries.partition_point(|boundary| *boundary < offset))
.copied(),
Some(offset),
"operation {operation}: full-buffer grapheme {point:?}, raw {raw:?}"
);
let forward = snapshot
.tab_snapshot()
.point_to_tab_point(point, Bias::Left);
assert_eq!(forward.row(), row as u32);
assert_eq!(
snapshot
.tab_snapshot()
.tab_point_to_point(forward, Bias::Left),
point
);
let column = boundaries
.binary_search(&(forward.column() as usize))
.expect("tab grapheme boundary")
as u32;
assert_eq!(
query.points_for_row(row as u32, &(column..column)),
Some((point, point))
);
let display = point.to_display_point(snapshot);
assert!(!snapshot.is_block_line(display.row()));
assert_eq!(display.to_point(snapshot), point);
}
}
}
}
assert_eq!(query.points_for_row(lines.len() as u32, &(0..0)), None);
}
fn separated_columnar_ranges(
row: u32,
indices: Range<usize>,
stride: u32,
width: u32,
reversed: bool,
) -> Vec<Range<Point>> {
indices
.map(|index| {
let start = Point::new(row, index as u32 * stride);
let end = Point::new(row, start.column + width);
if reversed { end..start } else { start..end }
})
.collect()
}
fn set_columnar_action_state(
cx: &mut EditorTestContext,
text: &str,
expected: &[Range<Point>],
) {
cx.update_editor(|editor, window, cx| {
editor.set_text(text, window, cx);
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select_ranges(expected.iter().cloned());
});
assert_eq!(
editor
.selections
.ranges::<Point>(&editor.display_snapshot(cx)),
expected,
);
});
}
#[track_caller]
fn assert_columnar_action_work(
cx: &mut EditorTestContext,
above: bool,
expected: &[Range<Point>],
work_bound: usize,
) {
cx.update_editor(|editor, window, cx| {
ROW_WORK.set((0, 0));
if above {
editor.add_selection_above(
&AddSelectionAbove {
skip_soft_wrap: true,
},
window,
cx,
);
} else {
editor.add_selection_below(
&AddSelectionBelow {
skip_soft_wrap: true,
},
window,
cx,
);
}
let (boundaries, chunks) = ROW_WORK.replace((0, 0));
assert!(boundaries > 0);
assert!(chunks > 0);
assert!(
boundaries + chunks <= work_bound,
"above {above}: {boundaries} boundaries + {chunks} chunks > {work_bound}",
);
let actual = editor
.selections
.ranges::<Point>(&editor.display_snapshot(cx));
assert_eq!(actual.len(), expected.len(), "above {above}");
for (index, (actual, expected)) in actual.iter().zip(expected).enumerate() {
assert_eq!(actual, expected, "above {above}, selection {index}");
}
});
}
}