diff --git a/crates/editor/src/columnar_selection.rs b/crates/editor/src/columnar_selection.rs new file mode 100644 index 0000000000..99654c89c2 --- /dev/null +++ b/crates/editor/src/columnar_selection.rs @@ -0,0 +1,2852 @@ +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]) -> Vec> { + 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::>(); + 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)], + ) -> Vec> { + 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) -> Range { + self.columns_for_ranges(&[range]).pop().expect("one range") + } + + #[cfg(test)] + pub(crate) fn points_for_row( + &mut self, + row: u32, + columns: &Range, + ) -> 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)>, + buffer_cursor: &mut Option>, + ) -> Option { + 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>, + backward: &mut Option>, + 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::(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>, + remaining: Box + '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, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum BoundaryQuery { + Next, + Previous, + Check, +} + +impl<'a> RowText<'a> { + fn new(length: u32, chunks: impl Iterator + 'a) -> Self { + Self::with_tabs(length, chunks.map(|text| (text, false))) + } + + fn with_tabs(length: u32, chunks: impl Iterator + '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) -> Range { + 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 { + 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 { + 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 { + #[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::>() + .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::>() + }; + 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::>(); + 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::>() + .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::>(); + 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::>(); + let mut expected_columns = (0..length as u32) + .flat_map(|column| [column..length as u32, column..length as u32]) + .collect::>(); + 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::>(); + 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::>(); + 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::>(); + 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::>(); + 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::>(); + 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::>(); + assert_eq!( + snapshot.tab_snapshot().text(), + expanded + .iter() + .map(|row| row.text.as_str()) + .collect::>() + .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::>(); + 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::>(); + 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::>(); + let mut ranges = points + .iter() + .flat_map(|start| points.iter().map(move |end| *start..*end)) + .collect::>(); + 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::>(); + 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::>(); + 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::>(); + 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::>(); + let text = prefixes + .iter() + .map(|prefix| format!("{prefix}\t\u{301}β")) + .collect::>() + .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::>() + .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::().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::>(); + let buffers = texts + .iter() + .map(|text| cx.new(|cx| Buffer::local(text.clone(), cx))) + .collect::>(); + 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::>(); + 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::(); + 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::>(); + 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, + tab_size: u32, + folds: Vec>, + inlays: Vec, + block: Option<(BlockPlacement, u32)>, + wrap_width: Option, + } + + impl MapState { + fn new(buffer: Entity) -> 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 { + 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> { + 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, + } + + fn assert_batch_points( + query: &mut ColumnarSelectionRows<'_>, + mut requests: Vec<(u32, Range)>, + mut expected: Vec>, + 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, + 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 { + (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::(|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 { + text.char_indices() + .map(|(offset, _)| offset) + .chain(iter::once(text.len())) + .collect() + } + + fn grapheme_boundaries(text: &str) -> Vec { + 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, + 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::>(); + assert_eq!( + snapshot.tab_snapshot().text(), + expected + .iter() + .map(|row| row.text.as_str()) + .collect::>() + .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::>() + }); + if let Some(expected) = &expected { + assert_eq!( + text, + expected + .iter() + .map(|row| row.text.as_str()) + .collect::>() + .join("\n"), + "operation {operation}: independent expansion" + ); + } + let lines = text.split('\n').collect::>(); + let raw_lines = raw.split('\n').collect::>(); + 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::>(); + 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::() + + 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, + stride: u32, + width: u32, + reversed: bool, + ) -> Vec> { + 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], + ) { + 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::(&editor.display_snapshot(cx)), + expected, + ); + }); + } + + #[track_caller] + fn assert_columnar_action_work( + cx: &mut EditorTestContext, + above: bool, + expected: &[Range], + 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::(&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}"); + } + }); + } +} diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index b2ba2c5e4c..f65dbf4052 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -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> { + 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 { diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 4306d239e3..f0a164abae 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -2628,6 +2628,11 @@ impl BlockSnapshot { self.wrap_snapshot.max_point() } } + + pub(super) fn replacement_block_input_range(&self, row: WrapRow) -> Option> { + let (start, end, item) = self.transforms.find::((), &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::>(), + 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); diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 2739a857ae..cfa9781983 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -926,6 +926,14 @@ impl FoldSnapshot { } } + pub(crate) fn placeholder_range_at(&self, point: FoldPoint) -> Option> { + let (start, end, item) = self + .transforms + .find::((), &point, Bias::Right); + item.filter(|transform| transform.placeholder.is_some()) + .map(|_| start..end) + } + #[ztracing::instrument(skip_all)] pub(crate) fn chunks<'a>( &'a self, diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index 611be1518b..ef08783339 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -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::(); + assert_eq!(text, expected_text); + } } diff --git a/crates/editor/src/edit_prediction.rs b/crates/editor/src/edit_prediction.rs index 1d2f5d6109..b3172ce32e 100644 --- a/crates/editor/src/edit_prediction.rs +++ b/crates/editor/src/edit_prediction.rs @@ -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(), + ); } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bb9820f9fc..2ad7d780c0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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]>, pub redo: Option]>>, + undo_add_selections_state: Option, + redo_add_selections_state: Option, } #[derive(Default)] @@ -1452,6 +1455,7 @@ impl SelectionHistory { &mut self, transaction_id: TransactionId, selections: Arc<[Selection]>, + add_selections_state: Option, ) { 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, + skip_soft_wrap: bool, } #[derive(Clone, Debug)] struct AddSelectionsGroup { above: bool, stack: Vec, + goal_source: Option>, } #[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]>>, + selections: Option<(Arc<[Selection]>, Option)>, window: &mut Window, cx: &mut Context, ) { - 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() } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index f57cc2450d..e92b1b8949 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -11859,6 +11859,1983 @@ async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestA ); } +#[gpui::test] +async fn test_add_selection_grapheme_columns(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for (text, columns) in [ + ("e\u{301}x|Z\néé|Zˇ", [4..4, 4..4]), + ("e\u{301}x|Z\néé|Zˇ", [4..5, 4..5]), + ("éx|Z\ne\u{301}x|Zˇ", [3..3, 4..4]), + ("👩\u{200d}💻x|Z\nax|Zˇ", [12..13, 2..3]), + ("🇦🇶x|Z\nax|Zˇ", [8..9, 1..2]), + ("\t\u{301}x\n\t\u{301}xˇ", [1..1, 1..1]), + ("\t\u{301}x\n\t\u{301}xˇ", [1..3, 1..3]), + ] { + for above in [false, true] { + for reversed in [false, true] { + cx.set_state(text); + cx.update_editor(|editor, window, cx| { + assert_add_selection_pair(editor, columns.clone(), above, reversed, window, cx); + }); + } + } + } +} + +#[gpui::test] +async fn test_add_selection_goal_after_moving(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("abˇcdef\nabcdef\nabcdef"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, true, window, cx); + editor.move_right(&MoveRight, window, cx); + add_selection_in_direction(editor, false, true, window, cx); + }); + cx.assert_editor_state("abcˇdef\nabcˇdef\nabcˇdef"); +} + +#[gpui::test] +async fn test_add_selection_unwrapped_unprojectable_sources(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for above in [false, true] { + for reversed in [false, true] { + for mixed in [false, true] { + cx.set_state("aˇ\nb\nabcdef\nabcdef"); + cx.update_editor(|editor, window, cx| { + let position = editor + .buffer + .read(cx) + .snapshot(cx) + .anchor_before(Point::new(1, 0)); + editor.splice_inlays( + &[], + vec![inlays::Inlay::mock_hint(0, position, "HH")], + cx, + ); + let source = if reversed { + Point::new(1, 0)..Point::new(0, 1) + } else { + Point::new(0, 1)..Point::new(1, 0) + }; + let other_row = if above { 3 } else { 2 }; + let mut original = vec![source.clone()]; + if mixed { + original.push(Point::new(other_row, 1)..Point::new(other_row, 1)); + } + editor.change_selections( + SelectionEffects::no_scroll(), + window, + cx, + |selections| selections.select_ranges(original.clone()), + ); + let original_id = editor.selections.first_anchor().id; + add_selection_in_direction(editor, above, true, window, cx); + let mut expected = vec![source]; + if mixed { + expected.extend([ + Point::new(2, 1)..Point::new(2, 1), + Point::new(3, 1)..Point::new(3, 1), + ]); + } + assert_add_selection_ranges(editor, &expected, cx); + assert_eq!(editor.selections.first_anchor().id, original_id); + add_selection_in_direction(editor, !above, true, window, cx); + assert_add_selection_ranges(editor, &original, cx); + assert_eq!(editor.selections.first_anchor().id, original_id); + editor.splice_inlays(&[project::InlayId::Hint(0)], Vec::new(), cx); + }); + } + } + } +} + +#[gpui::test] +async fn test_add_selection_unwrapped_multiline_goal_and_reversal(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for (above, text, source, columns) in [ + ( + false, + "abc\ndefghi\nuvwxyz\nabcdefˇ", + Point::new(0, 1)..Point::new(1, 4), + [1..3, 1..4, 1..4, 1..4], + ), + ( + true, + "abcdef\nuvwxyz\ndefghi\nabcˇ", + Point::new(2, 4)..Point::new(3, 1), + [1..4, 1..4, 1..4, 1..3], + ), + ] { + for reversed in [false, true] { + for deferred in [false, true] { + cx.set_state(text); + cx.update_editor(|editor, window, cx| { + editor.change_selections( + SelectionEffects::no_scroll(), + window, + cx, + |selections| { + selections.select_ranges([if reversed { + source.end..source.start + } else { + source.clone() + }]); + }, + ); + for (direction, count) in [ + (above, 3), + (above, 4), + (!above, 3), + (above, 4), + (!above, 3), + (!above, 2), + (!above, 1), + (!above, 1), + (above, 2), + ] { + if deferred { + editor.with_selection_effects_deferred( + window, + cx, + |editor, window, cx| { + add_selection_in_direction(editor, direction, true, window, cx); + }, + ); + } else { + add_selection_in_direction(editor, direction, true, window, cx); + } + let expected = columns + .iter() + .enumerate() + .filter(|(row, _)| { + if above { + *row >= 4 - count + } else { + *row < count + } + }) + .map(|(row, columns)| { + let start = Point::new(row as u32, columns.start); + let end = Point::new(row as u32, columns.end); + if reversed { end..start } else { start..end } + }) + .collect::>(); + assert_add_selection_ranges(editor, &expected, cx); + } + }); + } + } + } +} + +#[gpui::test] +async fn test_add_selection_unwrapped_single_projection_retains_goal(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("a«bc\nˇ»\nabcdef"); + cx.update_editor(|editor, window, cx| { + let position = editor + .buffer + .read(cx) + .snapshot(cx) + .anchor_before(Point::new(1, 0)); + editor.splice_inlays(&[], vec![inlays::Inlay::mock_hint(0, position, "HHHH")], cx); + editor.with_selection_effects_deferred(window, cx, |editor, window, cx| { + add_selection_in_direction(editor, true, true, window, cx); + }); + }); + cx.assert_editor_state("a«bcˇ»\n\nabcdef"); + for _ in 0..2 { + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, true, window, cx); + }); + cx.assert_editor_state("a«bcˇ»\n\na«bcdˇ»ef"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, true, true, window, cx); + }); + cx.assert_editor_state("a«bcˇ»\n\nabcdef"); + } +} + +#[gpui::test] +async fn test_add_selection_unwrapped_multiline_goal_after_moving_or_typing( + cx: &mut TestAppContext, +) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for typing in [false, true] { + cx.set_state("a«bc\ndefgˇ»hi\nuvwxyz\nabcdef"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, true, window, cx); + if typing { + editor.handle_input("X", window, cx); + } else { + editor.move_right(&MoveRight, window, cx); + } + add_selection_in_direction(editor, false, true, window, cx); + }); + let expected = if typing { + "aXˇ\ndXˇhi\nuXˇyz\nabˇcdef" + } else { + "abcˇ\ndefgˇhi\nuvwxˇyz\nabcˇdef" + }; + cx.assert_editor_state(expected); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, true, true, window, cx); + add_selection_in_direction(editor, false, true, window, cx); + }); + cx.assert_editor_state(expected); + } +} + +#[gpui::test] +fn test_add_selection_unwrapped_reseeds_after_folding_source_buffer(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new_multibuffer(cx, ["«abcdef»", "«x\nabcdef\nabcdef»"]); + let source_buffer_id = cx.multibuffer(|buffer, cx| { + buffer + .snapshot(cx) + .excerpts() + .next() + .expect("source excerpt") + .context + .start + .buffer_id + }); + cx.set_selections_state("abcdˇef\nx\nabcdef\nabcdef"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, true, window, cx); + add_selection_in_direction(editor, false, true, window, cx); + }); + cx.assert_editor_state("abcdˇef\nxˇ\nabcdˇef\nabcdef"); + cx.update_editor(|editor, _, cx| { + editor.fold_buffer(source_buffer_id, cx); + }); + cx.assert_editor_state("abcdef\nxˇ\nabcdˇef\nabcdef"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, true, window, cx); + }); + cx.assert_editor_state("abcdef\nxˇ\nabcdˇef\naˇbcdef"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, true, true, window, cx); + add_selection_in_direction(editor, false, true, window, cx); + }); + cx.assert_editor_state("abcdef\nxˇ\nabcdˇef\naˇbcdef"); +} + +#[gpui::test] +fn test_add_selection_unwrapped_retains_goal_after_folding_projection_buffer( + cx: &mut TestAppContext, +) { + init_test(cx, |_| {}); + for above in [false, true] { + let mut cx = EditorTestContext::new_multibuffer( + cx, + if above { + ["«abcdef»", "«uvwxyz»", "«defghi\nabc»"] + } else { + ["«abc\ndefghi»", "«uvwxyz»", "«abcdef»"] + }, + ); + let projection_buffer_id = cx.multibuffer(|buffer, cx| { + buffer + .snapshot(cx) + .excerpts() + .nth(1) + .expect("projection excerpt") + .context + .start + .buffer_id + }); + cx.set_selections_state(if above { + "abcdef\nuvwxyz\ndefg«hi\naˇ»bc" + } else { + "a«bc\ndefgˇ»hi\nuvwxyz\nabcdef" + }); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, above, true, window, cx); + }); + cx.assert_editor_state(if above { + "abcdef\nu«vwxˇ»yz\nd«efgˇ»hi\na«bcˇ»" + } else { + "a«bcˇ»\nd«efgˇ»hi\nu«vwxˇ»yz\nabcdef" + }); + cx.update_editor(|editor, _, cx| { + editor.fold_buffer(projection_buffer_id, cx); + }); + cx.assert_editor_state(if above { + "abcdef\nuvwxyz\nd«efgˇ»hi\na«bcˇ»" + } else { + "a«bcˇ»\nd«efgˇ»hi\nuvwxyz\nabcdef" + }); + for _ in 0..2 { + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, above, true, window, cx); + }); + cx.assert_editor_state(if above { + "a«bcdˇ»ef\nuvwxyz\nd«efgˇ»hi\na«bcˇ»" + } else { + "a«bcˇ»\nd«efgˇ»hi\nuvwxyz\na«bcdˇ»ef" + }); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, !above, true, window, cx); + }); + } + } +} + +#[gpui::test] +async fn test_add_selection_reverses_groups_with_deleted_sources(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for skip_soft_wrap in [false, true] { + cx.set_state("abˇcdef\nabcdef\nabcdef\nabˇcdef\nabcdef\nabcdef"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, skip_soft_wrap, window, cx); + }); + cx.assert_editor_state("abˇcdef\nabˇcdef\nabcdef\nabˇcdef\nabˇcdef\nabcdef"); + cx.update_editor(|editor, window, cx| { + let snapshot = editor.display_snapshot(cx); + let source_ids = editor + .selections + .all::(&snapshot) + .into_iter() + .filter(|selection| selection.start.row % 3 == 0) + .map(|selection| selection.id) + .collect::>(); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { + for id in source_ids { + selections.delete(id); + } + }); + add_selection_in_direction(editor, true, skip_soft_wrap, window, cx); + }); + cx.assert_editor_state("abˇcdef\nabˇcdef\nabcdef\nabˇcdef\nabˇcdef\nabcdef"); + } +} + +#[gpui::test] +async fn test_add_selection_after_members_span_rows(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for skip_soft_wrap in [false, true] { + cx.set_state("abˇcdef\nabcdef\nabcdef\nabcdef"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, skip_soft_wrap, window, cx); + editor.select_down(&SelectDown, window, cx); + }); + cx.assert_editor_state("ab«cdef\nabˇ»«cdef\nabˇ»cdef\nabcdef"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, skip_soft_wrap, window, cx); + }); + cx.assert_editor_state("ab«cdef\nabˇ»«cdef\nabˇ»cdef\nabˇcdef"); + } +} + +#[gpui::test] +async fn test_add_selection_fresh_pixel_source_reseeds_goal(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for above in [false, true] { + cx.set_state(if above { + "abcdef\nx\nabcdˇef" + } else { + "abcdˇef\nx\nabcdef" + }); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, above, false, window, cx); + }); + cx.assert_editor_state(if above { + "abcdef\nxˇ\nabcdˇef" + } else { + "abcdˇef\nxˇ\nabcdef" + }); + cx.update_editor(|editor, window, cx| { + let source_row = if above { 2 } else { 0 }; + let source_id = editor + .selections + .disjoint_anchors() + .iter() + .find(|selection| { + selection + .start + .to_point(&editor.buffer.read(cx).snapshot(cx)) + .row + == source_row + }) + .expect("source selection") + .id; + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { + selections.delete(source_id); + }); + let clamped_id = editor.selections.disjoint_anchors()[0].id; + add_selection_in_direction(editor, above, false, window, cx); + assert_eq!( + stored_selection_ranges(editor, cx), + if above { + [ + Point::new(0, 1)..Point::new(0, 1), + Point::new(1, 1)..Point::new(1, 1), + ] + } else { + [ + Point::new(1, 1)..Point::new(1, 1), + Point::new(2, 1)..Point::new(2, 1), + ] + } + ); + assert!( + editor + .selections + .disjoint_anchors() + .iter() + .any(|selection| selection.id == clamped_id) + ); + }); + } +} + +#[gpui::test] +async fn test_add_selection_keeps_fresh_source_inside_fold(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for skip_soft_wrap in [false, true] { + cx.set_state("abˇcdef\nabcdef\nabcdef"); + cx.update_editor(|editor, window, cx| { + editor.fold_creases( + vec![Crease::simple( + Point::new(0, 0)..Point::new(1, 6), + FoldPlaceholder::test(), + )], + false, + window, + cx, + ); + assert_eq!(editor.display_text(cx), "⋯\nabcdef"); + let source_id = editor.selections.disjoint_anchors()[0].id; + add_selection_in_direction(editor, false, skip_soft_wrap, window, cx); + let stored = stored_selection_ranges(editor, cx); + assert_eq!(stored[0], Point::new(0, 2)..Point::new(0, 2)); + assert_eq!(stored.len(), 2); + assert_eq!(stored[1].start.row, 2); + assert_eq!(editor.selections.disjoint_anchors()[0].id, source_id); + add_selection_in_direction(editor, true, skip_soft_wrap, window, cx); + assert_eq!( + stored_selection_ranges(editor, cx), + [Point::new(0, 2)..Point::new(0, 2)] + ); + assert_eq!(editor.selections.disjoint_anchors()[0].id, source_id); + }); + } +} + +#[gpui::test] +async fn test_add_selection_keeps_group_members_coalesced_by_fold(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for skip_soft_wrap in [false, true] { + cx.set_state("abˇcdef\nabcdef\nabcdef"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, skip_soft_wrap, window, cx); + editor.fold_creases( + vec![Crease::simple( + Point::new(0, 0)..Point::new(1, 6), + FoldPlaceholder::test(), + )], + false, + window, + cx, + ); + assert_eq!(editor.display_text(cx), "⋯\nabcdef"); + add_selection_in_direction(editor, false, skip_soft_wrap, window, cx); + let stored = stored_selection_ranges(editor, cx); + assert_eq!( + stored[..2], + [ + Point::new(0, 2)..Point::new(0, 2), + Point::new(1, 2)..Point::new(1, 2), + ] + ); + assert_eq!(stored.len(), 3); + assert_eq!(stored[2].start.row, 2); + add_selection_in_direction(editor, true, skip_soft_wrap, window, cx); + add_selection_in_direction(editor, true, skip_soft_wrap, window, cx); + assert_eq!( + stored_selection_ranges(editor, cx), + [Point::new(0, 2)..Point::new(0, 2)] + ); + }); + } +} + +#[gpui::test] +fn test_add_selection_keeps_sources_on_block_rows(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + for skip_soft_wrap in [false, true] { + for visible_source in [None, Some(false), Some(true)] { + let mut cx = EditorTestContext::new_multibuffer(cx, ["«abcdef»", "«abcdef\nabcdef»"]); + let buffer_ids = cx.multibuffer(|buffer, cx| { + buffer + .snapshot(cx) + .excerpts() + .map(|excerpt| excerpt.context.start.buffer_id) + .collect::>() + }); + let folded = if visible_source.is_some() { + &buffer_ids[..1] + } else { + &buffer_ids[..] + }; + cx.set_selections_state("abˇcdef\nabcdef\nabcdef"); + cx.update_editor(|editor, _, cx| { + editor.fold_buffers(folded.iter().copied(), cx); + }); + let mut expected = vec![Point::zero()..Point::zero()]; + cx.update_editor(|editor, window, cx| { + assert_eq!(stored_selection_ranges(editor, cx), expected); + if let Some(pending) = visible_source { + expected.push(Point::new(1, 2)..Point::new(1, 2)); + if pending { + let position = + Point::new(1, 2).to_display_point(&editor.display_snapshot(cx)); + editor.begin_selection(position, true, 1, window, cx); + assert!(editor.selections.pending_anchor().is_some()); + } else { + editor.change_selections( + SelectionEffects::no_scroll(), + window, + cx, + |selections| { + selections.select_ranges(expected.clone()); + }, + ); + } + assert_eq!(stored_selection_ranges(editor, cx), expected); + } + add_selection_in_direction(editor, false, skip_soft_wrap, window, cx); + if visible_source.is_some() { + expected.push(Point::new(2, 2)..Point::new(2, 2)); + } + assert_eq!(stored_selection_ranges(editor, cx), expected); + add_selection_in_direction(editor, true, skip_soft_wrap, window, cx); + if visible_source.is_some() { + expected.pop(); + } + assert_eq!(stored_selection_ranges(editor, cx), expected); + editor.handle_input("X", window, cx); + }); + cx.update_editor(|editor, _, cx| { + for buffer_id in folded { + editor.unfold_buffer(*buffer_id, cx); + } + }); + cx.assert_editor_state(if visible_source.is_some() { + "Xˇabcdef\nabXˇcdef\nabcdef" + } else { + "Xˇabcdef\nabcdef\nabcdef" + }); + } + } +} + +#[gpui::test] +fn test_add_selection_skips_block_rows(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new_multibuffer( + cx, + ["«abcdef»", "«abcdefghij\nx»", "«abcdef»", "«abcdefghij\nx»"], + ); + let folded_buffer_ids = cx.multibuffer(|buffer, cx| { + buffer + .snapshot(cx) + .excerpts() + .map(|excerpt| excerpt.context.start.buffer_id) + .skip(1) + .step_by(2) + .collect::>() + }); + for skip_soft_wrap in [false, true] { + for column in [0, 4] { + let source = Point::new(0, column)..Point::new(0, column); + cx.update_editor(|editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { + selections.select_ranges([source.clone()]); + }); + add_selection_in_direction(editor, false, skip_soft_wrap, window, cx); + assert_add_selection_ranges( + editor, + &[source.clone(), Point::new(1, column)..Point::new(1, column)], + cx, + ); + }); + } + } + cx.update_editor(|editor, _, cx| { + editor.fold_buffers(folded_buffer_ids, cx); + }); + for skip_soft_wrap in [false, true] { + for (column, above) in [(0, false), (4, false), (0, true), (4, true)] { + let source_row = if above { 3 } else { 0 }; + let source = Point::new(source_row, column)..Point::new(source_row, column); + let target = Point::new(3 - source_row, column)..Point::new(3 - source_row, column); + cx.update_editor(|editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { + selections.select_ranges([source.clone()]); + }); + add_selection_in_direction(editor, above, skip_soft_wrap, window, cx); + let mut expected = vec![source.clone(), target]; + expected.sort_by_key(|range| range.start); + assert_add_selection_ranges(editor, &expected, cx); + add_selection_in_direction(editor, above, skip_soft_wrap, window, cx); + assert_add_selection_ranges(editor, &expected, cx); + add_selection_in_direction(editor, !above, skip_soft_wrap, window, cx); + assert_add_selection_ranges(editor, std::slice::from_ref(&source), cx); + }); + } + } +} + +#[gpui::test] +async fn test_add_selection_history_restores_groups(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for (above, initial, two, three) in [ + ( + false, + "abcdˇef\nab\nabcdef\nabcdef", + "abcdˇef\nabˇ\nabcdef\nabcdef", + "abcdˇef\nabˇ\nabcdˇef\nabcdef", + ), + ( + true, + "abcdef\nabcdef\nab\nabcdˇef", + "abcdef\nabcdef\nabˇ\nabcdˇef", + "abcdef\nabcdˇef\nabˇ\nabcdˇef", + ), + ] { + for skip_soft_wrap in [false, true] { + for deferred in [false, true] { + cx.set_state(initial); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, above, skip_soft_wrap, window, cx); + }); + cx.assert_editor_state(two); + cx.update_editor(|editor, window, cx| { + if deferred { + editor.with_selection_effects_deferred(window, cx, |editor, window, cx| { + add_selection_in_direction(editor, above, skip_soft_wrap, window, cx); + }); + } else { + add_selection_in_direction(editor, above, skip_soft_wrap, window, cx); + } + }); + cx.assert_editor_state(three); + cx.update_editor(|editor, window, cx| { + editor.undo_selection(&UndoSelection, window, cx); + }); + cx.assert_editor_state(two); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, above, skip_soft_wrap, window, cx); + }); + cx.assert_editor_state(three); + cx.update_editor(|editor, window, cx| { + editor.undo_selection(&UndoSelection, window, cx); + add_selection_in_direction(editor, !above, skip_soft_wrap, window, cx); + }); + cx.assert_editor_state(initial); + cx.update_editor(|editor, window, cx| { + editor.undo_selection(&UndoSelection, window, cx); + }); + cx.assert_editor_state(two); + cx.update_editor(|editor, window, cx| { + editor.redo_selection(&RedoSelection, window, cx); + }); + cx.assert_editor_state(initial); + cx.update_editor(|editor, window, cx| { + editor.undo_selection(&UndoSelection, window, cx); + add_selection_in_direction(editor, above, skip_soft_wrap, window, cx); + }); + cx.assert_editor_state(three); + } + } + } +} + +#[gpui::test] +async fn test_add_selection_history_deferred_batch(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for skip_soft_wrap in [false, true] { + cx.set_state("abcdˇef\nab\nabcdef\nabcdef"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, skip_soft_wrap, window, cx); + editor.with_selection_effects_deferred(window, cx, |editor, window, cx| { + add_selection_in_direction(editor, false, skip_soft_wrap, window, cx); + add_selection_in_direction(editor, false, skip_soft_wrap, window, cx); + }); + }); + cx.assert_editor_state("abcdˇef\nabˇ\nabcdˇef\nabcdˇef"); + cx.update_editor(|editor, window, cx| { + editor.undo_selection(&UndoSelection, window, cx); + }); + cx.assert_editor_state("abcdˇef\nabˇ\nabcdef\nabcdef"); + cx.update_editor(|editor, window, cx| { + editor.redo_selection(&RedoSelection, window, cx); + }); + cx.assert_editor_state("abcdˇef\nabˇ\nabcdˇef\nabcdˇef"); + cx.update_editor(|editor, window, cx| { + editor.undo_selection(&UndoSelection, window, cx); + add_selection_in_direction(editor, false, skip_soft_wrap, window, cx); + }); + cx.assert_editor_state("abcdˇef\nabˇ\nabcdˇef\nabcdef"); + } +} + +#[gpui::test] +async fn test_add_selection_history_deferred_external_movement(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for move_first in [false, true] { + cx.set_state("abcdˇef\nab\nabcdef\nabcdef"); + cx.update_editor(|editor, window, cx| { + if move_first { + add_selection_in_direction(editor, false, true, window, cx); + } + editor.with_selection_effects_deferred(window, cx, |editor, window, cx| { + if !move_first { + add_selection_in_direction(editor, false, true, window, cx); + } + editor.move_right(&MoveRight, window, cx); + if move_first { + add_selection_in_direction(editor, false, true, window, cx); + } + }); + }); + if move_first { + cx.assert_editor_state("abcdeˇf\nab\nˇabcdef\nabcdeˇf"); + cx.update_editor(|editor, window, cx| { + editor.undo_selection(&UndoSelection, window, cx); + }); + cx.assert_editor_state("abcdˇef\nabˇ\nabcdef\nabcdef"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, true, window, cx); + }); + cx.assert_editor_state("abcdˇef\nabˇ\nabcdˇef\nabcdef"); + } else { + cx.assert_editor_state("abcdeˇf\nab\nˇabcdef\nabcdef"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, true, window, cx); + }); + cx.assert_editor_state("abcdeˇf\nab\nˇabcdef\nabcdeˇf"); + } + } +} + +#[gpui::test] +async fn test_add_selection_history_restores_mode_before_switch(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for skip_soft_wrap in [false, true] { + for deferred in [false, true] { + cx.set_state("abcdˇef\nab\nabcdef\nabcdef"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, skip_soft_wrap, window, cx); + if deferred { + editor.with_selection_effects_deferred(window, cx, |editor, window, cx| { + add_selection_in_direction(editor, false, !skip_soft_wrap, window, cx); + }); + } else { + add_selection_in_direction(editor, false, !skip_soft_wrap, window, cx); + } + }); + cx.assert_editor_state("abcdˇef\nabˇ\nabˇcdef\nabcdef"); + cx.update_editor(|editor, window, cx| { + editor.undo_selection(&UndoSelection, window, cx); + }); + cx.assert_editor_state("abcdˇef\nabˇ\nabcdef\nabcdef"); + cx.update_editor(|editor, window, cx| { + editor.redo_selection(&RedoSelection, window, cx); + }); + cx.assert_editor_state("abcdˇef\nabˇ\nabˇcdef\nabcdef"); + cx.update_editor(|editor, window, cx| { + editor.undo_selection(&UndoSelection, window, cx); + add_selection_in_direction(editor, false, skip_soft_wrap, window, cx); + }); + cx.assert_editor_state("abcdˇef\nabˇ\nabcdˇef\nabcdef"); + } + } +} + +#[gpui::test] +async fn test_add_selection_history_restores_unclipped_multiline_goal(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for (above, initial, three, four) in multiline_add_selection_history_states() { + cx.set_state(initial); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, above, true, window, cx); + add_selection_in_direction(editor, above, true, window, cx); + }); + cx.assert_editor_state(four); + cx.update_editor(|editor, window, cx| { + editor.undo_selection(&UndoSelection, window, cx); + }); + cx.assert_editor_state(three); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, above, true, window, cx); + }); + cx.assert_editor_state(four); + cx.update_editor(|editor, window, cx| { + editor.undo_selection(&UndoSelection, window, cx); + editor.redo_selection(&RedoSelection, window, cx); + }); + cx.assert_editor_state(four); + } +} + +#[gpui::test] +async fn test_add_selection_text_undo_restores_multiline_goal(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for (above, initial, three, four) in multiline_add_selection_history_states() { + for grouped in [false, true] { + cx.set_state(initial); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, above, true, window, cx); + editor.transact(window, cx, |editor, window, cx| { + editor.insert("X", window, cx); + if grouped { + editor.insert("Y", window, cx); + } + }); + editor.undo(&Undo, window, cx); + }); + cx.assert_editor_state(three); + cx.update_editor(|editor, window, cx| { + editor.redo(&Redo, window, cx); + editor.undo(&Undo, window, cx); + add_selection_in_direction(editor, above, true, window, cx); + }); + cx.assert_editor_state(four); + } + } +} + +#[gpui::test] +async fn test_add_selection_history_preserves_hidden_sources(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for skip_soft_wrap in [false, true] { + for source_count in [1, 2] { + cx.set_state("abˇcdef\nabcdef\nabcdef"); + cx.update_editor(|editor, window, cx| { + let sources = (0..source_count) + .map(|row| Point::new(row, 2)..Point::new(row, 2)) + .collect::>(); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { + selections.select_ranges(sources.clone()); + }); + editor.fold_creases( + vec![Crease::simple( + Point::new(0, 0)..Point::new(1, 6), + FoldPlaceholder::test(), + )], + false, + window, + cx, + ); + add_selection_in_direction(editor, false, skip_soft_wrap, window, cx); + let mut added = sources.clone(); + added.push(Point::new(2, 0)..Point::new(2, 0)); + assert_eq!(stored_selection_ranges(editor, cx), added); + editor.undo_selection(&UndoSelection, window, cx); + assert_eq!(stored_selection_ranges(editor, cx), sources); + editor.redo_selection(&RedoSelection, window, cx); + assert_eq!(stored_selection_ranges(editor, cx), added); + editor.undo_selection(&UndoSelection, window, cx); + assert_eq!(stored_selection_ranges(editor, cx), sources); + }); + } + } +} + +#[gpui::test] +async fn test_add_selection_removing_projection_retains_goal(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for remove_by_click in [false, true] { + cx.set_state("a«bc\ndefgˇ»hi\nuvwxyz"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, true, window, cx); + }); + cx.assert_editor_state("a«bcˇ»\nd«efgˇ»hi\nu«vwxˇ»yz"); + cx.update_editor(|editor, window, cx| { + if remove_by_click { + editor.begin_selection(DisplayPoint::new(DisplayRow(2), 4), true, 1, window, cx); + } else { + add_selection_in_direction(editor, true, true, window, cx); + } + }); + cx.assert_editor_state("a«bcˇ»\nd«efgˇ»hi\nuvwxyz"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, true, window, cx); + }); + cx.assert_editor_state("a«bcˇ»\nd«efgˇ»hi\nu«vwxˇ»yz"); + } +} + +#[gpui::test] +async fn test_add_selection_coalesced_source_reseeds_goal_and_history(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("«abcdˇ»\nabcˇd\nx\nabcd"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, true, window, cx); + }); + cx.assert_editor_state("«abcdˇ»\n«abcdˇ»\nxˇ\nabcd"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, true, window, cx); + }); + cx.assert_editor_state("«abcdˇ»\n«abcdˇ»\n«xˇ»\naˇbcd"); + cx.update_editor(|editor, window, cx| { + editor.undo_selection(&UndoSelection, window, cx); + }); + cx.assert_editor_state("«abcdˇ»\n«abcdˇ»\nxˇ\nabcd"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, true, window, cx); + }); + cx.assert_editor_state("«abcdˇ»\n«abcdˇ»\n«xˇ»\naˇbcd"); +} + +#[gpui::test] +async fn test_add_selection_replacement_discards_dead_groups_and_history(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for skip_soft_wrap in [false, true] { + cx.set_state("aˇbcd\nabcd\nabcd"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, skip_soft_wrap, window, cx); + add_selection_in_direction(editor, false, skip_soft_wrap, window, cx); + assert_eq!( + editor.add_selections_state.as_ref().map(|state| { + state + .groups + .iter() + .map(|group| group.stack.len()) + .collect::>() + }), + Some(vec![3]) + ); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { + selections.select_ranges([Point::new(0, 1)..Point::new(0, 1)]); + }); + assert!(editor.add_selections_state.is_none()); + editor.move_right(&MoveRight, window, cx); + editor.move_left(&MoveLeft, window, cx); + assert!(editor.add_selections_state.is_none()); + assert_eq!( + editor + .selection_history + .undo_stack + .iter() + .rev() + .take(2) + .map(|entry| entry + .add_selections_state + .as_ref() + .map(|state| state.groups.len())) + .collect::>(), + [None, None] + ); + editor.undo_selection(&UndoSelection, window, cx); + assert!(editor.add_selections_state.is_none()); + }); + cx.assert_editor_state("abˇcd\nabcd\nabcd"); + } +} + +#[gpui::test] +async fn test_add_selection_cancel_reseeds_goal(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for (above, initial, cancelled, restarted) in [ + ( + false, + "a«bc\ndefgˇ»hi\nuvwxyz", + "a«bcˇ»\ndefghi\nuvwxyz", + "a«bcˇ»\nd«efˇ»ghi\nuvwxyz", + ), + ( + true, + "uvwxyz\na«bc\ndefgˇ»hi", + "uvwxyz\na«bcˇ»\ndefghi", + "u«vwˇ»xyz\na«bcˇ»\ndefghi", + ), + ] { + for skip_soft_wrap in [false, true] { + cx.set_state(initial); + let grouped = cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, above, skip_soft_wrap, window, cx); + let grouped = editor + .selections + .ranges::(&editor.display_snapshot(cx)); + editor.cancel(&Cancel, window, cx); + grouped + }); + cx.assert_editor_state(cancelled); + cx.update_editor(|editor, window, cx| { + editor.undo_selection(&UndoSelection, window, cx); + add_selection_in_direction(editor, !above, skip_soft_wrap, window, cx); + add_selection_in_direction(editor, above, skip_soft_wrap, window, cx); + assert_add_selection_ranges(editor, &grouped, cx); + editor.cancel(&Cancel, window, cx); + editor.undo_selection(&UndoSelection, window, cx); + editor.redo_selection(&RedoSelection, window, cx); + add_selection_in_direction(editor, above, skip_soft_wrap, window, cx); + }); + cx.assert_editor_state(restarted); + } + } +} + +#[gpui::test] +async fn test_add_selection_unrelated_pending_retains_goal(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for (finish_pending, cancel_pending) in [(false, false), (true, false), (false, true)] { + cx.set_state("a«bc\ndefgˇ»hi\nuvwxyz\nabcdef\n012345"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, true, window, cx); + editor.begin_selection(DisplayPoint::new(DisplayRow(4), 0), true, 1, window, cx); + if finish_pending { + editor.end_selection(window, cx); + } else if cancel_pending { + editor.cancel(&Cancel, window, cx); + } + add_selection_in_direction(editor, false, true, window, cx); + }); + let caret = if cancel_pending { "" } else { "ˇ" }; + cx.assert_editor_state(&format!( + "a«bcˇ»\nd«efgˇ»hi\nu«vwxˇ»yz\na«bcdˇ»ef\n{caret}012345" + )); + } +} + +#[gpui::test] +async fn test_add_selection_pending_overlap_cancel_reverses_group(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for skip_soft_wrap in [false, true] { + for above in [false, true] { + for overlap_source in [false, true] { + for restore_history in [false, true] { + cx.set_state("abcdef\nabcdef\nabcdef\nabcdefˇ"); + cx.update_editor(|editor, window, cx| { + let source_row = if above { 2 } else { 1 }; + let added_row = if above { 1 } else { 2 }; + let source = Point::new(source_row, 2); + editor.change_selections( + SelectionEffects::no_scroll(), + window, + cx, + |selections| { + selections.select_ranges([source..source]); + }, + ); + add_selection_in_direction(editor, above, skip_soft_wrap, window, cx); + begin_add_selection_overlap( + editor, + if overlap_source { + source_row + } else { + added_row + }, + 0..3, + window, + cx, + ); + editor.cancel(&Cancel, window, cx); + if restore_history { + editor.undo_selection(&UndoSelection, window, cx); + editor.redo_selection(&RedoSelection, window, cx); + } + assert_add_selection_ranges( + editor, + &[ + Point::new(1, 2)..Point::new(1, 2), + Point::new(2, 2)..Point::new(2, 2), + ], + cx, + ); + add_selection_in_direction(editor, !above, skip_soft_wrap, window, cx); + assert_add_selection_ranges(editor, &[source..source], cx); + editor.undo_selection(&UndoSelection, window, cx); + editor.redo_selection(&RedoSelection, window, cx); + assert_add_selection_ranges(editor, &[source..source], cx); + }); + } + } + } + } +} + +#[gpui::test] +async fn test_add_selection_pending_overlap_commit_or_consume(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for skip_soft_wrap in [false, true] { + for finish_pending in [false, true] { + for (above, overlap_row, columns) in [ + (false, 1, [(0, 0..3), (1, 0..3), (2, 2..2)]), + (false, 2, [(0, 2..2), (1, 0..3), (2, 0..3)]), + (true, 1, [(1, 0..3), (2, 0..3), (3, 2..2)]), + (true, 2, [(1, 2..2), (2, 0..3), (3, 0..3)]), + ] { + cx.set_state("abcdef\nabcdef\nabcdef\nabcdefˇ"); + cx.update_editor(|editor, window, cx| { + let source = Point::new(if above { 2 } else { 1 }, 2); + editor.change_selections( + SelectionEffects::no_scroll(), + window, + cx, + |selections| { + selections.select_ranges([source..source]); + }, + ); + add_selection_in_direction(editor, above, skip_soft_wrap, window, cx); + begin_add_selection_overlap(editor, overlap_row, 0..3, window, cx); + if finish_pending { + editor.end_selection(window, cx); + } + add_selection_in_direction(editor, !above, skip_soft_wrap, window, cx); + let expected = columns + .into_iter() + .map(|(row, columns)| { + Point::new(row, columns.start)..Point::new(row, columns.end) + }) + .collect::>(); + assert_add_selection_ranges(editor, &expected, cx); + assert_eq!(editor.selections.pending_anchor(), None); + }); + } + } + } +} + +#[gpui::test] +async fn test_add_selection_pending_overlap_multiline_goal(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for skip_soft_wrap in [false, true] { + for (above, initial, _, _) in multiline_add_selection_history_states() { + for (cancel_pending, finish_pending) in [(true, false), (false, true), (false, false)] { + cx.set_state(initial); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, above, skip_soft_wrap, window, cx); + begin_add_selection_overlap( + editor, + if above { 2 } else { 1 }, + 0..2, + window, + cx, + ); + if cancel_pending { + editor.cancel(&Cancel, window, cx); + editor.undo_selection(&UndoSelection, window, cx); + editor.redo_selection(&RedoSelection, window, cx); + } else if finish_pending { + editor.end_selection(window, cx); + } + add_selection_in_direction(editor, above, skip_soft_wrap, window, cx); + let columns = if cancel_pending { + [1..3, 1..4, 1..4, 1..4] + } else { + [1..3, 0..4, 0..4, 1..if skip_soft_wrap { 3 } else { 4 }] + }; + let expected = (0..4) + .map(|row| { + let columns = &columns[if above { 3 - row } else { row }]; + Point::new(row as u32, columns.start) + ..Point::new(row as u32, columns.end) + }) + .collect::>(); + assert_add_selection_ranges(editor, &expected, cx); + }); + } + } + } +} + +#[gpui::test] +async fn test_add_selection_pending_overlap_committed_source_changes(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for (above, initial, _, _) in multiline_add_selection_history_states() { + cx.set_state(initial); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, above, true, window, cx); + begin_add_selection_overlap(editor, if above { 2 } else { 1 }, 0..2, window, cx); + let snapshot = editor.display_snapshot(cx); + let source_row = if above { 3 } else { 0 }; + let selections = editor + .selections + .disjoint_anchors() + .iter() + .map(|selection| { + let mut selection = + selection.map(|anchor| anchor.to_point(snapshot.buffer_snapshot())); + if selection.start.row == source_row { + selection.end.column = 2; + } + selection + }) + .collect::>(); + let pending = *editor.selections.pending_anchor().expect("pending overlap"); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |collection| { + collection.select(selections); + collection.set_pending(pending, SelectMode::Character); + }); + editor.cancel(&Cancel, window, cx); + add_selection_in_direction(editor, above, true, window, cx); + let columns = [1..2, 1..4, 1..4, 1..2]; + let expected = (0..4) + .map(|row| { + let columns = &columns[if above { 3 - row } else { row }]; + Point::new(row as u32, columns.start)..Point::new(row as u32, columns.end) + }) + .collect::>(); + assert_add_selection_ranges(editor, &expected, cx); + }); + } +} + +#[gpui::test] +async fn test_add_selection_pending_overlap_retreat_after_buffer_edit(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for skip_soft_wrap in [false, true] { + for (above, initial, _, _) in multiline_add_selection_history_states() { + for (cancel_pending, finish_pending) in [(true, false), (false, true), (false, false)] { + cx.set_state(initial); + let overlap_row = if above { 2 } else { 1 }; + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, above, skip_soft_wrap, window, cx); + begin_add_selection_overlap(editor, overlap_row, 0..2, window, cx); + }); + cx.update_buffer(|buffer, cx| { + let start = Point::new(overlap_row, 0); + buffer.edit([(start..start, "ZZ")], None, cx); + }); + cx.update_editor(|editor, window, cx| { + editor.update_selection( + DisplayPoint::new(DisplayRow(overlap_row), 0), + 0, + gpui::Point::::default(), + window, + cx, + ); + if cancel_pending { + editor.cancel(&Cancel, window, cx); + } else if finish_pending { + editor.end_selection(window, cx); + } + add_selection_in_direction(editor, above, skip_soft_wrap, window, cx); + let columns = [1..3, 3..6, 1..4, 1..if skip_soft_wrap { 6 } else { 4 }]; + let mut expected = (0..4) + .map(|row| { + let columns = &columns[if above { 3 - row } else { row }]; + Point::new(row as u32, columns.start) + ..Point::new(row as u32, columns.end) + }) + .collect::>(); + if !cancel_pending { + expected.extend((1..3).map(|row| Point::new(row, 0)..Point::new(row, 0))); + expected.sort_by_key(|range| range.start); + } + assert_add_selection_ranges(editor, &expected, cx); + }); + } + } + } +} + +#[gpui::test] +async fn test_add_selection_pending_overlap_absorbs_multiline_source(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for skip_soft_wrap in [false, true] { + for (cancel_pending, finish_pending) in [(true, false), (false, true), (false, false)] { + cx.set_state("a«bc\ndefgˇ»hi\nuv\nabcdef"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, skip_soft_wrap, window, cx); + editor.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx); + editor.update_selection( + DisplayPoint::new(DisplayRow(1), 4), + 0, + gpui::Point::::default(), + window, + cx, + ); + if cancel_pending { + editor.cancel(&Cancel, window, cx); + } else if finish_pending { + editor.end_selection(window, cx); + } + add_selection_in_direction(editor, false, skip_soft_wrap, window, cx); + let columns = if cancel_pending { + [1..3, 1..4, 1..2, 1..4] + } else { + [0..3, 0..4, 0..2, 1..2] + }; + let expected = columns + .into_iter() + .enumerate() + .map(|(row, columns)| { + Point::new(row as u32, columns.start)..Point::new(row as u32, columns.end) + }) + .collect::>(); + assert_add_selection_ranges(editor, &expected, cx); + }); + } + } +} + +#[gpui::test] +async fn test_add_selection_goal_changes_are_group_local(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for remote in [false, true] { + for (changed_row, columns, goal_end) in [ + (0, Some(1..2), 2), + (1, Some(1..2), 3), + (0, None, 4), + (1, None, 4), + (1, Some(1..4), 4), + ] { + cx.set_state( + "a«bc\ndefgˇ»hi\nuvwxyz\nabcdef\n\na«bc\ndefgˇ»hi\nuvwxyz\nabcdef\n012345", + ); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, true, window, cx); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { + selections.insert_range(Point::new(9, 0)..Point::new(9, 0)); + }); + let snapshot = editor.display_snapshot(cx); + let selections = editor + .selections + .all_unexpanded::(&snapshot) + .into_iter() + .filter_map(|mut selection| { + if selection.start.row == changed_row { + let columns = columns.as_ref()?; + selection.start.column = columns.start; + selection.end.column = columns.end; + } + Some(selection) + }) + .collect::>(); + if remote { + let selections = selections + .into_iter() + .map(|selection| { + selection.map(|point| snapshot.buffer_snapshot().anchor_before(point)) + }) + .collect(); + editor.set_selections_from_remote(selections, None, window, cx); + } else { + editor.change_selections( + SelectionEffects::no_scroll(), + window, + cx, + |collection| { + collection.select(selections); + }, + ); + } + add_selection_in_direction(editor, false, true, window, cx); + let expected = (0..10) + .filter_map(|row| { + let columns = if row == changed_row { + columns.clone()? + } else { + match row { + 0 | 5 => 1..3, + 1 | 2 | 6 | 7 | 8 => 1..4, + 3 => 1..goal_end, + 9 => 0..0, + _ => return None, + } + }; + Some(Point::new(row, columns.start)..Point::new(row, columns.end)) + }) + .collect::>(); + assert_add_selection_ranges(editor, &expected, cx); + }); + } + } +} + +#[gpui::test] +async fn test_add_selection_overlap_invalidates_goal(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for (pending, finish_pending) in [(false, false), (true, false), (true, true)] { + for preceding_range in [false, true] { + for (columns, pending_goal, inserted_goal) in + [(0..1, 4, 4), (0..2, 3, 3), (4..4, 3, 4), (4..5, 4, 4)] + { + cx.set_state("a«bc\ndefgˇ»hi\nuvwxyz\nabcdef"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, true, window, cx); + if preceding_range { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { + selections.insert_range(Point::new(1, 0)..Point::new(1, 1)); + }); + } + let snapshot = editor.display_snapshot(cx); + let range = snapshot.buffer_snapshot().anchor_before(Point::new(1, columns.start)) + ..snapshot.buffer_snapshot().anchor_before(Point::new(1, columns.end)); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { + if pending { + selections.set_pending_anchor_range(range, SelectMode::Character); + } else { + selections.insert_range(range); + } + }); + if finish_pending { + editor.end_selection(window, cx); + } + add_selection_in_direction(editor, false, true, window, cx); + let goal_end = if pending { pending_goal } else { inserted_goal }; + assert_eq!( + editor.selections.ranges::(&editor.display_snapshot(cx)) + .into_iter() + .filter(|range| range.start.row == 3) + .collect::>(), + [Point::new(3, 1)..Point::new(3, goal_end)], + "pending={pending}, finish_pending={finish_pending}, preceding_range={preceding_range}, columns={columns:?}", + ); + }); + } + } + } +} + +#[gpui::test] +async fn test_add_selection_sync_preserves_unchanged_goals(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for subscribe_before in [false, true] { + for move_source in [false, true] { + cx.set_state("a«bc\ndefgˇ»hi\nuvwxyz\nabcdef"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, true, window, cx); + }); + let (_source, _subscription) = cx.update_editor(|editor, window, cx| { + let source = cx.new(|cx| editor.clone(window, cx)); + let subscription = + subscribe_before.then(|| editor.sync_selections(source.clone(), cx)); + source.update(cx, |source, cx| { + source.change_selections( + SelectionEffects::no_scroll(), + window, + cx, + |collection| { + let mut selections = collection.all_unexpanded::( + &collection.display_snapshot(), + ); + for selection in &mut selections { + if move_source && selection.start == MultiBufferOffset(1) { + selection.end = MultiBufferOffset(2); + } + } + collection.select(selections); + }, + ); + }); + let subscription = + subscription.unwrap_or_else(|| editor.sync_selections(source.clone(), cx)); + (source, subscription) + }); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, true, window, cx); + }); + cx.assert_editor_state(if move_source { + "a«bˇ»c\nd«efgˇ»hi\nu«vwxˇ»yz\na«bˇ»cdef" + } else { + "a«bcˇ»\nd«efgˇ»hi\nu«vwxˇ»yz\na«bcdˇ»ef" + }); + } + } +} + +#[gpui::test] +async fn test_add_selection_unwrapped_goal_reseeds_from_oldest_survivor(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("a«bc\ndefgˇ»hi\nuv\nabcdef"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, true, window, cx); + let oldest_id = editor.selections.oldest_anchor().id; + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { + selections.delete(oldest_id); + }); + add_selection_in_direction(editor, false, true, window, cx); + }); + cx.assert_editor_state("abc\nd«efgˇ»hi\nu«vˇ»\na«bcdˇ»ef"); +} + +#[gpui::test] +async fn test_add_selection_unwrapped_multiline_goal_follows_buffer_edits(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for add_pending in [false, true] { + cx.set_state("a«bc\ndefgˇ»hi\nuvwxyz\nabcdefgh"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, true, window, cx); + }); + cx.update_buffer(|buffer, cx| { + buffer.edit([(Point::new(1, 0)..Point::new(1, 0), "ZZ")], None, cx); + }); + cx.update_editor(|editor, window, cx| { + if add_pending { + editor.begin_selection(DisplayPoint::new(DisplayRow(3), 0), true, 1, window, cx); + editor.end_selection(window, cx); + } + add_selection_in_direction(editor, false, true, window, cx); + }); + let caret = if add_pending { "ˇ" } else { "" }; + cx.assert_editor_state(&format!( + "a«bcˇ»\nZZd«efgˇ»hi\nu«vwxˇ»yz\n{caret}a«bcdefˇ»gh" + )); + } +} + +#[gpui::test] +async fn test_add_selection_unwrapped_multiline_goal_follows_tab_size(cx: &mut TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4); + }); + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("a«bc\n\tˇ»defgh\nabcdefghij\nabcdefghij"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, true, window, cx); + }); + cx.assert_editor_state("a«bcˇ»\n«\tˇ»defgh\na«bcdˇ»efghij\nabcdefghij"); + for (tab_size, expected) in [ + (8, "a«bcˇ»\n«\tˇ»defgh\na«bcdˇ»efghij\na«bcdefghˇ»ij"), + (4, "a«bcˇ»\n«\tˇ»defgh\na«bcdˇ»efghij\na«bcdˇ»efghij"), + ] { + update_test_language_settings(&mut cx, &|settings| { + settings.defaults.tab_size = NonZeroU32::new(tab_size); + }); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, true, window, cx); + }); + cx.assert_editor_state(expected); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, true, true, window, cx); + }); + } +} + +#[gpui::test] +async fn test_add_selection_unwrapped_multiline_goal_follows_inlays(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("a«bc\ndefgˇ»hi\nabcdefghij\nabcdefghij"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, true, window, cx); + let position = editor + .buffer + .read(cx) + .snapshot(cx) + .anchor_before(Point::new(1, 0)); + editor.splice_inlays(&[], vec![inlays::Inlay::mock_hint(0, position, "HH")], cx); + add_selection_in_direction(editor, false, true, window, cx); + }); + cx.assert_editor_state("a«bcˇ»\nd«efgˇ»hi\na«bcdˇ»efghij\na«bcdefˇ»ghij"); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, true, true, window, cx); + editor.splice_inlays(&[project::InlayId::Hint(0)], Vec::new(), cx); + add_selection_in_direction(editor, false, true, window, cx); + }); + cx.assert_editor_state("a«bcˇ»\nd«efgˇ»hi\na«bcdˇ»efghij\na«bcdˇ»efghij"); +} + +#[gpui::test] +async fn test_add_selection_unwrapped_clears_pixel_goals(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("abˇcdef\nabcdef"); + cx.update_editor(|editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { + selections.move_with(&mut |_, selection| { + selection.goal = SelectionGoal::HorizontalRange { + start: 0.0, + end: 200.0, + }; + }); + }); + add_selection_in_direction(editor, false, true, window, cx); + assert_eq!( + editor + .selections + .all::(&editor.display_snapshot(cx)) + .iter() + .map(|selection| selection.goal) + .collect::>(), + [SelectionGoal::None, SelectionGoal::None], + ); + }); + cx.assert_editor_state("abˇcdef\nabˇcdef"); +} + +#[gpui::test] +async fn test_add_selection_skips_inlay_inside_grapheme(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + for (prefix, row) in [ + (String::new(), 0), + (format!("{}\n\n\n", "a".repeat(121)), 3), + ] { + cx.set_state(&format!("{prefix}👩\u{200d}💻|Z\nˇ")); + cx.update_editor(|editor, window, cx| { + let buffer = editor.buffer.read(cx).snapshot(cx); + let position = buffer.anchor_before(Point::new(row, 4)); + editor.splice_inlays(&[], vec![inlays::Inlay::mock_hint(0, position, "\n")], cx); + add_selection_in_direction(editor, true, true, window, cx); + assert_eq!( + editor + .selections + .ranges::(&editor.display_snapshot(cx)), + [ + Point::new(row, 0)..Point::new(row, 0), + Point::new(row + 1, 0)..Point::new(row + 1, 0) + ] + ); + }); + } +} + +#[gpui::test] +async fn test_add_selection_unwrapped_en_dash(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + + for columns in [[4..4, 2..2], [3..4, 1..2], [4..6, 2..4]] { + for above in [false, true] { + for reversed in [false, true] { + cx.set_state("–x|Z\nab|Zˇ"); + cx.update_editor(|editor, window, cx| { + assert_add_selection_pair(editor, columns.clone(), above, reversed, window, cx); + }); + } + } + } +} + +#[gpui::test] +async fn test_add_selection_unwrapped_unicode_tabs(cx: &mut TestAppContext) { + init_test(cx, |settings| { + settings.defaults.hard_tabs = Some(true); + settings.defaults.tab_size = Some(4.try_into().expect("nonzero tab size")); + }); + let mut cx = EditorTestContext::new(cx).await; + + for columns in [[8..8, 4..4], [6..8, 3..4], [8..10, 4..6]] { + for above in [false, true] { + for reversed in [false, true] { + cx.set_state("–é\tλ|Z\nab\tc|Zˇ"); + cx.update_editor(|editor, window, cx| { + assert_add_selection_pair(editor, columns.clone(), above, reversed, window, cx); + }); + } + } + } +} + +#[gpui::test] +async fn test_add_selection_unwrapped_source_range_across_wraps(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + + for wrap_width in [None, Some(px(60.0)), Some(px(90.0))] { + for columns in [3..18, 3..20, 18..18] { + for above in [false, true] { + for reversed in [false, true] { + cx.set_state(" abcdefghijklmnop|Z\n ab cd efghijklmnop|Zˇ"); + cx.update_editor(|editor, window, cx| { + editor.set_wrap_width(wrap_width, cx); + if wrap_width.is_some() { + let snapshot = editor.display_snapshot(cx); + for row in 0..2 { + assert_ne!( + Point::new(row, 3).to_display_point(&snapshot).row(), + Point::new(row, 18).to_display_point(&snapshot).row(), + ); + } + } + assert_add_selection_pair( + editor, + [columns.clone(), columns.clone()], + above, + reversed, + window, + cx, + ); + }); + } + } + } + } +} + +#[gpui::test] +async fn test_add_selection_unwrapped_clamp_retains_goal(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + + for (before, intermediate, after, above) in [ + ( + "–abcˇ|Z\nx\nwxyz|Z", + "–abcˇ|Z\nxˇ\nwxyz|Z", + "–abcˇ|Z\nxˇ\nwxyzˇ|Z", + false, + ), + ( + "wxyz|Z\nx\n–abcˇ|Z", + "wxyz|Z\nxˇ\n–abcˇ|Z", + "wxyzˇ|Z\nxˇ\n–abcˇ|Z", + true, + ), + ( + "–a«ˇbc|Z»\nxyz\nuvwxyz", + "–a«ˇbc|Z»\nxy«ˇz»\nuvwxyz", + "–a«ˇbc|Z»\nxy«ˇz»\nuv«ˇwxyz»", + false, + ), + ( + "uvwxyz\nxyz\n–a«bc|Zˇ»", + "uvwxyz\nxy«zˇ»\n–a«bc|Zˇ»", + "uv«wxyzˇ»\nxy«zˇ»\n–a«bc|Zˇ»", + true, + ), + ] { + cx.set_state(before); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, above, true, window, cx); + }); + cx.assert_editor_state(intermediate); + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, above, true, window, cx); + }); + cx.assert_editor_state(after); + } +} + +#[gpui::test] +async fn test_add_selection_unwrapped_multiple_groups(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("–aˇbcdef\nx\nabcdef\nabc«ˇde»f\nxy\nabcdef"); + + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, true, window, cx); + }); + cx.assert_editor_state("–aˇbcdef\nxˇ\nabcdef\nabc«ˇde»f\nxy\nabc«ˇde»f"); + + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, false, true, window, cx); + }); + cx.assert_editor_state("–aˇbcdef\nxˇ\nabˇcdef\nabc«ˇde»f\nxy\nabc«ˇde»f"); + + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, true, true, window, cx); + }); + cx.assert_editor_state("–aˇbcdef\nxˇ\nabcdef\nabc«ˇde»f\nxy\nabcdef"); + + cx.update_editor(|editor, window, cx| { + add_selection_in_direction(editor, true, true, window, cx); + }); + cx.assert_editor_state("–aˇbcdef\nx\nabc«ˇde»f\nabc«ˇde»f\nxy\nabcdef"); +} + +#[gpui::test] +async fn test_add_selection_unwrapped_mode_switch_resets_groups(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + + for skip_soft_wrap in [false, true] { + cx.set_state("abˇcdefghijklmnop\nqrstuvwxyzABCDEF\n0123456789abcdef"); + cx.update_editor(|editor, window, cx| { + editor.set_wrap_width(None, cx); + let details = editor.text_layout_details(window, cx); + let width = editor + .display_snapshot(cx) + .layout_row(DisplayRow(0), &details) + .width + / 2.0; + editor.set_wrap_width(Some(width), cx); + assert_eq!( + editor.display_text(cx), + "abcdefgh\nijklmnop\nqrstuvwx\nyzABCDEF\n01234567\n89abcdef", + ); + add_selection_in_direction(editor, false, skip_soft_wrap, window, cx); + add_selection_in_direction(editor, skip_soft_wrap, !skip_soft_wrap, window, cx); + }); + if skip_soft_wrap { + cx.assert_editor_state("abˇcdefghijˇklmnop\nqrˇstuvwxyzABCDEF\n0123456789abcdef"); + } else { + cx.assert_editor_state("abˇcdefghijˇklmnop\nqrˇstuvwxyzˇABCDEF\n0123456789abcdef"); + } + } +} + +#[gpui::test] +async fn test_add_selection_unwrapped_eof_and_skipped_range_terminate(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + + for state in [ + "abcdefghijklmnopˇ", + "abcdefghijklmnop\nqrstuvwxyzABCDEFˇ", + "abcdefghijkl«mnopˇ»\nabcdefgh", + ] { + cx.set_state(state); + cx.update_editor(|editor, window, cx| { + editor.set_wrap_width(None, cx); + let details = editor.text_layout_details(window, cx); + let width = editor + .display_snapshot(cx) + .layout_row(DisplayRow(0), &details) + .width + / 4.0; + editor.set_wrap_width(Some(width), cx); + assert!(editor.display_snapshot(cx).max_point().row().0 >= 3); + for _ in 0..2 { + add_selection_in_direction(editor, false, true, window, cx); + } + }); + cx.assert_editor_state(state); + } +} + +#[gpui::test] +async fn test_add_selection_unwrapped_inlay_rows_and_widths(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + + for columns in [[1..1, 3..3], [1..3, 3..5]] { + for above in [false, true] { + cx.set_state("a|Z\nabc|Zˇ"); + cx.update_editor(|editor, window, cx| { + let position = editor + .buffer + .read(cx) + .snapshot(cx) + .anchor_before(Point::new(0, 0)); + editor.splice_inlays( + &[], + vec![inlays::Inlay::mock_hint(0, position, "hint\n–é")], + cx, + ); + assert_eq!(editor.display_text(cx), "hint\n–éa|Z\nabc|Z"); + assert_add_selection_pair(editor, columns.clone(), above, false, window, cx); + editor.splice_inlays(&[project::InlayId::Hint(0)], Vec::new(), cx); + }); + } + } +} + +#[gpui::test] +async fn test_add_selection_unwrapped_folded_target_boundaries(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + + for (source, target) in [(4..4, 2..2), (5..5, 8..8), (4..5, 2..8)] { + for above in [false, true] { + for reversed in [false, true] { + cx.set_state(if above { + "abhidden|Z\na–|Zˇ" + } else { + "a–|Z\nabhidden|Zˇ" + }); + cx.update_editor(|editor, window, cx| { + let target_row = u32::from(!above); + editor.fold_creases( + vec![Crease::simple( + Point::new(target_row, 2)..Point::new(target_row, 8), + FoldPlaceholder::test(), + )], + true, + window, + cx, + ); + assert_eq!( + editor.display_text(cx), + if above { + "ab⋯|Z\na–|Z" + } else { + "a–|Z\nab⋯|Z" + } + ); + let columns = if above { + [target.clone(), source.clone()] + } else { + [source.clone(), target.clone()] + }; + assert_add_selection_pair(editor, columns, above, reversed, window, cx); + }); + } + } + } +} + +#[gpui::test] +async fn test_add_selection_unwrapped_multiline_fold_rows(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + + for above in [false, true] { + cx.set_state(if above { + "abhidden\nhidden\n|Z\na–ˇ|Z" + } else { + "a–ˇ|Z\nabhidden\nhidden\n|Z" + }); + cx.update_editor(|editor, window, cx| { + let target_row = u32::from(!above); + editor.fold_creases( + vec![Crease::simple( + Point::new(target_row, 2)..Point::new(target_row + 2, 0), + FoldPlaceholder::test(), + )], + true, + window, + cx, + ); + assert_eq!( + editor.display_text(cx), + if above { + "ab⋯|Z\na–|Z" + } else { + "a–|Z\nab⋯|Z" + } + ); + add_selection_in_direction(editor, above, true, window, cx); + }); + cx.assert_editor_state(if above { + "abˇhidden\nhidden\n|Z\na–ˇ|Z" + } else { + "a–ˇ|Z\nabˇhidden\nhidden\n|Z" + }); + } +} + /// A different number of tabs can align the same column on each row, so a cursor has to /// be placed by the column the tabs expand to. Counting a tab as a single column lands it /// wherever that many characters happen to reach on the next row. @@ -47171,3 +49148,125 @@ fn document_highlight_count(cx: &mut EditorLspTestContext) -> usize { .map_or(0, |(_, ranges)| ranges.len()) }) } + +#[track_caller] +fn assert_add_selection_pair( + editor: &mut Editor, + columns: [Range; 2], + above: bool, + reversed: bool, + window: &mut Window, + cx: &mut Context, +) { + let ranges = columns + .into_iter() + .enumerate() + .map(|(row, columns)| { + Point::new(row as u32, columns.start)..Point::new(row as u32, columns.end) + }) + .collect::>(); + let source = &ranges[usize::from(above)]; + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { + selections.select_ranges([if reversed { + source.end..source.start + } else { + source.clone() + }]); + }); + add_selection_in_direction(editor, above, true, window, cx); + let selections = editor.selections.all::(&editor.display_snapshot(cx)); + assert_eq!( + selections + .iter() + .map(|selection| (selection.range(), selection.reversed)) + .collect::>(), + ranges + .into_iter() + .map(|range| { + let reversed = reversed && !range.is_empty(); + (range, reversed) + }) + .collect::>(), + "above={above}, reversed={reversed}", + ); +} + +#[track_caller] +fn assert_add_selection_ranges( + editor: &mut Editor, + expected: &[Range], + cx: &mut Context, +) { + assert_eq!( + editor + .selections + .ranges::(&editor.display_snapshot(cx)), + expected, + ); +} + +fn stored_selection_ranges(editor: &Editor, cx: &App) -> Vec> { + let buffer = editor.buffer.read(cx).snapshot(cx); + editor + .selections + .disjoint_anchors() + .iter() + .chain(editor.selections.pending_anchor()) + .map(|selection| selection.range().to_point(&buffer)) + .collect() +} + +fn add_selection_in_direction( + editor: &mut Editor, + above: bool, + skip_soft_wrap: bool, + window: &mut Window, + cx: &mut Context, +) { + if above { + editor.add_selection_above(&AddSelectionAbove { skip_soft_wrap }, window, cx); + } else { + editor.add_selection_below(&AddSelectionBelow { skip_soft_wrap }, window, cx); + } +} + +fn begin_add_selection_overlap( + editor: &mut Editor, + row: u32, + columns: Range, + window: &mut Window, + cx: &mut Context, +) { + editor.begin_selection( + DisplayPoint::new(DisplayRow(row), columns.start), + true, + 1, + window, + cx, + ); + editor.update_selection( + DisplayPoint::new(DisplayRow(row), columns.end), + 0, + gpui::Point::::default(), + window, + cx, + ); +} + +fn multiline_add_selection_history_states() -> [(bool, &'static str, &'static str, &'static str); 2] +{ + [ + ( + false, + "a«bc\ndefgˇ»hi\nuvwxyz\nabcdef", + "a«bcˇ»\nd«efgˇ»hi\nu«vwxˇ»yz\nabcdef", + "a«bcˇ»\nd«efgˇ»hi\nu«vwxˇ»yz\na«bcdˇ»ef", + ), + ( + true, + "abcdef\nuvwxyz\ndefg«hi\naˇ»bc", + "abcdef\nu«vwxˇ»yz\nd«efgˇ»hi\na«bcˇ»", + "a«bcdˇ»ef\nu«vwxˇ»yz\nd«efgˇ»hi\na«bcˇ»", + ), + ] +} diff --git a/crates/editor/src/element/mouse.rs b/crates/editor/src/element/mouse.rs index c82edb9f02..52f9677d39 100644 --- a/crates/editor/src/element/mouse.rs +++ b/crates/editor/src/element/mouse.rs @@ -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, diff --git a/crates/editor/src/fold.rs b/crates/editor/src/fold.rs index c5b6459bc7..73aa6b3058 100644 --- a/crates/editor/src/fold.rs +++ b/crates/editor/src/fold.rs @@ -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, diff --git a/crates/editor/src/selection.rs b/crates/editor/src/selection.rs index d8d54ba48c..875faf9811 100644 --- a/crates/editor/src/selection.rs +++ b/crates/editor/src/selection.rs @@ -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, cx: &mut Context, ) -> 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::(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::(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, 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) { let snapshot = self.display_snapshot(cx); let selections = self.selections.all::(&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, ) { 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, ) -> 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]>, + ) { + 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::>(); + let unchanged = previous + .iter() + .filter(|selection| current.get(&selection.id) == Some(selection)) + .map(|selection| selection.id) + .collect::>(); + let removed = previous + .iter() + .filter(|selection| !current.contains_key(&selection.id)) + .map(|selection| selection.id) + .collect::>(); + 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>, cx: &mut Context) { + if !selections.is_empty() { + self.change_selections_without_effects(&self.display_snapshot(cx), |collection| { + collection.select_anchors(selections); + }); + } + } + + fn retain_add_selection_goals( + state: &mut AddSelectionsState, + unchanged: &HashSet, + removed: &HashSet, + current: &HashMap>, + ) { + 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], + current: &[Selection], + ) { + let current_by_id = current + .iter() + .map(|selection| (selection.id, selection)) + .collect::>(); + 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], + snapshot: &DisplaySnapshot, + ) -> Vec> { + 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( + &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( + &mut self, + effects: SelectionEffects, + history_entry: Option, + window: &mut Window, + cx: &mut Context, + 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, ) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let all_selections = self.selections.all::(&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::(&display_map); + let goal_source_for_selection = |selection: &Selection| { + 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::>(); + let mut columnar_rows = ColumnarSelectionRows::new(&display_map); + let (mut columnar_selections, new_selections_to_columnarize) = + all_selections + .into_iter() + .partition::, _>(|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::>(); + 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::>(); + 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::>(); + 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::(&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::(&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; diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index b2e32fd4c7..f358bb6b78 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -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(&self, snapshot: &DisplaySnapshot) -> Vec> + where + D: MultiBufferDimension + Sub + AddAssign<::Output> + Ord, + { + self.all_iter(snapshot, false).collect() + } + pub(crate) fn pending_mode(&self) -> Option { self.pending.as_ref().map(|pending| pending.mode.clone()) } @@ -123,19 +132,21 @@ impl SelectionsCollection { where D: MultiBufferDimension + Sub + AddAssign<::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> where D: 'a + MultiBufferDimension + Sub + AddAssign<::Output> + Ord, { let mut disjoint = - resolve_selections_wrapping_blocks::(self.disjoint.iter(), snapshot).peekable(); - let mut pending_opt = self.pending::(snapshot); + resolve_selections::(self.disjoint.iter(), snapshot, expand_blocks).peekable(); + let mut pending_opt = + resolve_selections::(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(&self, snapshot: &DisplaySnapshot) -> Selection @@ -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> { + 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, - reversed: bool, - text_layout_details: &TextLayoutDetails, - ) -> Option> { - 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, bool)], + ) -> Vec>> { + let row_queries = queries + .iter() + .map(|(row, columns, _)| (*row, columns.clone())) + .collect::>(); + 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, Range)], above: bool, - goal_columns: &Range, - reversed: bool, - text_layout_details: &TextLayoutDetails, - ) -> Option> { - 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>> { + 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::>(); + 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( @@ -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(&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>) { + 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> +where + D: MultiBufferDimension + Sub + AddAssign<::Output> + Ord, + I: 'a + IntoIterator>, +{ + resolve_selections(selections, map, true) +} + +fn resolve_selections<'a, D, I>( + selections: I, + map: &'a DisplaySnapshot, + expand_blocks: bool, +) -> impl 'a + Iterator> where D: MultiBufferDimension + Sub + AddAssign<::Output> + Ord, I: 'a + IntoIterator>, { // 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::>(); + let anchors = saved + .iter() + .map(|selection| { + selection_to_anchor_selection( + selection.map(|point| point.to_offset(&snapshot)), + &snapshot, + ) + }) + .collect::>(); + let mut collection = SelectionsCollection::new(); + collection.change_with(&snapshot, |selections| { + selections.select_anchors_unexpanded(anchors); + }); + assert_eq!(collection.all_unexpanded::(&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::(&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::>(); + 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::>().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::>(); + 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::>().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, + reversed: bool, + ) -> Selection { + 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 { + 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) {