diff --git a/crates/editor2/Cargo.toml b/crates/editor2/Cargo.toml new file mode 100644 index 0000000000..5113b5e7de --- /dev/null +++ b/crates/editor2/Cargo.toml @@ -0,0 +1,92 @@ +[package] +name = "editor" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/editor.rs" +doctest = false + +[features] +test-support = [ + "copilot/test-support", + "text/test-support", + "language/test-support", + "gpui/test-support", + "multi_buffer/test-support", + "project/test-support", + "util/test-support", + "workspace/test-support", + "tree-sitter-rust", + "tree-sitter-typescript" +] + +[dependencies] +client = { path = "../client" } +clock = { path = "../clock" } +copilot = { path = "../copilot" } +db = { path = "../db" } +drag_and_drop = { path = "../drag_and_drop" } +collections = { path = "../collections" } +context_menu = { path = "../context_menu" } +fuzzy = { path = "../fuzzy" } +git = { path = "../git" } +gpui = { path = "../gpui" } +language = { path = "../language" } +lsp = { path = "../lsp" } +multi_buffer = { path = "../multi_buffer" } +project = { path = "../project" } +rpc = { path = "../rpc" } +rich_text = { path = "../rich_text" } +settings = { path = "../settings" } +snippet = { path = "../snippet" } +sum_tree = { path = "../sum_tree" } +text = { path = "../text" } +theme = { path = "../theme" } +util = { path = "../util" } +sqlez = { path = "../sqlez" } +workspace = { path = "../workspace" } + +aho-corasick = "1.1" +anyhow.workspace = true +convert_case = "0.6.0" +futures.workspace = true +indoc = "1.0.4" +itertools = "0.10" +lazy_static.workspace = true +log.workspace = true +ordered-float.workspace = true +parking_lot.workspace = true +postage.workspace = true +rand.workspace = true +schemars.workspace = true +serde.workspace = true +serde_derive.workspace = true +smallvec.workspace = true +smol.workspace = true + +tree-sitter-rust = { workspace = true, optional = true } +tree-sitter-html = { workspace = true, optional = true } +tree-sitter-typescript = { workspace = true, optional = true } + +[dev-dependencies] +copilot = { path = "../copilot", features = ["test-support"] } +text = { path = "../text", features = ["test-support"] } +language = { path = "../language", features = ["test-support"] } +lsp = { path = "../lsp", features = ["test-support"] } +gpui = { path = "../gpui", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } +project = { path = "../project", features = ["test-support"] } +settings = { path = "../settings", features = ["test-support"] } +workspace = { path = "../workspace", features = ["test-support"] } +multi_buffer = { path = "../multi_buffer", features = ["test-support"] } + +ctor.workspace = true +env_logger.workspace = true +rand.workspace = true +unindent.workspace = true +tree-sitter.workspace = true +tree-sitter-rust.workspace = true +tree-sitter-html.workspace = true +tree-sitter-typescript.workspace = true diff --git a/crates/editor2/src/blink_manager.rs b/crates/editor2/src/blink_manager.rs new file mode 100644 index 0000000000..fa5a3af0c6 --- /dev/null +++ b/crates/editor2/src/blink_manager.rs @@ -0,0 +1,113 @@ +use crate::EditorSettings; +use gpui::{Entity, ModelContext}; +use settings::SettingsStore; +use smol::Timer; +use std::time::Duration; + +pub struct BlinkManager { + blink_interval: Duration, + + blink_epoch: usize, + blinking_paused: bool, + visible: bool, + enabled: bool, +} + +impl BlinkManager { + pub fn new(blink_interval: Duration, cx: &mut ModelContext) -> Self { + // Make sure we blink the cursors if the setting is re-enabled + cx.observe_global::(move |this, cx| { + this.blink_cursors(this.blink_epoch, cx) + }) + .detach(); + + Self { + blink_interval, + + blink_epoch: 0, + blinking_paused: false, + visible: true, + enabled: false, + } + } + + fn next_blink_epoch(&mut self) -> usize { + self.blink_epoch += 1; + self.blink_epoch + } + + pub fn pause_blinking(&mut self, cx: &mut ModelContext) { + self.show_cursor(cx); + + let epoch = self.next_blink_epoch(); + let interval = self.blink_interval; + cx.spawn(|this, mut cx| { + let this = this.downgrade(); + async move { + Timer::after(interval).await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx)) + } + } + }) + .detach(); + } + + fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ModelContext) { + if epoch == self.blink_epoch { + self.blinking_paused = false; + self.blink_cursors(epoch, cx); + } + } + + fn blink_cursors(&mut self, epoch: usize, cx: &mut ModelContext) { + if settings::get::(cx).cursor_blink { + if epoch == self.blink_epoch && self.enabled && !self.blinking_paused { + self.visible = !self.visible; + cx.notify(); + + let epoch = self.next_blink_epoch(); + let interval = self.blink_interval; + cx.spawn(|this, mut cx| { + let this = this.downgrade(); + async move { + Timer::after(interval).await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx)); + } + } + }) + .detach(); + } + } else { + self.show_cursor(cx); + } + } + + pub fn show_cursor(&mut self, cx: &mut ModelContext<'_, BlinkManager>) { + if !self.visible { + self.visible = true; + cx.notify(); + } + } + + pub fn enable(&mut self, cx: &mut ModelContext) { + self.enabled = true; + // Set cursors as invisible and start blinking: this causes cursors + // to be visible during the next render. + self.visible = false; + self.blink_cursors(self.blink_epoch, cx); + } + + pub fn disable(&mut self, _cx: &mut ModelContext) { + self.enabled = false; + } + + pub fn visible(&self) -> bool { + self.visible + } +} + +impl Entity for BlinkManager { + type Event = (); +} diff --git a/crates/editor2/src/display_map.rs b/crates/editor2/src/display_map.rs new file mode 100644 index 0000000000..1d6deb910a --- /dev/null +++ b/crates/editor2/src/display_map.rs @@ -0,0 +1,1912 @@ +mod block_map; +mod fold_map; +mod inlay_map; +mod tab_map; +mod wrap_map; + +use crate::{ + link_go_to_definition::InlayHighlight, movement::TextLayoutDetails, Anchor, AnchorRangeExt, + EditorStyle, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, +}; +pub use block_map::{BlockMap, BlockPoint}; +use collections::{BTreeMap, HashMap, HashSet}; +use fold_map::FoldMap; +use gpui::{ + color::Color, + fonts::{FontId, HighlightStyle, Underline}, + text_layout::{Line, RunStyle}, + Entity, ModelContext, ModelHandle, +}; +use inlay_map::InlayMap; +use language::{ + language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription, +}; +use lsp::DiagnosticSeverity; +use std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc}; +use sum_tree::{Bias, TreeMap}; +use tab_map::TabMap; +use wrap_map::WrapMap; + +pub use block_map::{ + BlockBufferRows as DisplayBufferRows, BlockChunks as DisplayChunks, BlockContext, + BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock, +}; + +pub use self::fold_map::FoldPoint; +pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum FoldStatus { + Folded, + Foldable, +} + +pub trait ToDisplayPoint { + fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint; +} + +type TextHighlights = TreeMap, Arc<(HighlightStyle, Vec>)>>; +type InlayHighlights = BTreeMap>; + +pub struct DisplayMap { + buffer: ModelHandle, + buffer_subscription: BufferSubscription, + fold_map: FoldMap, + inlay_map: InlayMap, + tab_map: TabMap, + wrap_map: ModelHandle, + block_map: BlockMap, + text_highlights: TextHighlights, + inlay_highlights: InlayHighlights, + pub clip_at_line_ends: bool, +} + +impl Entity for DisplayMap { + type Event = (); +} + +impl DisplayMap { + pub fn new( + buffer: ModelHandle, + font_id: FontId, + font_size: f32, + wrap_width: Option, + buffer_header_height: u8, + excerpt_header_height: u8, + cx: &mut ModelContext, + ) -> Self { + let buffer_subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); + + let tab_size = Self::tab_size(&buffer, cx); + let (inlay_map, snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx)); + let (fold_map, snapshot) = FoldMap::new(snapshot); + let (tab_map, snapshot) = TabMap::new(snapshot, tab_size); + let (wrap_map, snapshot) = WrapMap::new(snapshot, font_id, font_size, wrap_width, cx); + let block_map = BlockMap::new(snapshot, buffer_header_height, excerpt_header_height); + cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach(); + DisplayMap { + buffer, + buffer_subscription, + fold_map, + inlay_map, + tab_map, + wrap_map, + block_map, + text_highlights: Default::default(), + inlay_highlights: Default::default(), + clip_at_line_ends: false, + } + } + + pub fn snapshot(&mut self, cx: &mut ModelContext) -> DisplaySnapshot { + let buffer_snapshot = self.buffer.read(cx).snapshot(cx); + let edits = self.buffer_subscription.consume().into_inner(); + let (inlay_snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits); + let (fold_snapshot, edits) = self.fold_map.read(inlay_snapshot.clone(), edits); + let tab_size = Self::tab_size(&self.buffer, cx); + let (tab_snapshot, edits) = self.tab_map.sync(fold_snapshot.clone(), edits, tab_size); + let (wrap_snapshot, edits) = self + .wrap_map + .update(cx, |map, cx| map.sync(tab_snapshot.clone(), edits, cx)); + let block_snapshot = self.block_map.read(wrap_snapshot.clone(), edits); + + DisplaySnapshot { + buffer_snapshot: self.buffer.read(cx).snapshot(cx), + fold_snapshot, + inlay_snapshot, + tab_snapshot, + wrap_snapshot, + block_snapshot, + text_highlights: self.text_highlights.clone(), + inlay_highlights: self.inlay_highlights.clone(), + clip_at_line_ends: self.clip_at_line_ends, + } + } + + pub fn set_state(&mut self, other: &DisplaySnapshot, cx: &mut ModelContext) { + self.fold( + other + .folds_in_range(0..other.buffer_snapshot.len()) + .map(|fold| fold.to_offset(&other.buffer_snapshot)), + cx, + ); + } + + pub fn fold( + &mut self, + ranges: impl IntoIterator>, + cx: &mut ModelContext, + ) { + let snapshot = self.buffer.read(cx).snapshot(cx); + let edits = self.buffer_subscription.consume().into_inner(); + let tab_size = Self::tab_size(&self.buffer, cx); + let (snapshot, edits) = self.inlay_map.sync(snapshot, edits); + let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits); + let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); + let (snapshot, edits) = self + .wrap_map + .update(cx, |map, cx| map.sync(snapshot, edits, cx)); + self.block_map.read(snapshot, edits); + let (snapshot, edits) = fold_map.fold(ranges); + let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); + let (snapshot, edits) = self + .wrap_map + .update(cx, |map, cx| map.sync(snapshot, edits, cx)); + self.block_map.read(snapshot, edits); + } + + pub fn unfold( + &mut self, + ranges: impl IntoIterator>, + inclusive: bool, + cx: &mut ModelContext, + ) { + let snapshot = self.buffer.read(cx).snapshot(cx); + let edits = self.buffer_subscription.consume().into_inner(); + let tab_size = Self::tab_size(&self.buffer, cx); + let (snapshot, edits) = self.inlay_map.sync(snapshot, edits); + let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits); + let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); + let (snapshot, edits) = self + .wrap_map + .update(cx, |map, cx| map.sync(snapshot, edits, cx)); + self.block_map.read(snapshot, edits); + let (snapshot, edits) = fold_map.unfold(ranges, inclusive); + let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); + let (snapshot, edits) = self + .wrap_map + .update(cx, |map, cx| map.sync(snapshot, edits, cx)); + self.block_map.read(snapshot, edits); + } + + pub fn insert_blocks( + &mut self, + blocks: impl IntoIterator>, + cx: &mut ModelContext, + ) -> Vec { + let snapshot = self.buffer.read(cx).snapshot(cx); + let edits = self.buffer_subscription.consume().into_inner(); + let tab_size = Self::tab_size(&self.buffer, cx); + let (snapshot, edits) = self.inlay_map.sync(snapshot, edits); + let (snapshot, edits) = self.fold_map.read(snapshot, edits); + let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); + let (snapshot, edits) = self + .wrap_map + .update(cx, |map, cx| map.sync(snapshot, edits, cx)); + let mut block_map = self.block_map.write(snapshot, edits); + block_map.insert(blocks) + } + + pub fn replace_blocks(&mut self, styles: HashMap) { + self.block_map.replace(styles); + } + + pub fn remove_blocks(&mut self, ids: HashSet, cx: &mut ModelContext) { + let snapshot = self.buffer.read(cx).snapshot(cx); + let edits = self.buffer_subscription.consume().into_inner(); + let tab_size = Self::tab_size(&self.buffer, cx); + let (snapshot, edits) = self.inlay_map.sync(snapshot, edits); + let (snapshot, edits) = self.fold_map.read(snapshot, edits); + let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); + let (snapshot, edits) = self + .wrap_map + .update(cx, |map, cx| map.sync(snapshot, edits, cx)); + let mut block_map = self.block_map.write(snapshot, edits); + block_map.remove(ids); + } + + pub fn highlight_text( + &mut self, + type_id: TypeId, + ranges: Vec>, + style: HighlightStyle, + ) { + self.text_highlights + .insert(Some(type_id), Arc::new((style, ranges))); + } + + pub fn highlight_inlays( + &mut self, + type_id: TypeId, + highlights: Vec, + style: HighlightStyle, + ) { + for highlight in highlights { + self.inlay_highlights + .entry(type_id) + .or_default() + .insert(highlight.inlay, (style, highlight)); + } + } + + pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range])> { + let highlights = self.text_highlights.get(&Some(type_id))?; + Some((highlights.0, &highlights.1)) + } + pub fn clear_highlights(&mut self, type_id: TypeId) -> bool { + let mut cleared = self.text_highlights.remove(&Some(type_id)).is_some(); + cleared |= self.inlay_highlights.remove(&type_id).is_none(); + cleared + } + + pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext) -> bool { + self.wrap_map + .update(cx, |map, cx| map.set_font(font_id, font_size, cx)) + } + + pub fn set_fold_ellipses_color(&mut self, color: Color) -> bool { + self.fold_map.set_ellipses_color(color) + } + + pub fn set_wrap_width(&self, width: Option, cx: &mut ModelContext) -> bool { + self.wrap_map + .update(cx, |map, cx| map.set_wrap_width(width, cx)) + } + + pub fn current_inlays(&self) -> impl Iterator { + self.inlay_map.current_inlays() + } + + pub fn splice_inlays( + &mut self, + to_remove: Vec, + to_insert: Vec, + cx: &mut ModelContext, + ) { + if to_remove.is_empty() && to_insert.is_empty() { + return; + } + let buffer_snapshot = self.buffer.read(cx).snapshot(cx); + let edits = self.buffer_subscription.consume().into_inner(); + let (snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits); + let (snapshot, edits) = self.fold_map.read(snapshot, edits); + let tab_size = Self::tab_size(&self.buffer, cx); + let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); + let (snapshot, edits) = self + .wrap_map + .update(cx, |map, cx| map.sync(snapshot, edits, cx)); + self.block_map.read(snapshot, edits); + + let (snapshot, edits) = self.inlay_map.splice(to_remove, to_insert); + let (snapshot, edits) = self.fold_map.read(snapshot, edits); + let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); + let (snapshot, edits) = self + .wrap_map + .update(cx, |map, cx| map.sync(snapshot, edits, cx)); + self.block_map.read(snapshot, edits); + } + + fn tab_size(buffer: &ModelHandle, cx: &mut ModelContext) -> NonZeroU32 { + let language = buffer + .read(cx) + .as_singleton() + .and_then(|buffer| buffer.read(cx).language()); + language_settings(language.as_deref(), None, cx).tab_size + } + + #[cfg(test)] + pub fn is_rewrapping(&self, cx: &gpui::AppContext) -> bool { + self.wrap_map.read(cx).is_rewrapping() + } +} + +#[derive(Debug, Default)] +pub struct Highlights<'a> { + pub text_highlights: Option<&'a TextHighlights>, + pub inlay_highlights: Option<&'a InlayHighlights>, + pub inlay_highlight_style: Option, + pub suggestion_highlight_style: Option, +} + +pub struct HighlightedChunk<'a> { + pub chunk: &'a str, + pub style: Option, + pub is_tab: bool, +} + +pub struct DisplaySnapshot { + pub buffer_snapshot: MultiBufferSnapshot, + pub fold_snapshot: fold_map::FoldSnapshot, + inlay_snapshot: inlay_map::InlaySnapshot, + tab_snapshot: tab_map::TabSnapshot, + wrap_snapshot: wrap_map::WrapSnapshot, + block_snapshot: block_map::BlockSnapshot, + text_highlights: TextHighlights, + inlay_highlights: InlayHighlights, + clip_at_line_ends: bool, +} + +impl DisplaySnapshot { + #[cfg(test)] + pub fn fold_count(&self) -> usize { + self.fold_snapshot.fold_count() + } + + pub fn is_empty(&self) -> bool { + self.buffer_snapshot.len() == 0 + } + + pub fn buffer_rows(&self, start_row: u32) -> DisplayBufferRows { + self.block_snapshot.buffer_rows(start_row) + } + + pub fn max_buffer_row(&self) -> u32 { + self.buffer_snapshot.max_buffer_row() + } + + pub fn prev_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) { + loop { + let mut inlay_point = self.inlay_snapshot.to_inlay_point(point); + let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Left); + fold_point.0.column = 0; + inlay_point = fold_point.to_inlay_point(&self.fold_snapshot); + point = self.inlay_snapshot.to_buffer_point(inlay_point); + + let mut display_point = self.point_to_display_point(point, Bias::Left); + *display_point.column_mut() = 0; + let next_point = self.display_point_to_point(display_point, Bias::Left); + if next_point == point { + return (point, display_point); + } + point = next_point; + } + } + + pub fn next_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) { + loop { + let mut inlay_point = self.inlay_snapshot.to_inlay_point(point); + let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Right); + fold_point.0.column = self.fold_snapshot.line_len(fold_point.row()); + inlay_point = fold_point.to_inlay_point(&self.fold_snapshot); + point = self.inlay_snapshot.to_buffer_point(inlay_point); + + let mut display_point = self.point_to_display_point(point, Bias::Right); + *display_point.column_mut() = self.line_len(display_point.row()); + let next_point = self.display_point_to_point(display_point, Bias::Right); + if next_point == point { + return (point, display_point); + } + point = next_point; + } + } + + // used by line_mode selections and tries to match vim behaviour + pub fn expand_to_line(&self, range: Range) -> Range { + let new_start = if range.start.row == 0 { + Point::new(0, 0) + } else if range.start.row == self.max_buffer_row() + || (range.end.column > 0 && range.end.row == self.max_buffer_row()) + { + Point::new(range.start.row - 1, self.line_len(range.start.row - 1)) + } else { + self.prev_line_boundary(range.start).0 + }; + + let new_end = if range.end.column == 0 { + range.end + } else if range.end.row < self.max_buffer_row() { + self.buffer_snapshot + .clip_point(Point::new(range.end.row + 1, 0), Bias::Left) + } else { + self.buffer_snapshot.max_point() + }; + + new_start..new_end + } + + fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint { + let inlay_point = self.inlay_snapshot.to_inlay_point(point); + let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias); + let tab_point = self.tab_snapshot.to_tab_point(fold_point); + let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point); + let block_point = self.block_snapshot.to_block_point(wrap_point); + DisplayPoint(block_point) + } + + fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point { + self.inlay_snapshot + .to_buffer_point(self.display_point_to_inlay_point(point, bias)) + } + + pub fn display_point_to_inlay_offset(&self, point: DisplayPoint, bias: Bias) -> InlayOffset { + self.inlay_snapshot + .to_offset(self.display_point_to_inlay_point(point, bias)) + } + + pub fn anchor_to_inlay_offset(&self, anchor: Anchor) -> InlayOffset { + self.inlay_snapshot + .to_inlay_offset(anchor.to_offset(&self.buffer_snapshot)) + } + + fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint { + let block_point = point.0; + let wrap_point = self.block_snapshot.to_wrap_point(block_point); + let tab_point = self.wrap_snapshot.to_tab_point(wrap_point); + let fold_point = self.tab_snapshot.to_fold_point(tab_point, bias).0; + fold_point.to_inlay_point(&self.fold_snapshot) + } + + pub fn display_point_to_fold_point(&self, point: DisplayPoint, bias: Bias) -> FoldPoint { + let block_point = point.0; + let wrap_point = self.block_snapshot.to_wrap_point(block_point); + let tab_point = self.wrap_snapshot.to_tab_point(wrap_point); + self.tab_snapshot.to_fold_point(tab_point, bias).0 + } + + pub fn fold_point_to_display_point(&self, fold_point: FoldPoint) -> DisplayPoint { + let tab_point = self.tab_snapshot.to_tab_point(fold_point); + let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point); + let block_point = self.block_snapshot.to_block_point(wrap_point); + DisplayPoint(block_point) + } + + pub fn max_point(&self) -> DisplayPoint { + DisplayPoint(self.block_snapshot.max_point()) + } + + /// Returns text chunks starting at the given display row until the end of the file + pub fn text_chunks(&self, display_row: u32) -> impl Iterator { + self.block_snapshot + .chunks( + display_row..self.max_point().row() + 1, + false, + Highlights::default(), + ) + .map(|h| h.text) + } + + /// Returns text chunks starting at the end of the given display row in reverse until the start of the file + pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator { + (0..=display_row).into_iter().rev().flat_map(|row| { + self.block_snapshot + .chunks(row..row + 1, false, Highlights::default()) + .map(|h| h.text) + .collect::>() + .into_iter() + .rev() + }) + } + + pub fn chunks<'a>( + &'a self, + display_rows: Range, + language_aware: bool, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, + ) -> DisplayChunks<'a> { + self.block_snapshot.chunks( + display_rows, + language_aware, + Highlights { + text_highlights: Some(&self.text_highlights), + inlay_highlights: Some(&self.inlay_highlights), + inlay_highlight_style, + suggestion_highlight_style, + }, + ) + } + + pub fn highlighted_chunks<'a>( + &'a self, + display_rows: Range, + language_aware: bool, + style: &'a EditorStyle, + ) -> impl Iterator> { + self.chunks( + display_rows, + language_aware, + Some(style.theme.hint), + Some(style.theme.suggestion), + ) + .map(|chunk| { + let mut highlight_style = chunk + .syntax_highlight_id + .and_then(|id| id.style(&style.syntax)); + + if let Some(chunk_highlight) = chunk.highlight_style { + if let Some(highlight_style) = highlight_style.as_mut() { + highlight_style.highlight(chunk_highlight); + } else { + highlight_style = Some(chunk_highlight); + } + } + + let mut diagnostic_highlight = HighlightStyle::default(); + + if chunk.is_unnecessary { + diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade); + } + + if let Some(severity) = chunk.diagnostic_severity { + // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code. + if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary { + let diagnostic_style = super::diagnostic_style(severity, true, style); + diagnostic_highlight.underline = Some(Underline { + color: Some(diagnostic_style.message.text.color), + thickness: 1.0.into(), + squiggly: true, + }); + } + } + + if let Some(highlight_style) = highlight_style.as_mut() { + highlight_style.highlight(diagnostic_highlight); + } else { + highlight_style = Some(diagnostic_highlight); + } + + HighlightedChunk { + chunk: chunk.text, + style: highlight_style, + is_tab: chunk.is_tab, + } + }) + } + + pub fn lay_out_line_for_row( + &self, + display_row: u32, + TextLayoutDetails { + font_cache, + text_layout_cache, + editor_style, + }: &TextLayoutDetails, + ) -> Line { + let mut styles = Vec::new(); + let mut line = String::new(); + let mut ended_in_newline = false; + + let range = display_row..display_row + 1; + for chunk in self.highlighted_chunks(range, false, editor_style) { + line.push_str(chunk.chunk); + + let text_style = if let Some(style) = chunk.style { + editor_style + .text + .clone() + .highlight(style, font_cache) + .map(Cow::Owned) + .unwrap_or_else(|_| Cow::Borrowed(&editor_style.text)) + } else { + Cow::Borrowed(&editor_style.text) + }; + ended_in_newline = chunk.chunk.ends_with("\n"); + + styles.push(( + chunk.chunk.len(), + RunStyle { + font_id: text_style.font_id, + color: text_style.color, + underline: text_style.underline, + }, + )); + } + + // our pixel positioning logic assumes each line ends in \n, + // this is almost always true except for the last line which + // may have no trailing newline. + if !ended_in_newline && display_row == self.max_point().row() { + line.push_str("\n"); + + styles.push(( + "\n".len(), + RunStyle { + font_id: editor_style.text.font_id, + color: editor_style.text_color, + underline: editor_style.text.underline, + }, + )); + } + + text_layout_cache.layout_str(&line, editor_style.text.font_size, &styles) + } + + pub fn x_for_point( + &self, + display_point: DisplayPoint, + text_layout_details: &TextLayoutDetails, + ) -> f32 { + let layout_line = self.lay_out_line_for_row(display_point.row(), text_layout_details); + layout_line.x_for_index(display_point.column() as usize) + } + + pub fn column_for_x( + &self, + display_row: u32, + x_coordinate: f32, + text_layout_details: &TextLayoutDetails, + ) -> u32 { + let layout_line = self.lay_out_line_for_row(display_row, text_layout_details); + layout_line.closest_index_for_x(x_coordinate) as u32 + } + + pub fn chars_at( + &self, + mut point: DisplayPoint, + ) -> impl Iterator + '_ { + point = DisplayPoint(self.block_snapshot.clip_point(point.0, Bias::Left)); + self.text_chunks(point.row()) + .flat_map(str::chars) + .skip_while({ + let mut column = 0; + move |char| { + let at_point = column >= point.column(); + column += char.len_utf8() as u32; + !at_point + } + }) + .map(move |ch| { + let result = (ch, point); + if ch == '\n' { + *point.row_mut() += 1; + *point.column_mut() = 0; + } else { + *point.column_mut() += ch.len_utf8() as u32; + } + result + }) + } + + pub fn reverse_chars_at( + &self, + mut point: DisplayPoint, + ) -> impl Iterator + '_ { + point = DisplayPoint(self.block_snapshot.clip_point(point.0, Bias::Left)); + self.reverse_text_chunks(point.row()) + .flat_map(|chunk| chunk.chars().rev()) + .skip_while({ + let mut column = self.line_len(point.row()); + if self.max_point().row() > point.row() { + column += 1; + } + + move |char| { + let at_point = column <= point.column(); + column = column.saturating_sub(char.len_utf8() as u32); + !at_point + } + }) + .map(move |ch| { + if ch == '\n' { + *point.row_mut() -= 1; + *point.column_mut() = self.line_len(point.row()); + } else { + *point.column_mut() = point.column().saturating_sub(ch.len_utf8() as u32); + } + (ch, point) + }) + } + + pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 { + let mut count = 0; + let mut column = 0; + for (c, _) in self.chars_at(DisplayPoint::new(display_row, 0)) { + if column >= target { + break; + } + count += 1; + column += c.len_utf8() as u32; + } + count + } + + pub fn column_from_chars(&self, display_row: u32, char_count: u32) -> u32 { + let mut column = 0; + + for (count, (c, _)) in self.chars_at(DisplayPoint::new(display_row, 0)).enumerate() { + if c == '\n' || count >= char_count as usize { + break; + } + column += c.len_utf8() as u32; + } + + column + } + + pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint { + let mut clipped = self.block_snapshot.clip_point(point.0, bias); + if self.clip_at_line_ends { + clipped = self.clip_at_line_end(DisplayPoint(clipped)).0 + } + DisplayPoint(clipped) + } + + pub fn clip_at_line_end(&self, point: DisplayPoint) -> DisplayPoint { + let mut point = point.0; + if point.column == self.line_len(point.row) { + point.column = point.column.saturating_sub(1); + point = self.block_snapshot.clip_point(point, Bias::Left); + } + DisplayPoint(point) + } + + pub fn folds_in_range(&self, range: Range) -> impl Iterator> + where + T: ToOffset, + { + self.fold_snapshot.folds_in_range(range) + } + + pub fn blocks_in_range( + &self, + rows: Range, + ) -> impl Iterator { + self.block_snapshot.blocks_in_range(rows) + } + + pub fn intersects_fold(&self, offset: T) -> bool { + self.fold_snapshot.intersects_fold(offset) + } + + pub fn is_line_folded(&self, buffer_row: u32) -> bool { + self.fold_snapshot.is_line_folded(buffer_row) + } + + pub fn is_block_line(&self, display_row: u32) -> bool { + self.block_snapshot.is_block_line(display_row) + } + + pub fn soft_wrap_indent(&self, display_row: u32) -> Option { + let wrap_row = self + .block_snapshot + .to_wrap_point(BlockPoint::new(display_row, 0)) + .row(); + self.wrap_snapshot.soft_wrap_indent(wrap_row) + } + + pub fn text(&self) -> String { + self.text_chunks(0).collect() + } + + pub fn line(&self, display_row: u32) -> String { + let mut result = String::new(); + for chunk in self.text_chunks(display_row) { + if let Some(ix) = chunk.find('\n') { + result.push_str(&chunk[0..ix]); + break; + } else { + result.push_str(chunk); + } + } + result + } + + pub fn line_indent(&self, display_row: u32) -> (u32, bool) { + let mut indent = 0; + let mut is_blank = true; + for (c, _) in self.chars_at(DisplayPoint::new(display_row, 0)) { + if c == ' ' { + indent += 1; + } else { + is_blank = c == '\n'; + break; + } + } + (indent, is_blank) + } + + pub fn line_indent_for_buffer_row(&self, buffer_row: u32) -> (u32, bool) { + let (buffer, range) = self + .buffer_snapshot + .buffer_line_for_row(buffer_row) + .unwrap(); + + let mut indent_size = 0; + let mut is_blank = false; + for c in buffer.chars_at(Point::new(range.start.row, 0)) { + if c == ' ' || c == '\t' { + indent_size += 1; + } else { + if c == '\n' { + is_blank = true; + } + break; + } + } + + (indent_size, is_blank) + } + + pub fn line_len(&self, row: u32) -> u32 { + self.block_snapshot.line_len(row) + } + + pub fn longest_row(&self) -> u32 { + self.block_snapshot.longest_row() + } + + pub fn fold_for_line(self: &Self, buffer_row: u32) -> Option { + if self.is_line_folded(buffer_row) { + Some(FoldStatus::Folded) + } else if self.is_foldable(buffer_row) { + Some(FoldStatus::Foldable) + } else { + None + } + } + + pub fn is_foldable(self: &Self, buffer_row: u32) -> bool { + let max_row = self.buffer_snapshot.max_buffer_row(); + if buffer_row >= max_row { + return false; + } + + let (indent_size, is_blank) = self.line_indent_for_buffer_row(buffer_row); + if is_blank { + return false; + } + + for next_row in (buffer_row + 1)..=max_row { + let (next_indent_size, next_line_is_blank) = self.line_indent_for_buffer_row(next_row); + if next_indent_size > indent_size { + return true; + } else if !next_line_is_blank { + break; + } + } + + false + } + + pub fn foldable_range(self: &Self, buffer_row: u32) -> Option> { + let start = Point::new(buffer_row, self.buffer_snapshot.line_len(buffer_row)); + if self.is_foldable(start.row) && !self.is_line_folded(start.row) { + let (start_indent, _) = self.line_indent_for_buffer_row(buffer_row); + let max_point = self.buffer_snapshot.max_point(); + let mut end = None; + + for row in (buffer_row + 1)..=max_point.row { + let (indent, is_blank) = self.line_indent_for_buffer_row(row); + if !is_blank && indent <= start_indent { + let prev_row = row - 1; + end = Some(Point::new( + prev_row, + self.buffer_snapshot.line_len(prev_row), + )); + break; + } + } + let end = end.unwrap_or(max_point); + Some(start..end) + } else { + None + } + } + + #[cfg(any(test, feature = "test-support"))] + pub fn text_highlight_ranges( + &self, + ) -> Option>)>> { + let type_id = TypeId::of::(); + self.text_highlights.get(&Some(type_id)).cloned() + } + + #[cfg(any(test, feature = "test-support"))] + pub fn inlay_highlights( + &self, + ) -> Option<&HashMap> { + let type_id = TypeId::of::(); + self.inlay_highlights.get(&type_id) + } +} + +#[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)] +pub struct DisplayPoint(BlockPoint); + +impl Debug for DisplayPoint { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "DisplayPoint({}, {})", + self.row(), + self.column() + )) + } +} + +impl DisplayPoint { + pub fn new(row: u32, column: u32) -> Self { + Self(BlockPoint(Point::new(row, column))) + } + + pub fn zero() -> Self { + Self::new(0, 0) + } + + pub fn is_zero(&self) -> bool { + self.0.is_zero() + } + + pub fn row(self) -> u32 { + self.0.row + } + + pub fn column(self) -> u32 { + self.0.column + } + + pub fn row_mut(&mut self) -> &mut u32 { + &mut self.0.row + } + + pub fn column_mut(&mut self) -> &mut u32 { + &mut self.0.column + } + + pub fn to_point(self, map: &DisplaySnapshot) -> Point { + map.display_point_to_point(self, Bias::Left) + } + + pub fn to_offset(self, map: &DisplaySnapshot, bias: Bias) -> usize { + let wrap_point = map.block_snapshot.to_wrap_point(self.0); + let tab_point = map.wrap_snapshot.to_tab_point(wrap_point); + let fold_point = map.tab_snapshot.to_fold_point(tab_point, bias).0; + let inlay_point = fold_point.to_inlay_point(&map.fold_snapshot); + map.inlay_snapshot + .to_buffer_offset(map.inlay_snapshot.to_offset(inlay_point)) + } +} + +impl ToDisplayPoint for usize { + fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint { + map.point_to_display_point(self.to_point(&map.buffer_snapshot), Bias::Left) + } +} + +impl ToDisplayPoint for OffsetUtf16 { + fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint { + self.to_offset(&map.buffer_snapshot).to_display_point(map) + } +} + +impl ToDisplayPoint for Point { + fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint { + map.point_to_display_point(*self, Bias::Left) + } +} + +impl ToDisplayPoint for Anchor { + fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint { + self.to_point(&map.buffer_snapshot).to_display_point(map) + } +} + +pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterator { + let max_row = display_map.max_point().row(); + let start_row = display_row + 1; + let mut current = None; + std::iter::from_fn(move || { + if current == None { + current = Some(start_row); + } else { + current = Some(current.unwrap() + 1) + } + if current.unwrap() > max_row { + None + } else { + current + } + }) +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::{ + movement, + test::{editor_test_context::EditorTestContext, marked_display_snapshot}, + }; + use gpui::{color::Color, elements::*, test::observe, AppContext}; + use language::{ + language_settings::{AllLanguageSettings, AllLanguageSettingsContent}, + Buffer, Language, LanguageConfig, SelectionGoal, + }; + use project::Project; + use rand::{prelude::*, Rng}; + use settings::SettingsStore; + use smol::stream::StreamExt; + use std::{env, sync::Arc}; + use theme::SyntaxTheme; + use util::test::{marked_text_ranges, sample_text}; + use Bias::*; + + #[gpui::test(iterations = 100)] + async fn test_random_display_map(cx: &mut gpui::TestAppContext, mut rng: StdRng) { + cx.foreground().set_block_on_ticks(0..=50); + cx.foreground().forbid_parking(); + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + + let font_cache = cx.font_cache().clone(); + let mut tab_size = rng.gen_range(1..=4); + let buffer_start_excerpt_header_height = rng.gen_range(1..=5); + let excerpt_header_height = rng.gen_range(1..=5); + let family_id = font_cache + .load_family(&["Helvetica"], &Default::default()) + .unwrap(); + let font_id = font_cache + .select_font(family_id, &Default::default()) + .unwrap(); + let font_size = 14.0; + let max_wrap_width = 300.0; + let mut wrap_width = if rng.gen_bool(0.1) { + None + } else { + Some(rng.gen_range(0.0..=max_wrap_width)) + }; + + log::info!("tab size: {}", tab_size); + log::info!("wrap width: {:?}", wrap_width); + + cx.update(|cx| { + init_test(cx, |s| s.defaults.tab_size = NonZeroU32::new(tab_size)); + }); + + let buffer = cx.update(|cx| { + if rng.gen() { + let len = rng.gen_range(0..10); + let text = util::RandomCharIter::new(&mut rng) + .take(len) + .collect::(); + MultiBuffer::build_simple(&text, cx) + } else { + MultiBuffer::build_random(&mut rng, cx) + } + }); + + let map = cx.add_model(|cx| { + DisplayMap::new( + buffer.clone(), + font_id, + font_size, + wrap_width, + buffer_start_excerpt_header_height, + excerpt_header_height, + cx, + ) + }); + let mut notifications = observe(&map, cx); + let mut fold_count = 0; + let mut blocks = Vec::new(); + + let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); + log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text()); + log::info!("fold text: {:?}", snapshot.fold_snapshot.text()); + log::info!("tab text: {:?}", snapshot.tab_snapshot.text()); + log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text()); + log::info!("block text: {:?}", snapshot.block_snapshot.text()); + log::info!("display text: {:?}", snapshot.text()); + + for _i in 0..operations { + match rng.gen_range(0..100) { + 0..=19 => { + wrap_width = if rng.gen_bool(0.2) { + None + } else { + Some(rng.gen_range(0.0..=max_wrap_width)) + }; + log::info!("setting wrap width to {:?}", wrap_width); + map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); + } + 20..=29 => { + let mut tab_sizes = vec![1, 2, 3, 4]; + tab_sizes.remove((tab_size - 1) as usize); + tab_size = *tab_sizes.choose(&mut rng).unwrap(); + log::info!("setting tab size to {:?}", tab_size); + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |s| { + s.defaults.tab_size = NonZeroU32::new(tab_size); + }); + }); + }); + } + 30..=44 => { + map.update(cx, |map, cx| { + if rng.gen() || blocks.is_empty() { + let buffer = map.snapshot(cx).buffer_snapshot; + let block_properties = (0..rng.gen_range(1..=1)) + .map(|_| { + let position = + buffer.anchor_after(buffer.clip_offset( + rng.gen_range(0..=buffer.len()), + Bias::Left, + )); + + let disposition = if rng.gen() { + BlockDisposition::Above + } else { + BlockDisposition::Below + }; + let height = rng.gen_range(1..5); + log::info!( + "inserting block {:?} {:?} with height {}", + disposition, + position.to_point(&buffer), + height + ); + BlockProperties { + style: BlockStyle::Fixed, + position, + height, + disposition, + render: Arc::new(|_| Empty::new().into_any()), + } + }) + .collect::>(); + blocks.extend(map.insert_blocks(block_properties, cx)); + } else { + blocks.shuffle(&mut rng); + let remove_count = rng.gen_range(1..=4.min(blocks.len())); + let block_ids_to_remove = (0..remove_count) + .map(|_| blocks.remove(rng.gen_range(0..blocks.len()))) + .collect(); + log::info!("removing block ids {:?}", block_ids_to_remove); + map.remove_blocks(block_ids_to_remove, cx); + } + }); + } + 45..=79 => { + let mut ranges = Vec::new(); + for _ in 0..rng.gen_range(1..=3) { + buffer.read_with(cx, |buffer, cx| { + let buffer = buffer.read(cx); + let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right); + let start = buffer.clip_offset(rng.gen_range(0..=end), Left); + ranges.push(start..end); + }); + } + + if rng.gen() && fold_count > 0 { + log::info!("unfolding ranges: {:?}", ranges); + map.update(cx, |map, cx| { + map.unfold(ranges, true, cx); + }); + } else { + log::info!("folding ranges: {:?}", ranges); + map.update(cx, |map, cx| { + map.fold(ranges, cx); + }); + } + } + _ => { + buffer.update(cx, |buffer, cx| buffer.randomly_mutate(&mut rng, 5, cx)); + } + } + + if map.read_with(cx, |map, cx| map.is_rewrapping(cx)) { + notifications.next().await.unwrap(); + } + + let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); + fold_count = snapshot.fold_count(); + log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text()); + log::info!("fold text: {:?}", snapshot.fold_snapshot.text()); + log::info!("tab text: {:?}", snapshot.tab_snapshot.text()); + log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text()); + log::info!("block text: {:?}", snapshot.block_snapshot.text()); + log::info!("display text: {:?}", snapshot.text()); + + // Line boundaries + let buffer = &snapshot.buffer_snapshot; + for _ in 0..5 { + let row = rng.gen_range(0..=buffer.max_point().row); + let column = rng.gen_range(0..=buffer.line_len(row)); + let point = buffer.clip_point(Point::new(row, column), Left); + + let (prev_buffer_bound, prev_display_bound) = snapshot.prev_line_boundary(point); + let (next_buffer_bound, next_display_bound) = snapshot.next_line_boundary(point); + + assert!(prev_buffer_bound <= point); + assert!(next_buffer_bound >= point); + assert_eq!(prev_buffer_bound.column, 0); + assert_eq!(prev_display_bound.column(), 0); + if next_buffer_bound < buffer.max_point() { + assert_eq!(buffer.chars_at(next_buffer_bound).next(), Some('\n')); + } + + assert_eq!( + prev_display_bound, + prev_buffer_bound.to_display_point(&snapshot), + "row boundary before {:?}. reported buffer row boundary: {:?}", + point, + prev_buffer_bound + ); + assert_eq!( + next_display_bound, + next_buffer_bound.to_display_point(&snapshot), + "display row boundary after {:?}. reported buffer row boundary: {:?}", + point, + next_buffer_bound + ); + assert_eq!( + prev_buffer_bound, + prev_display_bound.to_point(&snapshot), + "row boundary before {:?}. reported display row boundary: {:?}", + point, + prev_display_bound + ); + assert_eq!( + next_buffer_bound, + next_display_bound.to_point(&snapshot), + "row boundary after {:?}. reported display row boundary: {:?}", + point, + next_display_bound + ); + } + + // Movement + let min_point = snapshot.clip_point(DisplayPoint::new(0, 0), Left); + let max_point = snapshot.clip_point(snapshot.max_point(), Right); + for _ in 0..5 { + let row = rng.gen_range(0..=snapshot.max_point().row()); + let column = rng.gen_range(0..=snapshot.line_len(row)); + let point = snapshot.clip_point(DisplayPoint::new(row, column), Left); + + log::info!("Moving from point {:?}", point); + + let moved_right = movement::right(&snapshot, point); + log::info!("Right {:?}", moved_right); + if point < max_point { + assert!(moved_right > point); + if point.column() == snapshot.line_len(point.row()) + || snapshot.soft_wrap_indent(point.row()).is_some() + && point.column() == snapshot.line_len(point.row()) - 1 + { + assert!(moved_right.row() > point.row()); + } + } else { + assert_eq!(moved_right, point); + } + + let moved_left = movement::left(&snapshot, point); + log::info!("Left {:?}", moved_left); + if point > min_point { + assert!(moved_left < point); + if point.column() == 0 { + assert!(moved_left.row() < point.row()); + } + } else { + assert_eq!(moved_left, point); + } + } + } + } + + #[gpui::test(retries = 5)] + async fn test_soft_wraps(cx: &mut gpui::TestAppContext) { + cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); + cx.update(|cx| { + init_test(cx, |_| {}); + }); + + let mut cx = EditorTestContext::new(cx).await; + let editor = cx.editor.clone(); + let window = cx.window.clone(); + + cx.update_window(window, |cx| { + let text_layout_details = + editor.read_with(cx, |editor, cx| editor.text_layout_details(cx)); + + let font_cache = cx.font_cache().clone(); + + let family_id = font_cache + .load_family(&["Helvetica"], &Default::default()) + .unwrap(); + let font_id = font_cache + .select_font(family_id, &Default::default()) + .unwrap(); + let font_size = 12.0; + let wrap_width = Some(64.); + + let text = "one two three four five\nsix seven eight"; + let buffer = MultiBuffer::build_simple(text, cx); + let map = cx.add_model(|cx| { + DisplayMap::new(buffer.clone(), font_id, font_size, wrap_width, 1, 1, cx) + }); + + let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); + assert_eq!( + snapshot.text_chunks(0).collect::(), + "one two \nthree four \nfive\nsix seven \neight" + ); + assert_eq!( + snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Left), + DisplayPoint::new(0, 7) + ); + assert_eq!( + snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Right), + DisplayPoint::new(1, 0) + ); + assert_eq!( + movement::right(&snapshot, DisplayPoint::new(0, 7)), + DisplayPoint::new(1, 0) + ); + assert_eq!( + movement::left(&snapshot, DisplayPoint::new(1, 0)), + DisplayPoint::new(0, 7) + ); + + let x = snapshot.x_for_point(DisplayPoint::new(1, 10), &text_layout_details); + assert_eq!( + movement::up( + &snapshot, + DisplayPoint::new(1, 10), + SelectionGoal::None, + false, + &text_layout_details, + ), + ( + DisplayPoint::new(0, 7), + SelectionGoal::HorizontalPosition(x) + ) + ); + assert_eq!( + movement::down( + &snapshot, + DisplayPoint::new(0, 7), + SelectionGoal::HorizontalPosition(x), + false, + &text_layout_details + ), + ( + DisplayPoint::new(1, 10), + SelectionGoal::HorizontalPosition(x) + ) + ); + assert_eq!( + movement::down( + &snapshot, + DisplayPoint::new(1, 10), + SelectionGoal::HorizontalPosition(x), + false, + &text_layout_details + ), + ( + DisplayPoint::new(2, 4), + SelectionGoal::HorizontalPosition(x) + ) + ); + + let ix = snapshot.buffer_snapshot.text().find("seven").unwrap(); + buffer.update(cx, |buffer, cx| { + buffer.edit([(ix..ix, "and ")], None, cx); + }); + + let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); + assert_eq!( + snapshot.text_chunks(1).collect::(), + "three four \nfive\nsix and \nseven eight" + ); + + // Re-wrap on font size changes + map.update(cx, |map, cx| map.set_font(font_id, font_size + 3., cx)); + + let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); + assert_eq!( + snapshot.text_chunks(1).collect::(), + "three \nfour five\nsix and \nseven \neight" + ) + }); + } + + #[gpui::test] + fn test_text_chunks(cx: &mut gpui::AppContext) { + init_test(cx, |_| {}); + + let text = sample_text(6, 6, 'a'); + let buffer = MultiBuffer::build_simple(&text, cx); + let family_id = cx + .font_cache() + .load_family(&["Helvetica"], &Default::default()) + .unwrap(); + let font_id = cx + .font_cache() + .select_font(family_id, &Default::default()) + .unwrap(); + let font_size = 14.0; + let map = + cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx)); + + buffer.update(cx, |buffer, cx| { + buffer.edit( + vec![ + (Point::new(1, 0)..Point::new(1, 0), "\t"), + (Point::new(1, 1)..Point::new(1, 1), "\t"), + (Point::new(2, 1)..Point::new(2, 1), "\t"), + ], + None, + cx, + ) + }); + + assert_eq!( + map.update(cx, |map, cx| map.snapshot(cx)) + .text_chunks(1) + .collect::() + .lines() + .next(), + Some(" b bbbbb") + ); + assert_eq!( + map.update(cx, |map, cx| map.snapshot(cx)) + .text_chunks(2) + .collect::() + .lines() + .next(), + Some("c ccccc") + ); + } + + #[gpui::test] + async fn test_chunks(cx: &mut gpui::TestAppContext) { + use unindent::Unindent as _; + + let text = r#" + fn outer() {} + + mod module { + fn inner() {} + }"# + .unindent(); + + let theme = SyntaxTheme::new(vec![ + ("mod.body".to_string(), Color::red().into()), + ("fn.name".to_string(), Color::blue().into()), + ]); + let language = Arc::new( + Language::new( + LanguageConfig { + name: "Test".into(), + path_suffixes: vec![".test".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_highlights_query( + r#" + (mod_item name: (identifier) body: _ @mod.body) + (function_item name: (identifier) @fn.name) + "#, + ) + .unwrap(), + ); + language.set_theme(&theme); + + cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap()))); + + let buffer = cx + .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); + buffer.condition(cx, |buf, _| !buf.is_parsing()).await; + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + + let font_cache = cx.font_cache(); + let family_id = font_cache + .load_family(&["Helvetica"], &Default::default()) + .unwrap(); + let font_id = font_cache + .select_font(family_id, &Default::default()) + .unwrap(); + let font_size = 14.0; + + let map = cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx)); + assert_eq!( + cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)), + vec![ + ("fn ".to_string(), None), + ("outer".to_string(), Some(Color::blue())), + ("() {}\n\nmod module ".to_string(), None), + ("{\n fn ".to_string(), Some(Color::red())), + ("inner".to_string(), Some(Color::blue())), + ("() {}\n}".to_string(), Some(Color::red())), + ] + ); + assert_eq!( + cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)), + vec![ + (" fn ".to_string(), Some(Color::red())), + ("inner".to_string(), Some(Color::blue())), + ("() {}\n}".to_string(), Some(Color::red())), + ] + ); + + map.update(cx, |map, cx| { + map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx) + }); + assert_eq!( + cx.update(|cx| syntax_chunks(0..2, &map, &theme, cx)), + vec![ + ("fn ".to_string(), None), + ("out".to_string(), Some(Color::blue())), + ("⋯".to_string(), None), + (" fn ".to_string(), Some(Color::red())), + ("inner".to_string(), Some(Color::blue())), + ("() {}\n}".to_string(), Some(Color::red())), + ] + ); + } + + #[gpui::test] + async fn test_chunks_with_soft_wrapping(cx: &mut gpui::TestAppContext) { + use unindent::Unindent as _; + + cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); + + let text = r#" + fn outer() {} + + mod module { + fn inner() {} + }"# + .unindent(); + + let theme = SyntaxTheme::new(vec![ + ("mod.body".to_string(), Color::red().into()), + ("fn.name".to_string(), Color::blue().into()), + ]); + let language = Arc::new( + Language::new( + LanguageConfig { + name: "Test".into(), + path_suffixes: vec![".test".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_highlights_query( + r#" + (mod_item name: (identifier) body: _ @mod.body) + (function_item name: (identifier) @fn.name) + "#, + ) + .unwrap(), + ); + language.set_theme(&theme); + + cx.update(|cx| init_test(cx, |_| {})); + + let buffer = cx + .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); + buffer.condition(cx, |buf, _| !buf.is_parsing()).await; + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + + let font_cache = cx.font_cache(); + + let family_id = font_cache + .load_family(&["Courier"], &Default::default()) + .unwrap(); + let font_id = font_cache + .select_font(family_id, &Default::default()) + .unwrap(); + let font_size = 16.0; + + let map = + cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, Some(40.0), 1, 1, cx)); + assert_eq!( + cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)), + [ + ("fn \n".to_string(), None), + ("oute\nr".to_string(), Some(Color::blue())), + ("() \n{}\n\n".to_string(), None), + ] + ); + assert_eq!( + cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)), + [("{}\n\n".to_string(), None)] + ); + + map.update(cx, |map, cx| { + map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx) + }); + assert_eq!( + cx.update(|cx| syntax_chunks(1..4, &map, &theme, cx)), + [ + ("out".to_string(), Some(Color::blue())), + ("⋯\n".to_string(), None), + (" \nfn ".to_string(), Some(Color::red())), + ("i\n".to_string(), Some(Color::blue())) + ] + ); + } + + #[gpui::test] + async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) { + cx.update(|cx| init_test(cx, |_| {})); + + let theme = SyntaxTheme::new(vec![ + ("operator".to_string(), Color::red().into()), + ("string".to_string(), Color::green().into()), + ]); + let language = Arc::new( + Language::new( + LanguageConfig { + name: "Test".into(), + path_suffixes: vec![".test".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_highlights_query( + r#" + ":" @operator + (string_literal) @string + "#, + ) + .unwrap(), + ); + language.set_theme(&theme); + + let (text, highlighted_ranges) = marked_text_ranges(r#"constˇ «a»: B = "c «d»""#, false); + + let buffer = cx + .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); + buffer.condition(cx, |buf, _| !buf.is_parsing()).await; + + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); + + let font_cache = cx.font_cache(); + let family_id = font_cache + .load_family(&["Courier"], &Default::default()) + .unwrap(); + let font_id = font_cache + .select_font(family_id, &Default::default()) + .unwrap(); + let font_size = 16.0; + let map = cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx)); + + enum MyType {} + + let style = HighlightStyle { + color: Some(Color::blue()), + ..Default::default() + }; + + map.update(cx, |map, _cx| { + map.highlight_text( + TypeId::of::(), + highlighted_ranges + .into_iter() + .map(|range| { + buffer_snapshot.anchor_before(range.start) + ..buffer_snapshot.anchor_before(range.end) + }) + .collect(), + style, + ); + }); + + assert_eq!( + cx.update(|cx| chunks(0..10, &map, &theme, cx)), + [ + ("const ".to_string(), None, None), + ("a".to_string(), None, Some(Color::blue())), + (":".to_string(), Some(Color::red()), None), + (" B = ".to_string(), None, None), + ("\"c ".to_string(), Some(Color::green()), None), + ("d".to_string(), Some(Color::green()), Some(Color::blue())), + ("\"".to_string(), Some(Color::green()), None), + ] + ); + } + + #[gpui::test] + fn test_clip_point(cx: &mut gpui::AppContext) { + init_test(cx, |_| {}); + + fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::AppContext) { + let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx); + + match bias { + Bias::Left => { + if shift_right { + *markers[1].column_mut() += 1; + } + + assert_eq!(unmarked_snapshot.clip_point(markers[1], bias), markers[0]) + } + Bias::Right => { + if shift_right { + *markers[0].column_mut() += 1; + } + + assert_eq!(unmarked_snapshot.clip_point(markers[0], bias), markers[1]) + } + }; + } + + use Bias::{Left, Right}; + assert("ˇˇα", false, Left, cx); + assert("ˇˇα", true, Left, cx); + assert("ˇˇα", false, Right, cx); + assert("ˇαˇ", true, Right, cx); + assert("ˇˇ✋", false, Left, cx); + assert("ˇˇ✋", true, Left, cx); + assert("ˇˇ✋", false, Right, cx); + assert("ˇ✋ˇ", true, Right, cx); + assert("ˇˇ🍐", false, Left, cx); + assert("ˇˇ🍐", true, Left, cx); + assert("ˇˇ🍐", false, Right, cx); + assert("ˇ🍐ˇ", true, Right, cx); + assert("ˇˇ\t", false, Left, cx); + assert("ˇˇ\t", true, Left, cx); + assert("ˇˇ\t", false, Right, cx); + assert("ˇ\tˇ", true, Right, cx); + assert(" ˇˇ\t", false, Left, cx); + assert(" ˇˇ\t", true, Left, cx); + assert(" ˇˇ\t", false, Right, cx); + assert(" ˇ\tˇ", true, Right, cx); + assert(" ˇˇ\t", false, Left, cx); + assert(" ˇˇ\t", false, Right, cx); + } + + #[gpui::test] + fn test_clip_at_line_ends(cx: &mut gpui::AppContext) { + init_test(cx, |_| {}); + + fn assert(text: &str, cx: &mut gpui::AppContext) { + let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx); + unmarked_snapshot.clip_at_line_ends = true; + assert_eq!( + unmarked_snapshot.clip_point(markers[1], Bias::Left), + markers[0] + ); + } + + assert("ˇˇ", cx); + assert("ˇaˇ", cx); + assert("aˇbˇ", cx); + assert("aˇαˇ", cx); + } + + #[gpui::test] + fn test_tabs_with_multibyte_chars(cx: &mut gpui::AppContext) { + init_test(cx, |_| {}); + + let text = "✅\t\tα\nβ\t\n🏀β\t\tγ"; + let buffer = MultiBuffer::build_simple(text, cx); + let font_cache = cx.font_cache(); + let family_id = font_cache + .load_family(&["Helvetica"], &Default::default()) + .unwrap(); + let font_id = font_cache + .select_font(family_id, &Default::default()) + .unwrap(); + let font_size = 14.0; + + let map = + cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx)); + let map = map.update(cx, |map, cx| map.snapshot(cx)); + assert_eq!(map.text(), "✅ α\nβ \n🏀β γ"); + assert_eq!( + map.text_chunks(0).collect::(), + "✅ α\nβ \n🏀β γ" + ); + assert_eq!(map.text_chunks(1).collect::(), "β \n🏀β γ"); + assert_eq!(map.text_chunks(2).collect::(), "🏀β γ"); + + let point = Point::new(0, "✅\t\t".len() as u32); + let display_point = DisplayPoint::new(0, "✅ ".len() as u32); + assert_eq!(point.to_display_point(&map), display_point); + assert_eq!(display_point.to_point(&map), point); + + let point = Point::new(1, "β\t".len() as u32); + let display_point = DisplayPoint::new(1, "β ".len() as u32); + assert_eq!(point.to_display_point(&map), display_point); + assert_eq!(display_point.to_point(&map), point,); + + let point = Point::new(2, "🏀β\t\t".len() as u32); + let display_point = DisplayPoint::new(2, "🏀β ".len() as u32); + assert_eq!(point.to_display_point(&map), display_point); + assert_eq!(display_point.to_point(&map), point,); + + // Display points inside of expanded tabs + assert_eq!( + DisplayPoint::new(0, "✅ ".len() as u32).to_point(&map), + Point::new(0, "✅\t".len() as u32), + ); + assert_eq!( + DisplayPoint::new(0, "✅ ".len() as u32).to_point(&map), + Point::new(0, "✅".len() as u32), + ); + + // Clipping display points inside of multi-byte characters + assert_eq!( + map.clip_point(DisplayPoint::new(0, "✅".len() as u32 - 1), Left), + DisplayPoint::new(0, 0) + ); + assert_eq!( + map.clip_point(DisplayPoint::new(0, "✅".len() as u32 - 1), Bias::Right), + DisplayPoint::new(0, "✅".len() as u32) + ); + } + + #[gpui::test] + fn test_max_point(cx: &mut gpui::AppContext) { + init_test(cx, |_| {}); + + let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx); + let font_cache = cx.font_cache(); + let family_id = font_cache + .load_family(&["Helvetica"], &Default::default()) + .unwrap(); + let font_id = font_cache + .select_font(family_id, &Default::default()) + .unwrap(); + let font_size = 14.0; + let map = + cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx)); + assert_eq!( + map.update(cx, |map, cx| map.snapshot(cx)).max_point(), + DisplayPoint::new(1, 11) + ) + } + + fn syntax_chunks<'a>( + rows: Range, + map: &ModelHandle, + theme: &'a SyntaxTheme, + cx: &mut AppContext, + ) -> Vec<(String, Option)> { + chunks(rows, map, theme, cx) + .into_iter() + .map(|(text, color, _)| (text, color)) + .collect() + } + + fn chunks<'a>( + rows: Range, + map: &ModelHandle, + theme: &'a SyntaxTheme, + cx: &mut AppContext, + ) -> Vec<(String, Option, Option)> { + let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); + let mut chunks: Vec<(String, Option, Option)> = Vec::new(); + for chunk in snapshot.chunks(rows, true, None, None) { + let syntax_color = chunk + .syntax_highlight_id + .and_then(|id| id.style(theme)?.color); + let highlight_color = chunk.highlight_style.and_then(|style| style.color); + if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut() { + if syntax_color == *last_syntax_color && highlight_color == *last_highlight_color { + last_chunk.push_str(chunk.text); + continue; + } + } + chunks.push((chunk.text.to_string(), syntax_color, highlight_color)); + } + chunks + } + + fn init_test(cx: &mut AppContext, f: impl Fn(&mut AllLanguageSettingsContent)) { + cx.foreground().forbid_parking(); + cx.set_global(SettingsStore::test(cx)); + language::init(cx); + crate::init(cx); + Project::init_settings(cx); + theme::init((), cx); + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, f); + }); + } +} diff --git a/crates/editor2/src/display_map/block_map.rs b/crates/editor2/src/display_map/block_map.rs new file mode 100644 index 0000000000..c07625bf9c --- /dev/null +++ b/crates/editor2/src/display_map/block_map.rs @@ -0,0 +1,1667 @@ +use super::{ + wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot}, + Highlights, +}; +use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _}; +use collections::{Bound, HashMap, HashSet}; +use gpui::{AnyElement, ViewContext}; +use language::{BufferSnapshot, Chunk, Patch, Point}; +use parking_lot::Mutex; +use std::{ + cell::RefCell, + cmp::{self, Ordering}, + fmt::Debug, + ops::{Deref, DerefMut, Range}, + sync::{ + atomic::{AtomicUsize, Ordering::SeqCst}, + Arc, + }, +}; +use sum_tree::{Bias, SumTree}; +use text::Edit; + +const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize]; + +pub struct BlockMap { + next_block_id: AtomicUsize, + wrap_snapshot: RefCell, + blocks: Vec>, + transforms: RefCell>, + buffer_header_height: u8, + excerpt_header_height: u8, +} + +pub struct BlockMapWriter<'a>(&'a mut BlockMap); + +pub struct BlockSnapshot { + wrap_snapshot: WrapSnapshot, + transforms: SumTree, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct BlockId(usize); + +#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] +pub struct BlockPoint(pub Point); + +#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] +struct BlockRow(u32); + +#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] +struct WrapRow(u32); + +pub type RenderBlock = Arc AnyElement>; + +pub struct Block { + id: BlockId, + position: Anchor, + height: u8, + style: BlockStyle, + render: Mutex, + disposition: BlockDisposition, +} + +#[derive(Clone)] +pub struct BlockProperties

+where + P: Clone, +{ + pub position: P, + pub height: u8, + pub style: BlockStyle, + pub render: Arc AnyElement>, + pub disposition: BlockDisposition, +} + +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub enum BlockStyle { + Fixed, + Flex, + Sticky, +} + +pub struct BlockContext<'a, 'b, 'c> { + pub view_context: &'c mut ViewContext<'a, 'b, Editor>, + pub anchor_x: f32, + pub scroll_x: f32, + pub gutter_width: f32, + pub gutter_padding: f32, + pub em_width: f32, + pub line_height: f32, + pub block_id: usize, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum BlockDisposition { + Above, + Below, +} + +#[derive(Clone, Debug)] +struct Transform { + summary: TransformSummary, + block: Option, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Clone)] +pub enum TransformBlock { + Custom(Arc), + ExcerptHeader { + id: ExcerptId, + buffer: BufferSnapshot, + range: ExcerptRange, + height: u8, + starts_new_buffer: bool, + }, +} + +impl TransformBlock { + fn disposition(&self) -> BlockDisposition { + match self { + TransformBlock::Custom(block) => block.disposition, + TransformBlock::ExcerptHeader { .. } => BlockDisposition::Above, + } + } + + pub fn height(&self) -> u8 { + match self { + TransformBlock::Custom(block) => block.height, + TransformBlock::ExcerptHeader { height, .. } => *height, + } + } +} + +impl Debug for TransformBlock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Custom(block) => f.debug_struct("Custom").field("block", block).finish(), + Self::ExcerptHeader { buffer, .. } => f + .debug_struct("ExcerptHeader") + .field("path", &buffer.file().map(|f| f.path())) + .finish(), + } + } +} + +#[derive(Clone, Debug, Default)] +struct TransformSummary { + input_rows: u32, + output_rows: u32, +} + +pub struct BlockChunks<'a> { + transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>, + input_chunks: wrap_map::WrapChunks<'a>, + input_chunk: Chunk<'a>, + output_row: u32, + max_output_row: u32, +} + +#[derive(Clone)] +pub struct BlockBufferRows<'a> { + transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>, + input_buffer_rows: wrap_map::WrapBufferRows<'a>, + output_row: u32, + started: bool, +} + +impl BlockMap { + pub fn new( + wrap_snapshot: WrapSnapshot, + buffer_header_height: u8, + excerpt_header_height: u8, + ) -> Self { + let row_count = wrap_snapshot.max_point().row() + 1; + let map = Self { + next_block_id: AtomicUsize::new(0), + blocks: Vec::new(), + transforms: RefCell::new(SumTree::from_item(Transform::isomorphic(row_count), &())), + wrap_snapshot: RefCell::new(wrap_snapshot.clone()), + buffer_header_height, + excerpt_header_height, + }; + map.sync( + &wrap_snapshot, + Patch::new(vec![Edit { + old: 0..row_count, + new: 0..row_count, + }]), + ); + map + } + + pub fn read(&self, wrap_snapshot: WrapSnapshot, edits: Patch) -> BlockSnapshot { + self.sync(&wrap_snapshot, edits); + *self.wrap_snapshot.borrow_mut() = wrap_snapshot.clone(); + BlockSnapshot { + wrap_snapshot, + transforms: self.transforms.borrow().clone(), + } + } + + pub fn write(&mut self, wrap_snapshot: WrapSnapshot, edits: Patch) -> BlockMapWriter { + self.sync(&wrap_snapshot, edits); + *self.wrap_snapshot.borrow_mut() = wrap_snapshot; + BlockMapWriter(self) + } + + fn sync(&self, wrap_snapshot: &WrapSnapshot, mut edits: Patch) { + let buffer = wrap_snapshot.buffer_snapshot(); + + // Handle changing the last excerpt if it is empty. + if buffer.trailing_excerpt_update_count() + != self + .wrap_snapshot + .borrow() + .buffer_snapshot() + .trailing_excerpt_update_count() + { + let max_point = wrap_snapshot.max_point(); + let edit_start = wrap_snapshot.prev_row_boundary(max_point); + let edit_end = max_point.row() + 1; + edits = edits.compose([WrapEdit { + old: edit_start..edit_end, + new: edit_start..edit_end, + }]); + } + + let edits = edits.into_inner(); + if edits.is_empty() { + return; + } + + let mut transforms = self.transforms.borrow_mut(); + let mut new_transforms = SumTree::new(); + let old_row_count = transforms.summary().input_rows; + let new_row_count = wrap_snapshot.max_point().row() + 1; + let mut cursor = transforms.cursor::(); + let mut last_block_ix = 0; + let mut blocks_in_edit = Vec::new(); + let mut edits = edits.into_iter().peekable(); + + while let Some(edit) = edits.next() { + // Preserve any old transforms that precede this edit. + let old_start = WrapRow(edit.old.start); + let new_start = WrapRow(edit.new.start); + new_transforms.append(cursor.slice(&old_start, Bias::Left, &()), &()); + if let Some(transform) = cursor.item() { + if transform.is_isomorphic() && old_start == cursor.end(&()) { + new_transforms.push(transform.clone(), &()); + cursor.next(&()); + while let Some(transform) = cursor.item() { + if transform + .block + .as_ref() + .map_or(false, |b| b.disposition().is_below()) + { + new_transforms.push(transform.clone(), &()); + cursor.next(&()); + } else { + break; + } + } + } + } + + // Preserve any portion of an old transform that precedes this edit. + let extent_before_edit = old_start.0 - cursor.start().0; + push_isomorphic(&mut new_transforms, extent_before_edit); + + // Skip over any old transforms that intersect this edit. + let mut old_end = WrapRow(edit.old.end); + let mut new_end = WrapRow(edit.new.end); + cursor.seek(&old_end, Bias::Left, &()); + cursor.next(&()); + if old_end == *cursor.start() { + while let Some(transform) = cursor.item() { + if transform + .block + .as_ref() + .map_or(false, |b| b.disposition().is_below()) + { + cursor.next(&()); + } else { + break; + } + } + } + + // Combine this edit with any subsequent edits that intersect the same transform. + while let Some(next_edit) = edits.peek() { + if next_edit.old.start <= cursor.start().0 { + old_end = WrapRow(next_edit.old.end); + new_end = WrapRow(next_edit.new.end); + cursor.seek(&old_end, Bias::Left, &()); + cursor.next(&()); + if old_end == *cursor.start() { + while let Some(transform) = cursor.item() { + if transform + .block + .as_ref() + .map_or(false, |b| b.disposition().is_below()) + { + cursor.next(&()); + } else { + break; + } + } + } + edits.next(); + } else { + break; + } + } + + // Find the blocks within this edited region. + let new_buffer_start = + wrap_snapshot.to_point(WrapPoint::new(new_start.0, 0), Bias::Left); + let start_bound = Bound::Included(new_buffer_start); + let start_block_ix = match self.blocks[last_block_ix..].binary_search_by(|probe| { + probe + .position + .to_point(buffer) + .cmp(&new_buffer_start) + .then(Ordering::Greater) + }) { + Ok(ix) | Err(ix) => last_block_ix + ix, + }; + + let end_bound; + let end_block_ix = if new_end.0 > wrap_snapshot.max_point().row() { + end_bound = Bound::Unbounded; + self.blocks.len() + } else { + let new_buffer_end = + wrap_snapshot.to_point(WrapPoint::new(new_end.0, 0), Bias::Left); + end_bound = Bound::Excluded(new_buffer_end); + match self.blocks[start_block_ix..].binary_search_by(|probe| { + probe + .position + .to_point(buffer) + .cmp(&new_buffer_end) + .then(Ordering::Greater) + }) { + Ok(ix) | Err(ix) => start_block_ix + ix, + } + }; + last_block_ix = end_block_ix; + + debug_assert!(blocks_in_edit.is_empty()); + blocks_in_edit.extend( + self.blocks[start_block_ix..end_block_ix] + .iter() + .map(|block| { + let mut position = block.position.to_point(buffer); + match block.disposition { + BlockDisposition::Above => position.column = 0, + BlockDisposition::Below => { + position.column = buffer.line_len(position.row) + } + } + let position = wrap_snapshot.make_wrap_point(position, Bias::Left); + (position.row(), TransformBlock::Custom(block.clone())) + }), + ); + blocks_in_edit.extend( + buffer + .excerpt_boundaries_in_range((start_bound, end_bound)) + .map(|excerpt_boundary| { + ( + wrap_snapshot + .make_wrap_point(Point::new(excerpt_boundary.row, 0), Bias::Left) + .row(), + TransformBlock::ExcerptHeader { + id: excerpt_boundary.id, + buffer: excerpt_boundary.buffer, + range: excerpt_boundary.range, + height: if excerpt_boundary.starts_new_buffer { + self.buffer_header_height + } else { + self.excerpt_header_height + }, + starts_new_buffer: excerpt_boundary.starts_new_buffer, + }, + ) + }), + ); + + // Place excerpt headers above custom blocks on the same row. + blocks_in_edit.sort_unstable_by(|(row_a, block_a), (row_b, block_b)| { + row_a.cmp(row_b).then_with(|| match (block_a, block_b) { + ( + TransformBlock::ExcerptHeader { .. }, + TransformBlock::ExcerptHeader { .. }, + ) => Ordering::Equal, + (TransformBlock::ExcerptHeader { .. }, _) => Ordering::Less, + (_, TransformBlock::ExcerptHeader { .. }) => Ordering::Greater, + (TransformBlock::Custom(block_a), TransformBlock::Custom(block_b)) => block_a + .disposition + .cmp(&block_b.disposition) + .then_with(|| block_a.id.cmp(&block_b.id)), + }) + }); + + // For each of these blocks, insert a new isomorphic transform preceding the block, + // and then insert the block itself. + for (block_row, block) in blocks_in_edit.drain(..) { + let insertion_row = match block.disposition() { + BlockDisposition::Above => block_row, + BlockDisposition::Below => block_row + 1, + }; + let extent_before_block = insertion_row - new_transforms.summary().input_rows; + push_isomorphic(&mut new_transforms, extent_before_block); + new_transforms.push(Transform::block(block), &()); + } + + old_end = WrapRow(old_end.0.min(old_row_count)); + new_end = WrapRow(new_end.0.min(new_row_count)); + + // Insert an isomorphic transform after the final block. + let extent_after_last_block = new_end.0 - new_transforms.summary().input_rows; + push_isomorphic(&mut new_transforms, extent_after_last_block); + + // Preserve any portion of the old transform after this edit. + let extent_after_edit = cursor.start().0 - old_end.0; + push_isomorphic(&mut new_transforms, extent_after_edit); + } + + new_transforms.append(cursor.suffix(&()), &()); + debug_assert_eq!( + new_transforms.summary().input_rows, + wrap_snapshot.max_point().row() + 1 + ); + + drop(cursor); + *transforms = new_transforms; + } + + pub fn replace(&mut self, mut renderers: HashMap) { + for block in &self.blocks { + if let Some(render) = renderers.remove(&block.id) { + *block.render.lock() = render; + } + } + } +} + +fn push_isomorphic(tree: &mut SumTree, rows: u32) { + if rows == 0 { + return; + } + + let mut extent = Some(rows); + tree.update_last( + |last_transform| { + if last_transform.is_isomorphic() { + let extent = extent.take().unwrap(); + last_transform.summary.input_rows += extent; + last_transform.summary.output_rows += extent; + } + }, + &(), + ); + if let Some(extent) = extent { + tree.push(Transform::isomorphic(extent), &()); + } +} + +impl BlockPoint { + pub fn new(row: u32, column: u32) -> Self { + Self(Point::new(row, column)) + } +} + +impl Deref for BlockPoint { + type Target = Point; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for BlockPoint { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl<'a> BlockMapWriter<'a> { + pub fn insert( + &mut self, + blocks: impl IntoIterator>, + ) -> Vec { + let mut ids = Vec::new(); + let mut edits = Patch::default(); + let wrap_snapshot = &*self.0.wrap_snapshot.borrow(); + let buffer = wrap_snapshot.buffer_snapshot(); + + for block in blocks { + let id = BlockId(self.0.next_block_id.fetch_add(1, SeqCst)); + ids.push(id); + + let position = block.position; + let point = position.to_point(buffer); + let wrap_row = wrap_snapshot + .make_wrap_point(Point::new(point.row, 0), Bias::Left) + .row(); + let start_row = wrap_snapshot.prev_row_boundary(WrapPoint::new(wrap_row, 0)); + let end_row = wrap_snapshot + .next_row_boundary(WrapPoint::new(wrap_row, 0)) + .unwrap_or(wrap_snapshot.max_point().row() + 1); + + let block_ix = match self + .0 + .blocks + .binary_search_by(|probe| probe.position.cmp(&position, buffer)) + { + Ok(ix) | Err(ix) => ix, + }; + self.0.blocks.insert( + block_ix, + Arc::new(Block { + id, + position, + height: block.height, + render: Mutex::new(block.render), + disposition: block.disposition, + style: block.style, + }), + ); + + edits = edits.compose([Edit { + old: start_row..end_row, + new: start_row..end_row, + }]); + } + + self.0.sync(wrap_snapshot, edits); + ids + } + + pub fn remove(&mut self, block_ids: HashSet) { + let wrap_snapshot = &*self.0.wrap_snapshot.borrow(); + let buffer = wrap_snapshot.buffer_snapshot(); + let mut edits = Patch::default(); + let mut last_block_buffer_row = None; + self.0.blocks.retain(|block| { + if block_ids.contains(&block.id) { + let buffer_row = block.position.to_point(buffer).row; + if last_block_buffer_row != Some(buffer_row) { + last_block_buffer_row = Some(buffer_row); + let wrap_row = wrap_snapshot + .make_wrap_point(Point::new(buffer_row, 0), Bias::Left) + .row(); + let start_row = wrap_snapshot.prev_row_boundary(WrapPoint::new(wrap_row, 0)); + let end_row = wrap_snapshot + .next_row_boundary(WrapPoint::new(wrap_row, 0)) + .unwrap_or(wrap_snapshot.max_point().row() + 1); + edits.push(Edit { + old: start_row..end_row, + new: start_row..end_row, + }) + } + false + } else { + true + } + }); + self.0.sync(wrap_snapshot, edits); + } +} + +impl BlockSnapshot { + #[cfg(test)] + pub fn text(&self) -> String { + self.chunks( + 0..self.transforms.summary().output_rows, + false, + Highlights::default(), + ) + .map(|chunk| chunk.text) + .collect() + } + + pub fn chunks<'a>( + &'a self, + rows: Range, + language_aware: bool, + highlights: Highlights<'a>, + ) -> BlockChunks<'a> { + let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows); + let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(); + let input_end = { + cursor.seek(&BlockRow(rows.end), Bias::Right, &()); + let overshoot = if cursor + .item() + .map_or(false, |transform| transform.is_isomorphic()) + { + rows.end - cursor.start().0 .0 + } else { + 0 + }; + cursor.start().1 .0 + overshoot + }; + let input_start = { + cursor.seek(&BlockRow(rows.start), Bias::Right, &()); + let overshoot = if cursor + .item() + .map_or(false, |transform| transform.is_isomorphic()) + { + rows.start - cursor.start().0 .0 + } else { + 0 + }; + cursor.start().1 .0 + overshoot + }; + BlockChunks { + input_chunks: self.wrap_snapshot.chunks( + input_start..input_end, + language_aware, + highlights, + ), + input_chunk: Default::default(), + transforms: cursor, + output_row: rows.start, + max_output_row, + } + } + + pub fn buffer_rows(&self, start_row: u32) -> BlockBufferRows { + let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(); + cursor.seek(&BlockRow(start_row), Bias::Right, &()); + let (output_start, input_start) = cursor.start(); + let overshoot = if cursor.item().map_or(false, |t| t.is_isomorphic()) { + start_row - output_start.0 + } else { + 0 + }; + let input_start_row = input_start.0 + overshoot; + BlockBufferRows { + transforms: cursor, + input_buffer_rows: self.wrap_snapshot.buffer_rows(input_start_row), + output_row: start_row, + started: false, + } + } + + pub fn blocks_in_range( + &self, + rows: Range, + ) -> impl Iterator { + let mut cursor = self.transforms.cursor::(); + cursor.seek(&BlockRow(rows.start), Bias::Right, &()); + std::iter::from_fn(move || { + while let Some(transform) = cursor.item() { + let start_row = cursor.start().0; + if start_row >= rows.end { + break; + } + if let Some(block) = &transform.block { + cursor.next(&()); + return Some((start_row, block)); + } else { + cursor.next(&()); + } + } + None + }) + } + + pub fn max_point(&self) -> BlockPoint { + let row = self.transforms.summary().output_rows - 1; + BlockPoint::new(row, self.line_len(row)) + } + + pub fn longest_row(&self) -> u32 { + let input_row = self.wrap_snapshot.longest_row(); + self.to_block_point(WrapPoint::new(input_row, 0)).row + } + + pub fn line_len(&self, row: u32) -> u32 { + let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(); + cursor.seek(&BlockRow(row), Bias::Right, &()); + if let Some(transform) = cursor.item() { + let (output_start, input_start) = cursor.start(); + let overshoot = row - output_start.0; + if transform.block.is_some() { + 0 + } else { + self.wrap_snapshot.line_len(input_start.0 + overshoot) + } + } else { + panic!("row out of range"); + } + } + + pub fn is_block_line(&self, row: u32) -> bool { + let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(); + cursor.seek(&BlockRow(row), Bias::Right, &()); + cursor.item().map_or(false, |t| t.block.is_some()) + } + + pub fn clip_point(&self, point: BlockPoint, bias: Bias) -> BlockPoint { + let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(); + cursor.seek(&BlockRow(point.row), Bias::Right, &()); + + let max_input_row = WrapRow(self.transforms.summary().input_rows); + let mut search_left = + (bias == Bias::Left && cursor.start().1 .0 > 0) || cursor.end(&()).1 == max_input_row; + let mut reversed = false; + + loop { + if let Some(transform) = cursor.item() { + if transform.is_isomorphic() { + let (output_start_row, input_start_row) = cursor.start(); + let (output_end_row, input_end_row) = cursor.end(&()); + let output_start = Point::new(output_start_row.0, 0); + let input_start = Point::new(input_start_row.0, 0); + let input_end = Point::new(input_end_row.0, 0); + let input_point = if point.row >= output_end_row.0 { + let line_len = self.wrap_snapshot.line_len(input_end_row.0 - 1); + self.wrap_snapshot + .clip_point(WrapPoint::new(input_end_row.0 - 1, line_len), bias) + } else { + let output_overshoot = point.0.saturating_sub(output_start); + self.wrap_snapshot + .clip_point(WrapPoint(input_start + output_overshoot), bias) + }; + + if (input_start..input_end).contains(&input_point.0) { + let input_overshoot = input_point.0.saturating_sub(input_start); + return BlockPoint(output_start + input_overshoot); + } + } + + if search_left { + cursor.prev(&()); + } else { + cursor.next(&()); + } + } else if reversed { + return self.max_point(); + } else { + reversed = true; + search_left = !search_left; + cursor.seek(&BlockRow(point.row), Bias::Right, &()); + } + } + } + + pub fn to_block_point(&self, wrap_point: WrapPoint) -> BlockPoint { + let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(); + cursor.seek(&WrapRow(wrap_point.row()), Bias::Right, &()); + if let Some(transform) = cursor.item() { + debug_assert!(transform.is_isomorphic()); + } else { + return self.max_point(); + } + + let (input_start_row, output_start_row) = cursor.start(); + let input_start = Point::new(input_start_row.0, 0); + let output_start = Point::new(output_start_row.0, 0); + let input_overshoot = wrap_point.0 - input_start; + BlockPoint(output_start + input_overshoot) + } + + pub fn to_wrap_point(&self, block_point: BlockPoint) -> WrapPoint { + let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(); + cursor.seek(&BlockRow(block_point.row), Bias::Right, &()); + if let Some(transform) = cursor.item() { + match transform.block.as_ref().map(|b| b.disposition()) { + Some(BlockDisposition::Above) => WrapPoint::new(cursor.start().1 .0, 0), + Some(BlockDisposition::Below) => { + let wrap_row = cursor.start().1 .0 - 1; + WrapPoint::new(wrap_row, self.wrap_snapshot.line_len(wrap_row)) + } + None => { + let overshoot = block_point.row - cursor.start().0 .0; + let wrap_row = cursor.start().1 .0 + overshoot; + WrapPoint::new(wrap_row, block_point.column) + } + } + } else { + self.wrap_snapshot.max_point() + } + } +} + +impl Transform { + fn isomorphic(rows: u32) -> Self { + Self { + summary: TransformSummary { + input_rows: rows, + output_rows: rows, + }, + block: None, + } + } + + fn block(block: TransformBlock) -> Self { + Self { + summary: TransformSummary { + input_rows: 0, + output_rows: block.height() as u32, + }, + block: Some(block), + } + } + + fn is_isomorphic(&self) -> bool { + self.block.is_none() + } +} + +impl<'a> Iterator for BlockChunks<'a> { + type Item = Chunk<'a>; + + fn next(&mut self) -> Option { + if self.output_row >= self.max_output_row { + return None; + } + + let transform = self.transforms.item()?; + if transform.block.is_some() { + let block_start = self.transforms.start().0 .0; + let mut block_end = self.transforms.end(&()).0 .0; + self.transforms.next(&()); + if self.transforms.item().is_none() { + block_end -= 1; + } + + let start_in_block = self.output_row - block_start; + let end_in_block = cmp::min(self.max_output_row, block_end) - block_start; + let line_count = end_in_block - start_in_block; + self.output_row += line_count; + + return Some(Chunk { + text: unsafe { std::str::from_utf8_unchecked(&NEWLINES[..line_count as usize]) }, + ..Default::default() + }); + } + + if self.input_chunk.text.is_empty() { + if let Some(input_chunk) = self.input_chunks.next() { + self.input_chunk = input_chunk; + } else { + self.output_row += 1; + if self.output_row < self.max_output_row { + self.transforms.next(&()); + return Some(Chunk { + text: "\n", + ..Default::default() + }); + } else { + return None; + } + } + } + + let transform_end = self.transforms.end(&()).0 .0; + let (prefix_rows, prefix_bytes) = + offset_for_row(self.input_chunk.text, transform_end - self.output_row); + self.output_row += prefix_rows; + let (prefix, suffix) = self.input_chunk.text.split_at(prefix_bytes); + self.input_chunk.text = suffix; + if self.output_row == transform_end { + self.transforms.next(&()); + } + + Some(Chunk { + text: prefix, + ..self.input_chunk + }) + } +} + +impl<'a> Iterator for BlockBufferRows<'a> { + type Item = Option; + + fn next(&mut self) -> Option { + if self.started { + self.output_row += 1; + } else { + self.started = true; + } + + if self.output_row >= self.transforms.end(&()).0 .0 { + self.transforms.next(&()); + } + + let transform = self.transforms.item()?; + if transform.block.is_some() { + Some(None) + } else { + Some(self.input_buffer_rows.next().unwrap()) + } + } +} + +impl sum_tree::Item for Transform { + type Summary = TransformSummary; + + fn summary(&self) -> Self::Summary { + self.summary.clone() + } +} + +impl sum_tree::Summary for TransformSummary { + type Context = (); + + fn add_summary(&mut self, summary: &Self, _: &()) { + self.input_rows += summary.input_rows; + self.output_rows += summary.output_rows; + } +} + +impl<'a> sum_tree::Dimension<'a, TransformSummary> for WrapRow { + fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { + self.0 += summary.input_rows; + } +} + +impl<'a> sum_tree::Dimension<'a, TransformSummary> for BlockRow { + fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { + self.0 += summary.output_rows; + } +} + +impl BlockDisposition { + fn is_below(&self) -> bool { + matches!(self, BlockDisposition::Below) + } +} + +impl<'a, 'b, 'c> Deref for BlockContext<'a, 'b, 'c> { + type Target = ViewContext<'a, 'b, Editor>; + + fn deref(&self) -> &Self::Target { + self.view_context + } +} + +impl DerefMut for BlockContext<'_, '_, '_> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.view_context + } +} + +impl Block { + pub fn render(&self, cx: &mut BlockContext) -> AnyElement { + self.render.lock()(cx) + } + + pub fn position(&self) -> &Anchor { + &self.position + } + + pub fn style(&self) -> BlockStyle { + self.style + } +} + +impl Debug for Block { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Block") + .field("id", &self.id) + .field("position", &self.position) + .field("disposition", &self.disposition) + .finish() + } +} + +// Count the number of bytes prior to a target point. If the string doesn't contain the target +// point, return its total extent. Otherwise return the target point itself. +fn offset_for_row(s: &str, target: u32) -> (u32, usize) { + let mut row = 0; + let mut offset = 0; + for (ix, line) in s.split('\n').enumerate() { + if ix > 0 { + row += 1; + offset += 1; + } + if row >= target { + break; + } + offset += line.len() as usize; + } + (row, offset) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::display_map::inlay_map::InlayMap; + use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap}; + use gpui::{elements::Empty, Element}; + use multi_buffer::MultiBuffer; + use rand::prelude::*; + use settings::SettingsStore; + use std::env; + use util::RandomCharIter; + + #[gpui::test] + fn test_offset_for_row() { + assert_eq!(offset_for_row("", 0), (0, 0)); + assert_eq!(offset_for_row("", 1), (0, 0)); + assert_eq!(offset_for_row("abcd", 0), (0, 0)); + assert_eq!(offset_for_row("abcd", 1), (0, 4)); + assert_eq!(offset_for_row("\n", 0), (0, 0)); + assert_eq!(offset_for_row("\n", 1), (1, 1)); + assert_eq!(offset_for_row("abc\ndef\nghi", 0), (0, 0)); + assert_eq!(offset_for_row("abc\ndef\nghi", 1), (1, 4)); + assert_eq!(offset_for_row("abc\ndef\nghi", 2), (2, 8)); + assert_eq!(offset_for_row("abc\ndef\nghi", 3), (2, 11)); + } + + #[gpui::test] + fn test_basic_blocks(cx: &mut gpui::AppContext) { + init_test(cx); + + let family_id = cx + .font_cache() + .load_family(&["Helvetica"], &Default::default()) + .unwrap(); + let font_id = cx + .font_cache() + .select_font(family_id, &Default::default()) + .unwrap(); + + let text = "aaa\nbbb\nccc\nddd"; + + let buffer = MultiBuffer::build_simple(text, cx); + let buffer_snapshot = buffer.read(cx).snapshot(cx); + let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot); + let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap()); + let (wrap_map, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, None, cx); + let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1); + + let mut writer = block_map.write(wraps_snapshot.clone(), Default::default()); + let block_ids = writer.insert(vec![ + BlockProperties { + style: BlockStyle::Fixed, + position: buffer_snapshot.anchor_after(Point::new(1, 0)), + height: 1, + disposition: BlockDisposition::Above, + render: Arc::new(|_| Empty::new().into_any_named("block 1")), + }, + BlockProperties { + style: BlockStyle::Fixed, + position: buffer_snapshot.anchor_after(Point::new(1, 2)), + height: 2, + disposition: BlockDisposition::Above, + render: Arc::new(|_| Empty::new().into_any_named("block 2")), + }, + BlockProperties { + style: BlockStyle::Fixed, + position: buffer_snapshot.anchor_after(Point::new(3, 3)), + height: 3, + disposition: BlockDisposition::Below, + render: Arc::new(|_| Empty::new().into_any_named("block 3")), + }, + ]); + + let snapshot = block_map.read(wraps_snapshot, Default::default()); + assert_eq!(snapshot.text(), "aaa\n\n\n\nbbb\nccc\nddd\n\n\n"); + + let blocks = snapshot + .blocks_in_range(0..8) + .map(|(start_row, block)| { + let block = block.as_custom().unwrap(); + (start_row..start_row + block.height as u32, block.id) + }) + .collect::>(); + + // When multiple blocks are on the same line, the newer blocks appear first. + assert_eq!( + blocks, + &[ + (1..2, block_ids[0]), + (2..4, block_ids[1]), + (7..10, block_ids[2]), + ] + ); + + assert_eq!( + snapshot.to_block_point(WrapPoint::new(0, 3)), + BlockPoint::new(0, 3) + ); + assert_eq!( + snapshot.to_block_point(WrapPoint::new(1, 0)), + BlockPoint::new(4, 0) + ); + assert_eq!( + snapshot.to_block_point(WrapPoint::new(3, 3)), + BlockPoint::new(6, 3) + ); + + assert_eq!( + snapshot.to_wrap_point(BlockPoint::new(0, 3)), + WrapPoint::new(0, 3) + ); + assert_eq!( + snapshot.to_wrap_point(BlockPoint::new(1, 0)), + WrapPoint::new(1, 0) + ); + assert_eq!( + snapshot.to_wrap_point(BlockPoint::new(3, 0)), + WrapPoint::new(1, 0) + ); + assert_eq!( + snapshot.to_wrap_point(BlockPoint::new(7, 0)), + WrapPoint::new(3, 3) + ); + + assert_eq!( + snapshot.clip_point(BlockPoint::new(1, 0), Bias::Left), + BlockPoint::new(0, 3) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(1, 0), Bias::Right), + BlockPoint::new(4, 0) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(1, 1), Bias::Left), + BlockPoint::new(0, 3) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(1, 1), Bias::Right), + BlockPoint::new(4, 0) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(4, 0), Bias::Left), + BlockPoint::new(4, 0) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(4, 0), Bias::Right), + BlockPoint::new(4, 0) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(6, 3), Bias::Left), + BlockPoint::new(6, 3) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(6, 3), Bias::Right), + BlockPoint::new(6, 3) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(7, 0), Bias::Left), + BlockPoint::new(6, 3) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(7, 0), Bias::Right), + BlockPoint::new(6, 3) + ); + + assert_eq!( + snapshot.buffer_rows(0).collect::>(), + &[ + Some(0), + None, + None, + None, + Some(1), + Some(2), + Some(3), + None, + None, + None + ] + ); + + // Insert a line break, separating two block decorations into separate lines. + let buffer_snapshot = buffer.update(cx, |buffer, cx| { + buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "!!!\n")], None, cx); + buffer.snapshot(cx) + }); + + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot, subscription.consume().into_inner()); + let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); + let (tab_snapshot, tab_edits) = + tab_map.sync(fold_snapshot, fold_edits, 4.try_into().unwrap()); + let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { + wrap_map.sync(tab_snapshot, tab_edits, cx) + }); + let snapshot = block_map.read(wraps_snapshot, wrap_edits); + assert_eq!(snapshot.text(), "aaa\n\nb!!!\n\n\nbb\nccc\nddd\n\n\n"); + } + + #[gpui::test] + fn test_blocks_on_wrapped_lines(cx: &mut gpui::AppContext) { + init_test(cx); + + let family_id = cx + .font_cache() + .load_family(&["Helvetica"], &Default::default()) + .unwrap(); + let font_id = cx + .font_cache() + .select_font(family_id, &Default::default()) + .unwrap(); + + let text = "one two three\nfour five six\nseven eight"; + + let buffer = MultiBuffer::build_simple(text, cx); + let buffer_snapshot = buffer.read(cx).snapshot(cx); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); + let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); + let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, Some(60.), cx); + let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1); + + let mut writer = block_map.write(wraps_snapshot.clone(), Default::default()); + writer.insert(vec![ + BlockProperties { + style: BlockStyle::Fixed, + position: buffer_snapshot.anchor_after(Point::new(1, 12)), + disposition: BlockDisposition::Above, + render: Arc::new(|_| Empty::new().into_any_named("block 1")), + height: 1, + }, + BlockProperties { + style: BlockStyle::Fixed, + position: buffer_snapshot.anchor_after(Point::new(1, 1)), + disposition: BlockDisposition::Below, + render: Arc::new(|_| Empty::new().into_any_named("block 2")), + height: 1, + }, + ]); + + // Blocks with an 'above' disposition go above their corresponding buffer line. + // Blocks with a 'below' disposition go below their corresponding buffer line. + let snapshot = block_map.read(wraps_snapshot, Default::default()); + assert_eq!( + snapshot.text(), + "one two \nthree\n\nfour five \nsix\n\nseven \neight" + ); + } + + #[gpui::test(iterations = 100)] + fn test_random_blocks(cx: &mut gpui::AppContext, mut rng: StdRng) { + init_test(cx); + + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + + let wrap_width = if rng.gen_bool(0.2) { + None + } else { + Some(rng.gen_range(0.0..=100.0)) + }; + let tab_size = 1.try_into().unwrap(); + let family_id = cx + .font_cache() + .load_family(&["Helvetica"], &Default::default()) + .unwrap(); + let font_id = cx + .font_cache() + .select_font(family_id, &Default::default()) + .unwrap(); + let font_size = 14.0; + let buffer_start_header_height = rng.gen_range(1..=5); + let excerpt_header_height = rng.gen_range(1..=5); + + log::info!("Wrap width: {:?}", wrap_width); + log::info!("Excerpt Header Height: {:?}", excerpt_header_height); + + let buffer = if rng.gen() { + let len = rng.gen_range(0..10); + let text = RandomCharIter::new(&mut rng).take(len).collect::(); + log::info!("initial buffer text: {:?}", text); + MultiBuffer::build_simple(&text, cx) + } else { + MultiBuffer::build_random(&mut rng, cx) + }; + + let mut buffer_snapshot = buffer.read(cx).snapshot(cx); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot); + let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); + let (wrap_map, wraps_snapshot) = + WrapMap::new(tab_snapshot, font_id, font_size, wrap_width, cx); + let mut block_map = BlockMap::new( + wraps_snapshot, + buffer_start_header_height, + excerpt_header_height, + ); + let mut custom_blocks = Vec::new(); + + for _ in 0..operations { + let mut buffer_edits = Vec::new(); + match rng.gen_range(0..=100) { + 0..=19 => { + let wrap_width = if rng.gen_bool(0.2) { + None + } else { + Some(rng.gen_range(0.0..=100.0)) + }; + log::info!("Setting wrap width to {:?}", wrap_width); + wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); + } + 20..=39 => { + let block_count = rng.gen_range(1..=5); + let block_properties = (0..block_count) + .map(|_| { + let buffer = buffer.read(cx).read(cx); + let position = buffer.anchor_after( + buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Left), + ); + + let disposition = if rng.gen() { + BlockDisposition::Above + } else { + BlockDisposition::Below + }; + let height = rng.gen_range(1..5); + log::info!( + "inserting block {:?} {:?} with height {}", + disposition, + position.to_point(&buffer), + height + ); + BlockProperties { + style: BlockStyle::Fixed, + position, + height, + disposition, + render: Arc::new(|_| Empty::new().into_any()), + } + }) + .collect::>(); + + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot.clone(), vec![]); + let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); + let (tab_snapshot, tab_edits) = + tab_map.sync(fold_snapshot, fold_edits, tab_size); + let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { + wrap_map.sync(tab_snapshot, tab_edits, cx) + }); + let mut block_map = block_map.write(wraps_snapshot, wrap_edits); + let block_ids = block_map.insert(block_properties.clone()); + for (block_id, props) in block_ids.into_iter().zip(block_properties) { + custom_blocks.push((block_id, props)); + } + } + 40..=59 if !custom_blocks.is_empty() => { + let block_count = rng.gen_range(1..=4.min(custom_blocks.len())); + let block_ids_to_remove = (0..block_count) + .map(|_| { + custom_blocks + .remove(rng.gen_range(0..custom_blocks.len())) + .0 + }) + .collect(); + + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot.clone(), vec![]); + let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); + let (tab_snapshot, tab_edits) = + tab_map.sync(fold_snapshot, fold_edits, tab_size); + let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { + wrap_map.sync(tab_snapshot, tab_edits, cx) + }); + let mut block_map = block_map.write(wraps_snapshot, wrap_edits); + block_map.remove(block_ids_to_remove); + } + _ => { + buffer.update(cx, |buffer, cx| { + let mutation_count = rng.gen_range(1..=5); + let subscription = buffer.subscribe(); + buffer.randomly_mutate(&mut rng, mutation_count, cx); + buffer_snapshot = buffer.snapshot(cx); + buffer_edits.extend(subscription.consume()); + log::info!("buffer text: {:?}", buffer_snapshot.text()); + }); + } + } + + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot.clone(), buffer_edits); + let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); + let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size); + let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { + wrap_map.sync(tab_snapshot, tab_edits, cx) + }); + let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits); + assert_eq!( + blocks_snapshot.transforms.summary().input_rows, + wraps_snapshot.max_point().row() + 1 + ); + log::info!("blocks text: {:?}", blocks_snapshot.text()); + + let mut expected_blocks = Vec::new(); + expected_blocks.extend(custom_blocks.iter().map(|(id, block)| { + let mut position = block.position.to_point(&buffer_snapshot); + match block.disposition { + BlockDisposition::Above => { + position.column = 0; + } + BlockDisposition::Below => { + position.column = buffer_snapshot.line_len(position.row); + } + }; + let row = wraps_snapshot.make_wrap_point(position, Bias::Left).row(); + ( + row, + ExpectedBlock::Custom { + disposition: block.disposition, + id: *id, + height: block.height, + }, + ) + })); + expected_blocks.extend(buffer_snapshot.excerpt_boundaries_in_range(0..).map( + |boundary| { + let position = + wraps_snapshot.make_wrap_point(Point::new(boundary.row, 0), Bias::Left); + ( + position.row(), + ExpectedBlock::ExcerptHeader { + height: if boundary.starts_new_buffer { + buffer_start_header_height + } else { + excerpt_header_height + }, + starts_new_buffer: boundary.starts_new_buffer, + }, + ) + }, + )); + expected_blocks.sort_unstable(); + let mut sorted_blocks_iter = expected_blocks.into_iter().peekable(); + + let input_buffer_rows = buffer_snapshot.buffer_rows(0).collect::>(); + let mut expected_buffer_rows = Vec::new(); + let mut expected_text = String::new(); + let mut expected_block_positions = Vec::new(); + let input_text = wraps_snapshot.text(); + for (row, input_line) in input_text.split('\n').enumerate() { + let row = row as u32; + if row > 0 { + expected_text.push('\n'); + } + + let buffer_row = input_buffer_rows[wraps_snapshot + .to_point(WrapPoint::new(row, 0), Bias::Left) + .row as usize]; + + while let Some((block_row, block)) = sorted_blocks_iter.peek() { + if *block_row == row && block.disposition() == BlockDisposition::Above { + let (_, block) = sorted_blocks_iter.next().unwrap(); + let height = block.height() as usize; + expected_block_positions + .push((expected_text.matches('\n').count() as u32, block)); + let text = "\n".repeat(height); + expected_text.push_str(&text); + for _ in 0..height { + expected_buffer_rows.push(None); + } + } else { + break; + } + } + + let soft_wrapped = wraps_snapshot.to_tab_point(WrapPoint::new(row, 0)).column() > 0; + expected_buffer_rows.push(if soft_wrapped { None } else { buffer_row }); + expected_text.push_str(input_line); + + while let Some((block_row, block)) = sorted_blocks_iter.peek() { + if *block_row == row && block.disposition() == BlockDisposition::Below { + let (_, block) = sorted_blocks_iter.next().unwrap(); + let height = block.height() as usize; + expected_block_positions + .push((expected_text.matches('\n').count() as u32 + 1, block)); + let text = "\n".repeat(height); + expected_text.push_str(&text); + for _ in 0..height { + expected_buffer_rows.push(None); + } + } else { + break; + } + } + } + + let expected_lines = expected_text.split('\n').collect::>(); + let expected_row_count = expected_lines.len(); + for start_row in 0..expected_row_count { + let expected_text = expected_lines[start_row..].join("\n"); + let actual_text = blocks_snapshot + .chunks( + start_row as u32..blocks_snapshot.max_point().row + 1, + false, + Highlights::default(), + ) + .map(|chunk| chunk.text) + .collect::(); + assert_eq!( + actual_text, expected_text, + "incorrect text starting from row {}", + start_row + ); + assert_eq!( + blocks_snapshot + .buffer_rows(start_row as u32) + .collect::>(), + &expected_buffer_rows[start_row..] + ); + } + + assert_eq!( + blocks_snapshot + .blocks_in_range(0..(expected_row_count as u32)) + .map(|(row, block)| (row, block.clone().into())) + .collect::>(), + expected_block_positions + ); + + let mut expected_longest_rows = Vec::new(); + let mut longest_line_len = -1_isize; + for (row, line) in expected_lines.iter().enumerate() { + let row = row as u32; + + assert_eq!( + blocks_snapshot.line_len(row), + line.len() as u32, + "invalid line len for row {}", + row + ); + + let line_char_count = line.chars().count() as isize; + match line_char_count.cmp(&longest_line_len) { + Ordering::Less => {} + Ordering::Equal => expected_longest_rows.push(row), + Ordering::Greater => { + longest_line_len = line_char_count; + expected_longest_rows.clear(); + expected_longest_rows.push(row); + } + } + } + + let longest_row = blocks_snapshot.longest_row(); + assert!( + expected_longest_rows.contains(&longest_row), + "incorrect longest row {}. expected {:?} with length {}", + longest_row, + expected_longest_rows, + longest_line_len, + ); + + for row in 0..=blocks_snapshot.wrap_snapshot.max_point().row() { + let wrap_point = WrapPoint::new(row, 0); + let block_point = blocks_snapshot.to_block_point(wrap_point); + assert_eq!(blocks_snapshot.to_wrap_point(block_point), wrap_point); + } + + let mut block_point = BlockPoint::new(0, 0); + for c in expected_text.chars() { + let left_point = blocks_snapshot.clip_point(block_point, Bias::Left); + let left_buffer_point = blocks_snapshot.to_point(left_point, Bias::Left); + assert_eq!( + blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(left_point)), + left_point + ); + assert_eq!( + left_buffer_point, + buffer_snapshot.clip_point(left_buffer_point, Bias::Right), + "{:?} is not valid in buffer coordinates", + left_point + ); + + let right_point = blocks_snapshot.clip_point(block_point, Bias::Right); + let right_buffer_point = blocks_snapshot.to_point(right_point, Bias::Right); + assert_eq!( + blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(right_point)), + right_point + ); + assert_eq!( + right_buffer_point, + buffer_snapshot.clip_point(right_buffer_point, Bias::Left), + "{:?} is not valid in buffer coordinates", + right_point + ); + + if c == '\n' { + block_point.0 += Point::new(1, 0); + } else { + block_point.column += c.len_utf8() as u32; + } + } + } + + #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] + enum ExpectedBlock { + ExcerptHeader { + height: u8, + starts_new_buffer: bool, + }, + Custom { + disposition: BlockDisposition, + id: BlockId, + height: u8, + }, + } + + impl ExpectedBlock { + fn height(&self) -> u8 { + match self { + ExpectedBlock::ExcerptHeader { height, .. } => *height, + ExpectedBlock::Custom { height, .. } => *height, + } + } + + fn disposition(&self) -> BlockDisposition { + match self { + ExpectedBlock::ExcerptHeader { .. } => BlockDisposition::Above, + ExpectedBlock::Custom { disposition, .. } => *disposition, + } + } + } + + impl From for ExpectedBlock { + fn from(block: TransformBlock) -> Self { + match block { + TransformBlock::Custom(block) => ExpectedBlock::Custom { + id: block.id, + disposition: block.disposition, + height: block.height, + }, + TransformBlock::ExcerptHeader { + height, + starts_new_buffer, + .. + } => ExpectedBlock::ExcerptHeader { + height, + starts_new_buffer, + }, + } + } + } + } + + fn init_test(cx: &mut gpui::AppContext) { + cx.set_global(SettingsStore::test(cx)); + theme::init((), cx); + } + + impl TransformBlock { + fn as_custom(&self) -> Option<&Block> { + match self { + TransformBlock::Custom(block) => Some(block), + TransformBlock::ExcerptHeader { .. } => None, + } + } + } + + impl BlockSnapshot { + fn to_point(&self, point: BlockPoint, bias: Bias) -> Point { + self.wrap_snapshot.to_point(self.to_wrap_point(point), bias) + } + } +} diff --git a/crates/editor2/src/display_map/fold_map.rs b/crates/editor2/src/display_map/fold_map.rs new file mode 100644 index 0000000000..4636d9a17f --- /dev/null +++ b/crates/editor2/src/display_map/fold_map.rs @@ -0,0 +1,1706 @@ +use super::{ + inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot}, + Highlights, +}; +use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset}; +use gpui::{color::Color, fonts::HighlightStyle}; +use language::{Chunk, Edit, Point, TextSummary}; +use std::{ + any::TypeId, + cmp::{self, Ordering}, + iter, + ops::{Add, AddAssign, Range, Sub}, +}; +use sum_tree::{Bias, Cursor, FilterCursor, SumTree}; + +#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] +pub struct FoldPoint(pub Point); + +impl FoldPoint { + pub fn new(row: u32, column: u32) -> Self { + Self(Point::new(row, column)) + } + + pub fn row(self) -> u32 { + self.0.row + } + + pub fn column(self) -> u32 { + self.0.column + } + + pub fn row_mut(&mut self) -> &mut u32 { + &mut self.0.row + } + + #[cfg(test)] + pub fn column_mut(&mut self) -> &mut u32 { + &mut self.0.column + } + + pub fn to_inlay_point(self, snapshot: &FoldSnapshot) -> InlayPoint { + let mut cursor = snapshot.transforms.cursor::<(FoldPoint, InlayPoint)>(); + cursor.seek(&self, Bias::Right, &()); + let overshoot = self.0 - cursor.start().0 .0; + InlayPoint(cursor.start().1 .0 + overshoot) + } + + pub fn to_offset(self, snapshot: &FoldSnapshot) -> FoldOffset { + let mut cursor = snapshot + .transforms + .cursor::<(FoldPoint, TransformSummary)>(); + cursor.seek(&self, Bias::Right, &()); + let overshoot = self.0 - cursor.start().1.output.lines; + let mut offset = cursor.start().1.output.len; + if !overshoot.is_zero() { + let transform = cursor.item().expect("display point out of range"); + assert!(transform.output_text.is_none()); + let end_inlay_offset = snapshot + .inlay_snapshot + .to_offset(InlayPoint(cursor.start().1.input.lines + overshoot)); + offset += end_inlay_offset.0 - cursor.start().1.input.len; + } + FoldOffset(offset) + } +} + +impl<'a> sum_tree::Dimension<'a, TransformSummary> for FoldPoint { + fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { + self.0 += &summary.output.lines; + } +} + +pub struct FoldMapWriter<'a>(&'a mut FoldMap); + +impl<'a> FoldMapWriter<'a> { + pub fn fold( + &mut self, + ranges: impl IntoIterator>, + ) -> (FoldSnapshot, Vec) { + let mut edits = Vec::new(); + let mut folds = Vec::new(); + let snapshot = self.0.snapshot.inlay_snapshot.clone(); + for range in ranges.into_iter() { + let buffer = &snapshot.buffer; + let range = range.start.to_offset(&buffer)..range.end.to_offset(&buffer); + + // Ignore any empty ranges. + if range.start == range.end { + continue; + } + + // For now, ignore any ranges that span an excerpt boundary. + let fold = Fold(buffer.anchor_after(range.start)..buffer.anchor_before(range.end)); + if fold.0.start.excerpt_id != fold.0.end.excerpt_id { + continue; + } + + folds.push(fold); + + let inlay_range = + snapshot.to_inlay_offset(range.start)..snapshot.to_inlay_offset(range.end); + edits.push(InlayEdit { + old: inlay_range.clone(), + new: inlay_range, + }); + } + + let buffer = &snapshot.buffer; + folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(a, b, buffer)); + + self.0.snapshot.folds = { + let mut new_tree = SumTree::new(); + let mut cursor = self.0.snapshot.folds.cursor::(); + for fold in folds { + new_tree.append(cursor.slice(&fold, Bias::Right, buffer), buffer); + new_tree.push(fold, buffer); + } + new_tree.append(cursor.suffix(buffer), buffer); + new_tree + }; + + consolidate_inlay_edits(&mut edits); + let edits = self.0.sync(snapshot.clone(), edits); + (self.0.snapshot.clone(), edits) + } + + pub fn unfold( + &mut self, + ranges: impl IntoIterator>, + inclusive: bool, + ) -> (FoldSnapshot, Vec) { + let mut edits = Vec::new(); + let mut fold_ixs_to_delete = Vec::new(); + let snapshot = self.0.snapshot.inlay_snapshot.clone(); + let buffer = &snapshot.buffer; + for range in ranges.into_iter() { + // Remove intersecting folds and add their ranges to edits that are passed to sync. + let mut folds_cursor = + intersecting_folds(&snapshot, &self.0.snapshot.folds, range, inclusive); + while let Some(fold) = folds_cursor.item() { + let offset_range = fold.0.start.to_offset(buffer)..fold.0.end.to_offset(buffer); + if offset_range.end > offset_range.start { + let inlay_range = snapshot.to_inlay_offset(offset_range.start) + ..snapshot.to_inlay_offset(offset_range.end); + edits.push(InlayEdit { + old: inlay_range.clone(), + new: inlay_range, + }); + } + fold_ixs_to_delete.push(*folds_cursor.start()); + folds_cursor.next(buffer); + } + } + + fold_ixs_to_delete.sort_unstable(); + fold_ixs_to_delete.dedup(); + + self.0.snapshot.folds = { + let mut cursor = self.0.snapshot.folds.cursor::(); + let mut folds = SumTree::new(); + for fold_ix in fold_ixs_to_delete { + folds.append(cursor.slice(&fold_ix, Bias::Right, buffer), buffer); + cursor.next(buffer); + } + folds.append(cursor.suffix(buffer), buffer); + folds + }; + + consolidate_inlay_edits(&mut edits); + let edits = self.0.sync(snapshot.clone(), edits); + (self.0.snapshot.clone(), edits) + } +} + +pub struct FoldMap { + snapshot: FoldSnapshot, + ellipses_color: Option, +} + +impl FoldMap { + pub fn new(inlay_snapshot: InlaySnapshot) -> (Self, FoldSnapshot) { + let this = Self { + snapshot: FoldSnapshot { + folds: Default::default(), + transforms: SumTree::from_item( + Transform { + summary: TransformSummary { + input: inlay_snapshot.text_summary(), + output: inlay_snapshot.text_summary(), + }, + output_text: None, + }, + &(), + ), + inlay_snapshot: inlay_snapshot.clone(), + version: 0, + ellipses_color: None, + }, + ellipses_color: None, + }; + let snapshot = this.snapshot.clone(); + (this, snapshot) + } + + pub fn read( + &mut self, + inlay_snapshot: InlaySnapshot, + edits: Vec, + ) -> (FoldSnapshot, Vec) { + let edits = self.sync(inlay_snapshot, edits); + self.check_invariants(); + (self.snapshot.clone(), edits) + } + + pub fn write( + &mut self, + inlay_snapshot: InlaySnapshot, + edits: Vec, + ) -> (FoldMapWriter, FoldSnapshot, Vec) { + let (snapshot, edits) = self.read(inlay_snapshot, edits); + (FoldMapWriter(self), snapshot, edits) + } + + pub fn set_ellipses_color(&mut self, color: Color) -> bool { + if self.ellipses_color != Some(color) { + self.ellipses_color = Some(color); + true + } else { + false + } + } + + fn check_invariants(&self) { + if cfg!(test) { + assert_eq!( + self.snapshot.transforms.summary().input.len, + self.snapshot.inlay_snapshot.len().0, + "transform tree does not match inlay snapshot's length" + ); + + let mut folds = self.snapshot.folds.iter().peekable(); + while let Some(fold) = folds.next() { + if let Some(next_fold) = folds.peek() { + let comparison = fold + .0 + .cmp(&next_fold.0, &self.snapshot.inlay_snapshot.buffer); + assert!(comparison.is_le()); + } + } + } + } + + fn sync( + &mut self, + inlay_snapshot: InlaySnapshot, + inlay_edits: Vec, + ) -> Vec { + if inlay_edits.is_empty() { + if self.snapshot.inlay_snapshot.version != inlay_snapshot.version { + self.snapshot.version += 1; + } + self.snapshot.inlay_snapshot = inlay_snapshot; + Vec::new() + } else { + let mut inlay_edits_iter = inlay_edits.iter().cloned().peekable(); + + let mut new_transforms = SumTree::new(); + let mut cursor = self.snapshot.transforms.cursor::(); + cursor.seek(&InlayOffset(0), Bias::Right, &()); + + while let Some(mut edit) = inlay_edits_iter.next() { + new_transforms.append(cursor.slice(&edit.old.start, Bias::Left, &()), &()); + edit.new.start -= edit.old.start - *cursor.start(); + edit.old.start = *cursor.start(); + + cursor.seek(&edit.old.end, Bias::Right, &()); + cursor.next(&()); + + let mut delta = edit.new_len().0 as isize - edit.old_len().0 as isize; + loop { + edit.old.end = *cursor.start(); + + if let Some(next_edit) = inlay_edits_iter.peek() { + if next_edit.old.start > edit.old.end { + break; + } + + let next_edit = inlay_edits_iter.next().unwrap(); + delta += next_edit.new_len().0 as isize - next_edit.old_len().0 as isize; + + if next_edit.old.end >= edit.old.end { + edit.old.end = next_edit.old.end; + cursor.seek(&edit.old.end, Bias::Right, &()); + cursor.next(&()); + } + } else { + break; + } + } + + edit.new.end = + InlayOffset(((edit.new.start + edit.old_len()).0 as isize + delta) as usize); + + let anchor = inlay_snapshot + .buffer + .anchor_before(inlay_snapshot.to_buffer_offset(edit.new.start)); + let mut folds_cursor = self.snapshot.folds.cursor::(); + folds_cursor.seek( + &Fold(anchor..Anchor::max()), + Bias::Left, + &inlay_snapshot.buffer, + ); + + let mut folds = iter::from_fn({ + let inlay_snapshot = &inlay_snapshot; + move || { + let item = folds_cursor.item().map(|f| { + let buffer_start = f.0.start.to_offset(&inlay_snapshot.buffer); + let buffer_end = f.0.end.to_offset(&inlay_snapshot.buffer); + inlay_snapshot.to_inlay_offset(buffer_start) + ..inlay_snapshot.to_inlay_offset(buffer_end) + }); + folds_cursor.next(&inlay_snapshot.buffer); + item + } + }) + .peekable(); + + while folds.peek().map_or(false, |fold| fold.start < edit.new.end) { + let mut fold = folds.next().unwrap(); + let sum = new_transforms.summary(); + + assert!(fold.start.0 >= sum.input.len); + + while folds + .peek() + .map_or(false, |next_fold| next_fold.start <= fold.end) + { + let next_fold = folds.next().unwrap(); + if next_fold.end > fold.end { + fold.end = next_fold.end; + } + } + + if fold.start.0 > sum.input.len { + let text_summary = inlay_snapshot + .text_summary_for_range(InlayOffset(sum.input.len)..fold.start); + new_transforms.push( + Transform { + summary: TransformSummary { + output: text_summary.clone(), + input: text_summary, + }, + output_text: None, + }, + &(), + ); + } + + if fold.end > fold.start { + let output_text = "⋯"; + new_transforms.push( + Transform { + summary: TransformSummary { + output: TextSummary::from(output_text), + input: inlay_snapshot + .text_summary_for_range(fold.start..fold.end), + }, + output_text: Some(output_text), + }, + &(), + ); + } + } + + let sum = new_transforms.summary(); + if sum.input.len < edit.new.end.0 { + let text_summary = inlay_snapshot + .text_summary_for_range(InlayOffset(sum.input.len)..edit.new.end); + new_transforms.push( + Transform { + summary: TransformSummary { + output: text_summary.clone(), + input: text_summary, + }, + output_text: None, + }, + &(), + ); + } + } + + new_transforms.append(cursor.suffix(&()), &()); + if new_transforms.is_empty() { + let text_summary = inlay_snapshot.text_summary(); + new_transforms.push( + Transform { + summary: TransformSummary { + output: text_summary.clone(), + input: text_summary, + }, + output_text: None, + }, + &(), + ); + } + + drop(cursor); + + let mut fold_edits = Vec::with_capacity(inlay_edits.len()); + { + let mut old_transforms = self + .snapshot + .transforms + .cursor::<(InlayOffset, FoldOffset)>(); + let mut new_transforms = new_transforms.cursor::<(InlayOffset, FoldOffset)>(); + + for mut edit in inlay_edits { + old_transforms.seek(&edit.old.start, Bias::Left, &()); + if old_transforms.item().map_or(false, |t| t.is_fold()) { + edit.old.start = old_transforms.start().0; + } + let old_start = + old_transforms.start().1 .0 + (edit.old.start - old_transforms.start().0).0; + + old_transforms.seek_forward(&edit.old.end, Bias::Right, &()); + if old_transforms.item().map_or(false, |t| t.is_fold()) { + old_transforms.next(&()); + edit.old.end = old_transforms.start().0; + } + let old_end = + old_transforms.start().1 .0 + (edit.old.end - old_transforms.start().0).0; + + new_transforms.seek(&edit.new.start, Bias::Left, &()); + if new_transforms.item().map_or(false, |t| t.is_fold()) { + edit.new.start = new_transforms.start().0; + } + let new_start = + new_transforms.start().1 .0 + (edit.new.start - new_transforms.start().0).0; + + new_transforms.seek_forward(&edit.new.end, Bias::Right, &()); + if new_transforms.item().map_or(false, |t| t.is_fold()) { + new_transforms.next(&()); + edit.new.end = new_transforms.start().0; + } + let new_end = + new_transforms.start().1 .0 + (edit.new.end - new_transforms.start().0).0; + + fold_edits.push(FoldEdit { + old: FoldOffset(old_start)..FoldOffset(old_end), + new: FoldOffset(new_start)..FoldOffset(new_end), + }); + } + + consolidate_fold_edits(&mut fold_edits); + } + + self.snapshot.transforms = new_transforms; + self.snapshot.inlay_snapshot = inlay_snapshot; + self.snapshot.version += 1; + fold_edits + } + } +} + +#[derive(Clone)] +pub struct FoldSnapshot { + transforms: SumTree, + folds: SumTree, + pub inlay_snapshot: InlaySnapshot, + pub version: usize, + pub ellipses_color: Option, +} + +impl FoldSnapshot { + #[cfg(test)] + pub fn text(&self) -> String { + self.chunks(FoldOffset(0)..self.len(), false, Highlights::default()) + .map(|c| c.text) + .collect() + } + + #[cfg(test)] + pub fn fold_count(&self) -> usize { + self.folds.items(&self.inlay_snapshot.buffer).len() + } + + pub fn text_summary_for_range(&self, range: Range) -> TextSummary { + let mut summary = TextSummary::default(); + + let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>(); + cursor.seek(&range.start, Bias::Right, &()); + if let Some(transform) = cursor.item() { + let start_in_transform = range.start.0 - cursor.start().0 .0; + let end_in_transform = cmp::min(range.end, cursor.end(&()).0).0 - cursor.start().0 .0; + if let Some(output_text) = transform.output_text { + summary = TextSummary::from( + &output_text + [start_in_transform.column as usize..end_in_transform.column as usize], + ); + } else { + let inlay_start = self + .inlay_snapshot + .to_offset(InlayPoint(cursor.start().1 .0 + start_in_transform)); + let inlay_end = self + .inlay_snapshot + .to_offset(InlayPoint(cursor.start().1 .0 + end_in_transform)); + summary = self + .inlay_snapshot + .text_summary_for_range(inlay_start..inlay_end); + } + } + + if range.end > cursor.end(&()).0 { + cursor.next(&()); + summary += &cursor + .summary::<_, TransformSummary>(&range.end, Bias::Right, &()) + .output; + if let Some(transform) = cursor.item() { + let end_in_transform = range.end.0 - cursor.start().0 .0; + if let Some(output_text) = transform.output_text { + summary += TextSummary::from(&output_text[..end_in_transform.column as usize]); + } else { + let inlay_start = self.inlay_snapshot.to_offset(cursor.start().1); + let inlay_end = self + .inlay_snapshot + .to_offset(InlayPoint(cursor.start().1 .0 + end_in_transform)); + summary += self + .inlay_snapshot + .text_summary_for_range(inlay_start..inlay_end); + } + } + } + + summary + } + + pub fn to_fold_point(&self, point: InlayPoint, bias: Bias) -> FoldPoint { + let mut cursor = self.transforms.cursor::<(InlayPoint, FoldPoint)>(); + cursor.seek(&point, Bias::Right, &()); + if cursor.item().map_or(false, |t| t.is_fold()) { + if bias == Bias::Left || point == cursor.start().0 { + cursor.start().1 + } else { + cursor.end(&()).1 + } + } else { + let overshoot = point.0 - cursor.start().0 .0; + FoldPoint(cmp::min( + cursor.start().1 .0 + overshoot, + cursor.end(&()).1 .0, + )) + } + } + + pub fn len(&self) -> FoldOffset { + FoldOffset(self.transforms.summary().output.len) + } + + pub fn line_len(&self, row: u32) -> u32 { + let line_start = FoldPoint::new(row, 0).to_offset(self).0; + let line_end = if row >= self.max_point().row() { + self.len().0 + } else { + FoldPoint::new(row + 1, 0).to_offset(self).0 - 1 + }; + (line_end - line_start) as u32 + } + + pub fn buffer_rows(&self, start_row: u32) -> FoldBufferRows { + if start_row > self.transforms.summary().output.lines.row { + panic!("invalid display row {}", start_row); + } + + let fold_point = FoldPoint::new(start_row, 0); + let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>(); + cursor.seek(&fold_point, Bias::Left, &()); + + let overshoot = fold_point.0 - cursor.start().0 .0; + let inlay_point = InlayPoint(cursor.start().1 .0 + overshoot); + let input_buffer_rows = self.inlay_snapshot.buffer_rows(inlay_point.row()); + + FoldBufferRows { + fold_point, + input_buffer_rows, + cursor, + } + } + + pub fn max_point(&self) -> FoldPoint { + FoldPoint(self.transforms.summary().output.lines) + } + + #[cfg(test)] + pub fn longest_row(&self) -> u32 { + self.transforms.summary().output.longest_row + } + + pub fn folds_in_range(&self, range: Range) -> impl Iterator> + where + T: ToOffset, + { + let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false); + iter::from_fn(move || { + let item = folds.item().map(|f| &f.0); + folds.next(&self.inlay_snapshot.buffer); + item + }) + } + + pub fn intersects_fold(&self, offset: T) -> bool + where + T: ToOffset, + { + let buffer_offset = offset.to_offset(&self.inlay_snapshot.buffer); + let inlay_offset = self.inlay_snapshot.to_inlay_offset(buffer_offset); + let mut cursor = self.transforms.cursor::(); + cursor.seek(&inlay_offset, Bias::Right, &()); + cursor.item().map_or(false, |t| t.output_text.is_some()) + } + + pub fn is_line_folded(&self, buffer_row: u32) -> bool { + let mut inlay_point = self + .inlay_snapshot + .to_inlay_point(Point::new(buffer_row, 0)); + let mut cursor = self.transforms.cursor::(); + cursor.seek(&inlay_point, Bias::Right, &()); + loop { + match cursor.item() { + Some(transform) => { + let buffer_point = self.inlay_snapshot.to_buffer_point(inlay_point); + if buffer_point.row != buffer_row { + return false; + } else if transform.output_text.is_some() { + return true; + } + } + None => return false, + } + + if cursor.end(&()).row() == inlay_point.row() { + cursor.next(&()); + } else { + inlay_point.0 += Point::new(1, 0); + cursor.seek(&inlay_point, Bias::Right, &()); + } + } + } + + pub fn chunks<'a>( + &'a self, + range: Range, + language_aware: bool, + highlights: Highlights<'a>, + ) -> FoldChunks<'a> { + let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>(); + + let inlay_end = { + transform_cursor.seek(&range.end, Bias::Right, &()); + let overshoot = range.end.0 - transform_cursor.start().0 .0; + transform_cursor.start().1 + InlayOffset(overshoot) + }; + + let inlay_start = { + transform_cursor.seek(&range.start, Bias::Right, &()); + let overshoot = range.start.0 - transform_cursor.start().0 .0; + transform_cursor.start().1 + InlayOffset(overshoot) + }; + + FoldChunks { + transform_cursor, + inlay_chunks: self.inlay_snapshot.chunks( + inlay_start..inlay_end, + language_aware, + highlights, + ), + inlay_chunk: None, + inlay_offset: inlay_start, + output_offset: range.start.0, + max_output_offset: range.end.0, + ellipses_color: self.ellipses_color, + } + } + + pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator { + self.chunks( + start.to_offset(self)..self.len(), + false, + Highlights::default(), + ) + .flat_map(|chunk| chunk.text.chars()) + } + + #[cfg(test)] + pub fn clip_offset(&self, offset: FoldOffset, bias: Bias) -> FoldOffset { + if offset > self.len() { + self.len() + } else { + self.clip_point(offset.to_point(self), bias).to_offset(self) + } + } + + pub fn clip_point(&self, point: FoldPoint, bias: Bias) -> FoldPoint { + let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>(); + cursor.seek(&point, Bias::Right, &()); + if let Some(transform) = cursor.item() { + let transform_start = cursor.start().0 .0; + if transform.output_text.is_some() { + if point.0 == transform_start || matches!(bias, Bias::Left) { + FoldPoint(transform_start) + } else { + FoldPoint(cursor.end(&()).0 .0) + } + } else { + let overshoot = InlayPoint(point.0 - transform_start); + let inlay_point = cursor.start().1 + overshoot; + let clipped_inlay_point = self.inlay_snapshot.clip_point(inlay_point, bias); + FoldPoint(cursor.start().0 .0 + (clipped_inlay_point - cursor.start().1).0) + } + } else { + FoldPoint(self.transforms.summary().output.lines) + } + } +} + +fn intersecting_folds<'a, T>( + inlay_snapshot: &'a InlaySnapshot, + folds: &'a SumTree, + range: Range, + inclusive: bool, +) -> FilterCursor<'a, impl 'a + FnMut(&FoldSummary) -> bool, Fold, usize> +where + T: ToOffset, +{ + let buffer = &inlay_snapshot.buffer; + let start = buffer.anchor_before(range.start.to_offset(buffer)); + let end = buffer.anchor_after(range.end.to_offset(buffer)); + let mut cursor = folds.filter::<_, usize>(move |summary| { + let start_cmp = start.cmp(&summary.max_end, buffer); + let end_cmp = end.cmp(&summary.min_start, buffer); + + if inclusive { + start_cmp <= Ordering::Equal && end_cmp >= Ordering::Equal + } else { + start_cmp == Ordering::Less && end_cmp == Ordering::Greater + } + }); + cursor.next(buffer); + cursor +} + +fn consolidate_inlay_edits(edits: &mut Vec) { + edits.sort_unstable_by(|a, b| { + a.old + .start + .cmp(&b.old.start) + .then_with(|| b.old.end.cmp(&a.old.end)) + }); + + let mut i = 1; + while i < edits.len() { + let edit = edits[i].clone(); + let prev_edit = &mut edits[i - 1]; + if prev_edit.old.end >= edit.old.start { + prev_edit.old.end = prev_edit.old.end.max(edit.old.end); + prev_edit.new.start = prev_edit.new.start.min(edit.new.start); + prev_edit.new.end = prev_edit.new.end.max(edit.new.end); + edits.remove(i); + continue; + } + i += 1; + } +} + +fn consolidate_fold_edits(edits: &mut Vec) { + edits.sort_unstable_by(|a, b| { + a.old + .start + .cmp(&b.old.start) + .then_with(|| b.old.end.cmp(&a.old.end)) + }); + + let mut i = 1; + while i < edits.len() { + let edit = edits[i].clone(); + let prev_edit = &mut edits[i - 1]; + if prev_edit.old.end >= edit.old.start { + prev_edit.old.end = prev_edit.old.end.max(edit.old.end); + prev_edit.new.start = prev_edit.new.start.min(edit.new.start); + prev_edit.new.end = prev_edit.new.end.max(edit.new.end); + edits.remove(i); + continue; + } + i += 1; + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +struct Transform { + summary: TransformSummary, + output_text: Option<&'static str>, +} + +impl Transform { + fn is_fold(&self) -> bool { + self.output_text.is_some() + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +struct TransformSummary { + output: TextSummary, + input: TextSummary, +} + +impl sum_tree::Item for Transform { + type Summary = TransformSummary; + + fn summary(&self) -> Self::Summary { + self.summary.clone() + } +} + +impl sum_tree::Summary for TransformSummary { + type Context = (); + + fn add_summary(&mut self, other: &Self, _: &()) { + self.input += &other.input; + self.output += &other.output; + } +} + +#[derive(Clone, Debug)] +struct Fold(Range); + +impl Default for Fold { + fn default() -> Self { + Self(Anchor::min()..Anchor::max()) + } +} + +impl sum_tree::Item for Fold { + type Summary = FoldSummary; + + fn summary(&self) -> Self::Summary { + FoldSummary { + start: self.0.start.clone(), + end: self.0.end.clone(), + min_start: self.0.start.clone(), + max_end: self.0.end.clone(), + count: 1, + } + } +} + +#[derive(Clone, Debug)] +struct FoldSummary { + start: Anchor, + end: Anchor, + min_start: Anchor, + max_end: Anchor, + count: usize, +} + +impl Default for FoldSummary { + fn default() -> Self { + Self { + start: Anchor::min(), + end: Anchor::max(), + min_start: Anchor::max(), + max_end: Anchor::min(), + count: 0, + } + } +} + +impl sum_tree::Summary for FoldSummary { + type Context = MultiBufferSnapshot; + + fn add_summary(&mut self, other: &Self, buffer: &Self::Context) { + if other.min_start.cmp(&self.min_start, buffer) == Ordering::Less { + self.min_start = other.min_start.clone(); + } + if other.max_end.cmp(&self.max_end, buffer) == Ordering::Greater { + self.max_end = other.max_end.clone(); + } + + #[cfg(debug_assertions)] + { + let start_comparison = self.start.cmp(&other.start, buffer); + assert!(start_comparison <= Ordering::Equal); + if start_comparison == Ordering::Equal { + assert!(self.end.cmp(&other.end, buffer) >= Ordering::Equal); + } + } + + self.start = other.start.clone(); + self.end = other.end.clone(); + self.count += other.count; + } +} + +impl<'a> sum_tree::Dimension<'a, FoldSummary> for Fold { + fn add_summary(&mut self, summary: &'a FoldSummary, _: &MultiBufferSnapshot) { + self.0.start = summary.start.clone(); + self.0.end = summary.end.clone(); + } +} + +impl<'a> sum_tree::SeekTarget<'a, FoldSummary, Fold> for Fold { + fn cmp(&self, other: &Self, buffer: &MultiBufferSnapshot) -> Ordering { + self.0.cmp(&other.0, buffer) + } +} + +impl<'a> sum_tree::Dimension<'a, FoldSummary> for usize { + fn add_summary(&mut self, summary: &'a FoldSummary, _: &MultiBufferSnapshot) { + *self += summary.count; + } +} + +#[derive(Clone)] +pub struct FoldBufferRows<'a> { + cursor: Cursor<'a, Transform, (FoldPoint, InlayPoint)>, + input_buffer_rows: InlayBufferRows<'a>, + fold_point: FoldPoint, +} + +impl<'a> Iterator for FoldBufferRows<'a> { + type Item = Option; + + fn next(&mut self) -> Option { + let mut traversed_fold = false; + while self.fold_point > self.cursor.end(&()).0 { + self.cursor.next(&()); + traversed_fold = true; + if self.cursor.item().is_none() { + break; + } + } + + if self.cursor.item().is_some() { + if traversed_fold { + self.input_buffer_rows.seek(self.cursor.start().1.row()); + self.input_buffer_rows.next(); + } + *self.fold_point.row_mut() += 1; + self.input_buffer_rows.next() + } else { + None + } + } +} + +pub struct FoldChunks<'a> { + transform_cursor: Cursor<'a, Transform, (FoldOffset, InlayOffset)>, + inlay_chunks: InlayChunks<'a>, + inlay_chunk: Option<(InlayOffset, Chunk<'a>)>, + inlay_offset: InlayOffset, + output_offset: usize, + max_output_offset: usize, + ellipses_color: Option, +} + +impl<'a> Iterator for FoldChunks<'a> { + type Item = Chunk<'a>; + + fn next(&mut self) -> Option { + if self.output_offset >= self.max_output_offset { + return None; + } + + let transform = self.transform_cursor.item()?; + + // If we're in a fold, then return the fold's display text and + // advance the transform and buffer cursors to the end of the fold. + if let Some(output_text) = transform.output_text { + self.inlay_chunk.take(); + self.inlay_offset += InlayOffset(transform.summary.input.len); + self.inlay_chunks.seek(self.inlay_offset); + + while self.inlay_offset >= self.transform_cursor.end(&()).1 + && self.transform_cursor.item().is_some() + { + self.transform_cursor.next(&()); + } + + self.output_offset += output_text.len(); + return Some(Chunk { + text: output_text, + highlight_style: self.ellipses_color.map(|color| HighlightStyle { + color: Some(color), + ..Default::default() + }), + ..Default::default() + }); + } + + // Retrieve a chunk from the current location in the buffer. + if self.inlay_chunk.is_none() { + let chunk_offset = self.inlay_chunks.offset(); + self.inlay_chunk = self.inlay_chunks.next().map(|chunk| (chunk_offset, chunk)); + } + + // Otherwise, take a chunk from the buffer's text. + if let Some((buffer_chunk_start, mut chunk)) = self.inlay_chunk { + let buffer_chunk_end = buffer_chunk_start + InlayOffset(chunk.text.len()); + let transform_end = self.transform_cursor.end(&()).1; + let chunk_end = buffer_chunk_end.min(transform_end); + + chunk.text = &chunk.text + [(self.inlay_offset - buffer_chunk_start).0..(chunk_end - buffer_chunk_start).0]; + + if chunk_end == transform_end { + self.transform_cursor.next(&()); + } else if chunk_end == buffer_chunk_end { + self.inlay_chunk.take(); + } + + self.inlay_offset = chunk_end; + self.output_offset += chunk.text.len(); + return Some(chunk); + } + + None + } +} + +#[derive(Copy, Clone, Eq, PartialEq)] +struct HighlightEndpoint { + offset: InlayOffset, + is_start: bool, + tag: Option, + style: HighlightStyle, +} + +impl PartialOrd for HighlightEndpoint { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for HighlightEndpoint { + fn cmp(&self, other: &Self) -> Ordering { + self.offset + .cmp(&other.offset) + .then_with(|| other.is_start.cmp(&self.is_start)) + } +} + +#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] +pub struct FoldOffset(pub usize); + +impl FoldOffset { + pub fn to_point(self, snapshot: &FoldSnapshot) -> FoldPoint { + let mut cursor = snapshot + .transforms + .cursor::<(FoldOffset, TransformSummary)>(); + cursor.seek(&self, Bias::Right, &()); + let overshoot = if cursor.item().map_or(true, |t| t.is_fold()) { + Point::new(0, (self.0 - cursor.start().0 .0) as u32) + } else { + let inlay_offset = cursor.start().1.input.len + self.0 - cursor.start().0 .0; + let inlay_point = snapshot.inlay_snapshot.to_point(InlayOffset(inlay_offset)); + inlay_point.0 - cursor.start().1.input.lines + }; + FoldPoint(cursor.start().1.output.lines + overshoot) + } + + #[cfg(test)] + pub fn to_inlay_offset(self, snapshot: &FoldSnapshot) -> InlayOffset { + let mut cursor = snapshot.transforms.cursor::<(FoldOffset, InlayOffset)>(); + cursor.seek(&self, Bias::Right, &()); + let overshoot = self.0 - cursor.start().0 .0; + InlayOffset(cursor.start().1 .0 + overshoot) + } +} + +impl Add for FoldOffset { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) + } +} + +impl AddAssign for FoldOffset { + fn add_assign(&mut self, rhs: Self) { + self.0 += rhs.0; + } +} + +impl Sub for FoldOffset { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + Self(self.0 - rhs.0) + } +} + +impl<'a> sum_tree::Dimension<'a, TransformSummary> for FoldOffset { + fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { + self.0 += &summary.output.len; + } +} + +impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayPoint { + fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { + self.0 += &summary.input.lines; + } +} + +impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayOffset { + fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { + self.0 += &summary.input.len; + } +} + +pub type FoldEdit = Edit; + +#[cfg(test)] +mod tests { + use super::*; + use crate::{display_map::inlay_map::InlayMap, MultiBuffer, ToPoint}; + use collections::HashSet; + use rand::prelude::*; + use settings::SettingsStore; + use std::{env, mem}; + use text::Patch; + use util::test::sample_text; + use util::RandomCharIter; + use Bias::{Left, Right}; + + #[gpui::test] + fn test_basic_folds(cx: &mut gpui::AppContext) { + init_test(cx); + let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); + let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); + let buffer_snapshot = buffer.read(cx).snapshot(cx); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let mut map = FoldMap::new(inlay_snapshot.clone()).0; + + let (mut writer, _, _) = map.write(inlay_snapshot, vec![]); + let (snapshot2, edits) = writer.fold(vec![ + Point::new(0, 2)..Point::new(2, 2), + Point::new(2, 4)..Point::new(4, 1), + ]); + assert_eq!(snapshot2.text(), "aa⋯cc⋯eeeee"); + assert_eq!( + edits, + &[ + FoldEdit { + old: FoldOffset(2)..FoldOffset(16), + new: FoldOffset(2)..FoldOffset(5), + }, + FoldEdit { + old: FoldOffset(18)..FoldOffset(29), + new: FoldOffset(7)..FoldOffset(10) + }, + ] + ); + + let buffer_snapshot = buffer.update(cx, |buffer, cx| { + buffer.edit( + vec![ + (Point::new(0, 0)..Point::new(0, 1), "123"), + (Point::new(2, 3)..Point::new(2, 3), "123"), + ], + None, + cx, + ); + buffer.snapshot(cx) + }); + + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot, subscription.consume().into_inner()); + let (snapshot3, edits) = map.read(inlay_snapshot, inlay_edits); + assert_eq!(snapshot3.text(), "123a⋯c123c⋯eeeee"); + assert_eq!( + edits, + &[ + FoldEdit { + old: FoldOffset(0)..FoldOffset(1), + new: FoldOffset(0)..FoldOffset(3), + }, + FoldEdit { + old: FoldOffset(6)..FoldOffset(6), + new: FoldOffset(8)..FoldOffset(11), + }, + ] + ); + + let buffer_snapshot = buffer.update(cx, |buffer, cx| { + buffer.edit([(Point::new(2, 6)..Point::new(4, 3), "456")], None, cx); + buffer.snapshot(cx) + }); + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot, subscription.consume().into_inner()); + let (snapshot4, _) = map.read(inlay_snapshot.clone(), inlay_edits); + assert_eq!(snapshot4.text(), "123a⋯c123456eee"); + + let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); + writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), false); + let (snapshot5, _) = map.read(inlay_snapshot.clone(), vec![]); + assert_eq!(snapshot5.text(), "123a⋯c123456eee"); + + let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); + writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), true); + let (snapshot6, _) = map.read(inlay_snapshot, vec![]); + assert_eq!(snapshot6.text(), "123aaaaa\nbbbbbb\nccc123456eee"); + } + + #[gpui::test] + fn test_adjacent_folds(cx: &mut gpui::AppContext) { + init_test(cx); + let buffer = MultiBuffer::build_simple("abcdefghijkl", cx); + let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); + let buffer_snapshot = buffer.read(cx).snapshot(cx); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + + { + let mut map = FoldMap::new(inlay_snapshot.clone()).0; + + let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); + writer.fold(vec![5..8]); + let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); + assert_eq!(snapshot.text(), "abcde⋯ijkl"); + + // Create an fold adjacent to the start of the first fold. + let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); + writer.fold(vec![0..1, 2..5]); + let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); + assert_eq!(snapshot.text(), "⋯b⋯ijkl"); + + // Create an fold adjacent to the end of the first fold. + let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); + writer.fold(vec![11..11, 8..10]); + let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); + assert_eq!(snapshot.text(), "⋯b⋯kl"); + } + + { + let mut map = FoldMap::new(inlay_snapshot.clone()).0; + + // Create two adjacent folds. + let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); + writer.fold(vec![0..2, 2..5]); + let (snapshot, _) = map.read(inlay_snapshot, vec![]); + assert_eq!(snapshot.text(), "⋯fghijkl"); + + // Edit within one of the folds. + let buffer_snapshot = buffer.update(cx, |buffer, cx| { + buffer.edit([(0..1, "12345")], None, cx); + buffer.snapshot(cx) + }); + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot, subscription.consume().into_inner()); + let (snapshot, _) = map.read(inlay_snapshot, inlay_edits); + assert_eq!(snapshot.text(), "12345⋯fghijkl"); + } + } + + #[gpui::test] + fn test_overlapping_folds(cx: &mut gpui::AppContext) { + let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); + let buffer_snapshot = buffer.read(cx).snapshot(cx); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); + let mut map = FoldMap::new(inlay_snapshot.clone()).0; + let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); + writer.fold(vec![ + Point::new(0, 2)..Point::new(2, 2), + Point::new(0, 4)..Point::new(1, 0), + Point::new(1, 2)..Point::new(3, 2), + Point::new(3, 1)..Point::new(4, 1), + ]); + let (snapshot, _) = map.read(inlay_snapshot, vec![]); + assert_eq!(snapshot.text(), "aa⋯eeeee"); + } + + #[gpui::test] + fn test_merging_folds_via_edit(cx: &mut gpui::AppContext) { + init_test(cx); + let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); + let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); + let buffer_snapshot = buffer.read(cx).snapshot(cx); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let mut map = FoldMap::new(inlay_snapshot.clone()).0; + + let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); + writer.fold(vec![ + Point::new(0, 2)..Point::new(2, 2), + Point::new(3, 1)..Point::new(4, 1), + ]); + let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); + assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee"); + + let buffer_snapshot = buffer.update(cx, |buffer, cx| { + buffer.edit([(Point::new(2, 2)..Point::new(3, 1), "")], None, cx); + buffer.snapshot(cx) + }); + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot, subscription.consume().into_inner()); + let (snapshot, _) = map.read(inlay_snapshot, inlay_edits); + assert_eq!(snapshot.text(), "aa⋯eeeee"); + } + + #[gpui::test] + fn test_folds_in_range(cx: &mut gpui::AppContext) { + let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); + let buffer_snapshot = buffer.read(cx).snapshot(cx); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let mut map = FoldMap::new(inlay_snapshot.clone()).0; + + let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); + writer.fold(vec![ + Point::new(0, 2)..Point::new(2, 2), + Point::new(0, 4)..Point::new(1, 0), + Point::new(1, 2)..Point::new(3, 2), + Point::new(3, 1)..Point::new(4, 1), + ]); + let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); + let fold_ranges = snapshot + .folds_in_range(Point::new(1, 0)..Point::new(1, 3)) + .map(|fold| fold.start.to_point(&buffer_snapshot)..fold.end.to_point(&buffer_snapshot)) + .collect::>(); + assert_eq!( + fold_ranges, + vec![ + Point::new(0, 2)..Point::new(2, 2), + Point::new(1, 2)..Point::new(3, 2) + ] + ); + } + + #[gpui::test(iterations = 100)] + fn test_random_folds(cx: &mut gpui::AppContext, mut rng: StdRng) { + init_test(cx); + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + + let len = rng.gen_range(0..10); + let text = RandomCharIter::new(&mut rng).take(len).collect::(); + let buffer = if rng.gen() { + MultiBuffer::build_simple(&text, cx) + } else { + MultiBuffer::build_random(&mut rng, cx) + }; + let mut buffer_snapshot = buffer.read(cx).snapshot(cx); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let mut map = FoldMap::new(inlay_snapshot.clone()).0; + + let (mut initial_snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); + let mut snapshot_edits = Vec::new(); + + let mut next_inlay_id = 0; + for _ in 0..operations { + log::info!("text: {:?}", buffer_snapshot.text()); + let mut buffer_edits = Vec::new(); + let mut inlay_edits = Vec::new(); + match rng.gen_range(0..=100) { + 0..=39 => { + snapshot_edits.extend(map.randomly_mutate(&mut rng)); + } + 40..=59 => { + let (_, edits) = inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng); + inlay_edits = edits; + } + _ => buffer.update(cx, |buffer, cx| { + let subscription = buffer.subscribe(); + let edit_count = rng.gen_range(1..=5); + buffer.randomly_mutate(&mut rng, edit_count, cx); + buffer_snapshot = buffer.snapshot(cx); + let edits = subscription.consume().into_inner(); + log::info!("editing {:?}", edits); + buffer_edits.extend(edits); + }), + }; + + let (inlay_snapshot, new_inlay_edits) = + inlay_map.sync(buffer_snapshot.clone(), buffer_edits); + log::info!("inlay text {:?}", inlay_snapshot.text()); + + let inlay_edits = Patch::new(inlay_edits) + .compose(new_inlay_edits) + .into_inner(); + let (snapshot, edits) = map.read(inlay_snapshot.clone(), inlay_edits); + snapshot_edits.push((snapshot.clone(), edits)); + + let mut expected_text: String = inlay_snapshot.text().to_string(); + for fold_range in map.merged_fold_ranges().into_iter().rev() { + let fold_inlay_start = inlay_snapshot.to_inlay_offset(fold_range.start); + let fold_inlay_end = inlay_snapshot.to_inlay_offset(fold_range.end); + expected_text.replace_range(fold_inlay_start.0..fold_inlay_end.0, "⋯"); + } + + assert_eq!(snapshot.text(), expected_text); + log::info!( + "fold text {:?} ({} lines)", + expected_text, + expected_text.matches('\n').count() + 1 + ); + + let mut prev_row = 0; + let mut expected_buffer_rows = Vec::new(); + for fold_range in map.merged_fold_ranges().into_iter() { + let fold_start = inlay_snapshot + .to_point(inlay_snapshot.to_inlay_offset(fold_range.start)) + .row(); + let fold_end = inlay_snapshot + .to_point(inlay_snapshot.to_inlay_offset(fold_range.end)) + .row(); + expected_buffer_rows.extend( + inlay_snapshot + .buffer_rows(prev_row) + .take((1 + fold_start - prev_row) as usize), + ); + prev_row = 1 + fold_end; + } + expected_buffer_rows.extend(inlay_snapshot.buffer_rows(prev_row)); + + assert_eq!( + expected_buffer_rows.len(), + expected_text.matches('\n').count() + 1, + "wrong expected buffer rows {:?}. text: {:?}", + expected_buffer_rows, + expected_text + ); + + for (output_row, line) in expected_text.lines().enumerate() { + let line_len = snapshot.line_len(output_row as u32); + assert_eq!(line_len, line.len() as u32); + } + + let longest_row = snapshot.longest_row(); + let longest_char_column = expected_text + .split('\n') + .nth(longest_row as usize) + .unwrap() + .chars() + .count(); + let mut fold_point = FoldPoint::new(0, 0); + let mut fold_offset = FoldOffset(0); + let mut char_column = 0; + for c in expected_text.chars() { + let inlay_point = fold_point.to_inlay_point(&snapshot); + let inlay_offset = fold_offset.to_inlay_offset(&snapshot); + assert_eq!( + snapshot.to_fold_point(inlay_point, Right), + fold_point, + "{:?} -> fold point", + inlay_point, + ); + assert_eq!( + inlay_snapshot.to_offset(inlay_point), + inlay_offset, + "inlay_snapshot.to_offset({:?})", + inlay_point, + ); + assert_eq!( + fold_point.to_offset(&snapshot), + fold_offset, + "fold_point.to_offset({:?})", + fold_point, + ); + + if c == '\n' { + *fold_point.row_mut() += 1; + *fold_point.column_mut() = 0; + char_column = 0; + } else { + *fold_point.column_mut() += c.len_utf8() as u32; + char_column += 1; + } + fold_offset.0 += c.len_utf8(); + if char_column > longest_char_column { + panic!( + "invalid longest row {:?} (chars {}), found row {:?} (chars: {})", + longest_row, + longest_char_column, + fold_point.row(), + char_column + ); + } + } + + for _ in 0..5 { + let mut start = snapshot + .clip_offset(FoldOffset(rng.gen_range(0..=snapshot.len().0)), Bias::Left); + let mut end = snapshot + .clip_offset(FoldOffset(rng.gen_range(0..=snapshot.len().0)), Bias::Right); + if start > end { + mem::swap(&mut start, &mut end); + } + + let text = &expected_text[start.0..end.0]; + assert_eq!( + snapshot + .chunks(start..end, false, Highlights::default()) + .map(|c| c.text) + .collect::(), + text, + ); + } + + let mut fold_row = 0; + while fold_row < expected_buffer_rows.len() as u32 { + assert_eq!( + snapshot.buffer_rows(fold_row).collect::>(), + expected_buffer_rows[(fold_row as usize)..], + "wrong buffer rows starting at fold row {}", + fold_row, + ); + fold_row += 1; + } + + let folded_buffer_rows = map + .merged_fold_ranges() + .iter() + .flat_map(|range| { + let start_row = range.start.to_point(&buffer_snapshot).row; + let end = range.end.to_point(&buffer_snapshot); + if end.column == 0 { + start_row..end.row + } else { + start_row..end.row + 1 + } + }) + .collect::>(); + for row in 0..=buffer_snapshot.max_point().row { + assert_eq!( + snapshot.is_line_folded(row), + folded_buffer_rows.contains(&row), + "expected buffer row {}{} to be folded", + row, + if folded_buffer_rows.contains(&row) { + "" + } else { + " not" + } + ); + } + + for _ in 0..5 { + let end = + buffer_snapshot.clip_offset(rng.gen_range(0..=buffer_snapshot.len()), Right); + let start = buffer_snapshot.clip_offset(rng.gen_range(0..=end), Left); + let expected_folds = map + .snapshot + .folds + .items(&buffer_snapshot) + .into_iter() + .filter(|fold| { + let start = buffer_snapshot.anchor_before(start); + let end = buffer_snapshot.anchor_after(end); + start.cmp(&fold.0.end, &buffer_snapshot) == Ordering::Less + && end.cmp(&fold.0.start, &buffer_snapshot) == Ordering::Greater + }) + .map(|fold| fold.0) + .collect::>(); + + assert_eq!( + snapshot + .folds_in_range(start..end) + .cloned() + .collect::>(), + expected_folds + ); + } + + let text = snapshot.text(); + for _ in 0..5 { + let start_row = rng.gen_range(0..=snapshot.max_point().row()); + let start_column = rng.gen_range(0..=snapshot.line_len(start_row)); + let end_row = rng.gen_range(0..=snapshot.max_point().row()); + let end_column = rng.gen_range(0..=snapshot.line_len(end_row)); + let mut start = + snapshot.clip_point(FoldPoint::new(start_row, start_column), Bias::Left); + let mut end = snapshot.clip_point(FoldPoint::new(end_row, end_column), Bias::Right); + if start > end { + mem::swap(&mut start, &mut end); + } + + let lines = start..end; + let bytes = start.to_offset(&snapshot)..end.to_offset(&snapshot); + assert_eq!( + snapshot.text_summary_for_range(lines), + TextSummary::from(&text[bytes.start.0..bytes.end.0]) + ) + } + + let mut text = initial_snapshot.text(); + for (snapshot, edits) in snapshot_edits.drain(..) { + let new_text = snapshot.text(); + for edit in edits { + let old_bytes = edit.new.start.0..edit.new.start.0 + edit.old_len().0; + let new_bytes = edit.new.start.0..edit.new.end.0; + text.replace_range(old_bytes, &new_text[new_bytes]); + } + + assert_eq!(text, new_text); + initial_snapshot = snapshot; + } + } + } + + #[gpui::test] + fn test_buffer_rows(cx: &mut gpui::AppContext) { + let text = sample_text(6, 6, 'a') + "\n"; + let buffer = MultiBuffer::build_simple(&text, cx); + + let buffer_snapshot = buffer.read(cx).snapshot(cx); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); + let mut map = FoldMap::new(inlay_snapshot.clone()).0; + + let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); + writer.fold(vec![ + Point::new(0, 2)..Point::new(2, 2), + Point::new(3, 1)..Point::new(4, 1), + ]); + + let (snapshot, _) = map.read(inlay_snapshot, vec![]); + assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee\nffffff\n"); + assert_eq!( + snapshot.buffer_rows(0).collect::>(), + [Some(0), Some(3), Some(5), Some(6)] + ); + assert_eq!(snapshot.buffer_rows(3).collect::>(), [Some(6)]); + } + + fn init_test(cx: &mut gpui::AppContext) { + cx.set_global(SettingsStore::test(cx)); + } + + impl FoldMap { + fn merged_fold_ranges(&self) -> Vec> { + let inlay_snapshot = self.snapshot.inlay_snapshot.clone(); + let buffer = &inlay_snapshot.buffer; + let mut folds = self.snapshot.folds.items(buffer); + // Ensure sorting doesn't change how folds get merged and displayed. + folds.sort_by(|a, b| a.0.cmp(&b.0, buffer)); + let mut fold_ranges = folds + .iter() + .map(|fold| fold.0.start.to_offset(buffer)..fold.0.end.to_offset(buffer)) + .peekable(); + + let mut merged_ranges = Vec::new(); + while let Some(mut fold_range) = fold_ranges.next() { + while let Some(next_range) = fold_ranges.peek() { + if fold_range.end >= next_range.start { + if next_range.end > fold_range.end { + fold_range.end = next_range.end; + } + fold_ranges.next(); + } else { + break; + } + } + if fold_range.end > fold_range.start { + merged_ranges.push(fold_range); + } + } + merged_ranges + } + + pub fn randomly_mutate( + &mut self, + rng: &mut impl Rng, + ) -> Vec<(FoldSnapshot, Vec)> { + let mut snapshot_edits = Vec::new(); + match rng.gen_range(0..=100) { + 0..=39 if !self.snapshot.folds.is_empty() => { + let inlay_snapshot = self.snapshot.inlay_snapshot.clone(); + let buffer = &inlay_snapshot.buffer; + let mut to_unfold = Vec::new(); + for _ in 0..rng.gen_range(1..=3) { + let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right); + let start = buffer.clip_offset(rng.gen_range(0..=end), Left); + to_unfold.push(start..end); + } + log::info!("unfolding {:?}", to_unfold); + let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]); + snapshot_edits.push((snapshot, edits)); + let (snapshot, edits) = writer.fold(to_unfold); + snapshot_edits.push((snapshot, edits)); + } + _ => { + let inlay_snapshot = self.snapshot.inlay_snapshot.clone(); + let buffer = &inlay_snapshot.buffer; + let mut to_fold = Vec::new(); + for _ in 0..rng.gen_range(1..=2) { + let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right); + let start = buffer.clip_offset(rng.gen_range(0..=end), Left); + to_fold.push(start..end); + } + log::info!("folding {:?}", to_fold); + let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]); + snapshot_edits.push((snapshot, edits)); + let (snapshot, edits) = writer.fold(to_fold); + snapshot_edits.push((snapshot, edits)); + } + } + snapshot_edits + } + } +} diff --git a/crates/editor2/src/display_map/inlay_map.rs b/crates/editor2/src/display_map/inlay_map.rs new file mode 100644 index 0000000000..c0c352453b --- /dev/null +++ b/crates/editor2/src/display_map/inlay_map.rs @@ -0,0 +1,1895 @@ +use crate::{Anchor, InlayId, MultiBufferSnapshot, ToOffset}; +use collections::{BTreeMap, BTreeSet}; +use gpui::fonts::HighlightStyle; +use language::{Chunk, Edit, Point, TextSummary}; +use multi_buffer::{MultiBufferChunks, MultiBufferRows}; +use std::{ + any::TypeId, + cmp, + iter::Peekable, + ops::{Add, AddAssign, Range, Sub, SubAssign}, + sync::Arc, + vec, +}; +use sum_tree::{Bias, Cursor, SumTree, TreeMap}; +use text::{Patch, Rope}; + +use super::Highlights; + +pub struct InlayMap { + snapshot: InlaySnapshot, + inlays: Vec, +} + +#[derive(Clone)] +pub struct InlaySnapshot { + pub buffer: MultiBufferSnapshot, + transforms: SumTree, + pub version: usize, +} + +#[derive(Clone, Debug)] +enum Transform { + Isomorphic(TextSummary), + Inlay(Inlay), +} + +#[derive(Debug, Clone)] +pub struct Inlay { + pub id: InlayId, + pub position: Anchor, + pub text: text::Rope, +} + +impl Inlay { + pub fn hint(id: usize, position: Anchor, hint: &project::InlayHint) -> Self { + let mut text = hint.text(); + if hint.padding_right && !text.ends_with(' ') { + text.push(' '); + } + if hint.padding_left && !text.starts_with(' ') { + text.insert(0, ' '); + } + Self { + id: InlayId::Hint(id), + position, + text: text.into(), + } + } + + pub fn suggestion>(id: usize, position: Anchor, text: T) -> Self { + Self { + id: InlayId::Suggestion(id), + position, + text: text.into(), + } + } +} + +impl sum_tree::Item for Transform { + type Summary = TransformSummary; + + fn summary(&self) -> Self::Summary { + match self { + Transform::Isomorphic(summary) => TransformSummary { + input: summary.clone(), + output: summary.clone(), + }, + Transform::Inlay(inlay) => TransformSummary { + input: TextSummary::default(), + output: inlay.text.summary(), + }, + } + } +} + +#[derive(Clone, Debug, Default)] +struct TransformSummary { + input: TextSummary, + output: TextSummary, +} + +impl sum_tree::Summary for TransformSummary { + type Context = (); + + fn add_summary(&mut self, other: &Self, _: &()) { + self.input += &other.input; + self.output += &other.output; + } +} + +pub type InlayEdit = Edit; + +#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] +pub struct InlayOffset(pub usize); + +impl Add for InlayOffset { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) + } +} + +impl Sub for InlayOffset { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + Self(self.0 - rhs.0) + } +} + +impl AddAssign for InlayOffset { + fn add_assign(&mut self, rhs: Self) { + self.0 += rhs.0; + } +} + +impl SubAssign for InlayOffset { + fn sub_assign(&mut self, rhs: Self) { + self.0 -= rhs.0; + } +} + +impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayOffset { + fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { + self.0 += &summary.output.len; + } +} + +#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] +pub struct InlayPoint(pub Point); + +impl Add for InlayPoint { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) + } +} + +impl Sub for InlayPoint { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + Self(self.0 - rhs.0) + } +} + +impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayPoint { + fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { + self.0 += &summary.output.lines; + } +} + +impl<'a> sum_tree::Dimension<'a, TransformSummary> for usize { + fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { + *self += &summary.input.len; + } +} + +impl<'a> sum_tree::Dimension<'a, TransformSummary> for Point { + fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { + *self += &summary.input.lines; + } +} + +#[derive(Clone)] +pub struct InlayBufferRows<'a> { + transforms: Cursor<'a, Transform, (InlayPoint, Point)>, + buffer_rows: MultiBufferRows<'a>, + inlay_row: u32, + max_buffer_row: u32, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +struct HighlightEndpoint { + offset: InlayOffset, + is_start: bool, + tag: Option, + style: HighlightStyle, +} + +impl PartialOrd for HighlightEndpoint { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for HighlightEndpoint { + fn cmp(&self, other: &Self) -> cmp::Ordering { + self.offset + .cmp(&other.offset) + .then_with(|| other.is_start.cmp(&self.is_start)) + } +} + +pub struct InlayChunks<'a> { + transforms: Cursor<'a, Transform, (InlayOffset, usize)>, + buffer_chunks: MultiBufferChunks<'a>, + buffer_chunk: Option>, + inlay_chunks: Option>, + inlay_chunk: Option<&'a str>, + output_offset: InlayOffset, + max_output_offset: InlayOffset, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, + highlight_endpoints: Peekable>, + active_highlights: BTreeMap, HighlightStyle>, + highlights: Highlights<'a>, + snapshot: &'a InlaySnapshot, +} + +impl<'a> InlayChunks<'a> { + pub fn seek(&mut self, offset: InlayOffset) { + self.transforms.seek(&offset, Bias::Right, &()); + + let buffer_offset = self.snapshot.to_buffer_offset(offset); + self.buffer_chunks.seek(buffer_offset); + self.inlay_chunks = None; + self.buffer_chunk = None; + self.output_offset = offset; + } + + pub fn offset(&self) -> InlayOffset { + self.output_offset + } +} + +impl<'a> Iterator for InlayChunks<'a> { + type Item = Chunk<'a>; + + fn next(&mut self) -> Option { + if self.output_offset == self.max_output_offset { + return None; + } + + let mut next_highlight_endpoint = InlayOffset(usize::MAX); + while let Some(endpoint) = self.highlight_endpoints.peek().copied() { + if endpoint.offset <= self.output_offset { + if endpoint.is_start { + self.active_highlights.insert(endpoint.tag, endpoint.style); + } else { + self.active_highlights.remove(&endpoint.tag); + } + self.highlight_endpoints.next(); + } else { + next_highlight_endpoint = endpoint.offset; + break; + } + } + + let chunk = match self.transforms.item()? { + Transform::Isomorphic(_) => { + let chunk = self + .buffer_chunk + .get_or_insert_with(|| self.buffer_chunks.next().unwrap()); + if chunk.text.is_empty() { + *chunk = self.buffer_chunks.next().unwrap(); + } + + let (prefix, suffix) = chunk.text.split_at( + chunk + .text + .len() + .min(self.transforms.end(&()).0 .0 - self.output_offset.0) + .min(next_highlight_endpoint.0 - self.output_offset.0), + ); + + chunk.text = suffix; + self.output_offset.0 += prefix.len(); + let mut prefix = Chunk { + text: prefix, + ..chunk.clone() + }; + if !self.active_highlights.is_empty() { + let mut highlight_style = HighlightStyle::default(); + for active_highlight in self.active_highlights.values() { + highlight_style.highlight(*active_highlight); + } + prefix.highlight_style = Some(highlight_style); + } + prefix + } + Transform::Inlay(inlay) => { + let mut inlay_style_and_highlight = None; + if let Some(inlay_highlights) = self.highlights.inlay_highlights { + for (_, inlay_id_to_data) in inlay_highlights.iter() { + let style_and_highlight = inlay_id_to_data.get(&inlay.id); + if style_and_highlight.is_some() { + inlay_style_and_highlight = style_and_highlight; + break; + } + } + } + + let mut highlight_style = match inlay.id { + InlayId::Suggestion(_) => self.suggestion_highlight_style, + InlayId::Hint(_) => self.inlay_highlight_style, + }; + let next_inlay_highlight_endpoint; + let offset_in_inlay = self.output_offset - self.transforms.start().0; + if let Some((style, highlight)) = inlay_style_and_highlight { + let range = &highlight.range; + if offset_in_inlay.0 < range.start { + next_inlay_highlight_endpoint = range.start - offset_in_inlay.0; + } else if offset_in_inlay.0 >= range.end { + next_inlay_highlight_endpoint = usize::MAX; + } else { + next_inlay_highlight_endpoint = range.end - offset_in_inlay.0; + highlight_style + .get_or_insert_with(|| Default::default()) + .highlight(style.clone()); + } + } else { + next_inlay_highlight_endpoint = usize::MAX; + } + + let inlay_chunks = self.inlay_chunks.get_or_insert_with(|| { + let start = offset_in_inlay; + let end = cmp::min(self.max_output_offset, self.transforms.end(&()).0) + - self.transforms.start().0; + inlay.text.chunks_in_range(start.0..end.0) + }); + let inlay_chunk = self + .inlay_chunk + .get_or_insert_with(|| inlay_chunks.next().unwrap()); + let (chunk, remainder) = + inlay_chunk.split_at(inlay_chunk.len().min(next_inlay_highlight_endpoint)); + *inlay_chunk = remainder; + if inlay_chunk.is_empty() { + self.inlay_chunk = None; + } + + self.output_offset.0 += chunk.len(); + + if !self.active_highlights.is_empty() { + for active_highlight in self.active_highlights.values() { + highlight_style + .get_or_insert(Default::default()) + .highlight(*active_highlight); + } + } + Chunk { + text: chunk, + highlight_style, + ..Default::default() + } + } + }; + + if self.output_offset == self.transforms.end(&()).0 { + self.inlay_chunks = None; + self.transforms.next(&()); + } + + Some(chunk) + } +} + +impl<'a> InlayBufferRows<'a> { + pub fn seek(&mut self, row: u32) { + let inlay_point = InlayPoint::new(row, 0); + self.transforms.seek(&inlay_point, Bias::Left, &()); + + let mut buffer_point = self.transforms.start().1; + let buffer_row = if row == 0 { + 0 + } else { + match self.transforms.item() { + Some(Transform::Isomorphic(_)) => { + buffer_point += inlay_point.0 - self.transforms.start().0 .0; + buffer_point.row + } + _ => cmp::min(buffer_point.row + 1, self.max_buffer_row), + } + }; + self.inlay_row = inlay_point.row(); + self.buffer_rows.seek(buffer_row); + } +} + +impl<'a> Iterator for InlayBufferRows<'a> { + type Item = Option; + + fn next(&mut self) -> Option { + let buffer_row = if self.inlay_row == 0 { + self.buffer_rows.next().unwrap() + } else { + match self.transforms.item()? { + Transform::Inlay(_) => None, + Transform::Isomorphic(_) => self.buffer_rows.next().unwrap(), + } + }; + + self.inlay_row += 1; + self.transforms + .seek_forward(&InlayPoint::new(self.inlay_row, 0), Bias::Left, &()); + + Some(buffer_row) + } +} + +impl InlayPoint { + pub fn new(row: u32, column: u32) -> Self { + Self(Point::new(row, column)) + } + + pub fn row(self) -> u32 { + self.0.row + } +} + +impl InlayMap { + pub fn new(buffer: MultiBufferSnapshot) -> (Self, InlaySnapshot) { + let version = 0; + let snapshot = InlaySnapshot { + buffer: buffer.clone(), + transforms: SumTree::from_iter(Some(Transform::Isomorphic(buffer.text_summary())), &()), + version, + }; + + ( + Self { + snapshot: snapshot.clone(), + inlays: Vec::new(), + }, + snapshot, + ) + } + + pub fn sync( + &mut self, + buffer_snapshot: MultiBufferSnapshot, + mut buffer_edits: Vec>, + ) -> (InlaySnapshot, Vec) { + let snapshot = &mut self.snapshot; + + if buffer_edits.is_empty() { + if snapshot.buffer.trailing_excerpt_update_count() + != buffer_snapshot.trailing_excerpt_update_count() + { + buffer_edits.push(Edit { + old: snapshot.buffer.len()..snapshot.buffer.len(), + new: buffer_snapshot.len()..buffer_snapshot.len(), + }); + } + } + + if buffer_edits.is_empty() { + if snapshot.buffer.edit_count() != buffer_snapshot.edit_count() + || snapshot.buffer.parse_count() != buffer_snapshot.parse_count() + || snapshot.buffer.diagnostics_update_count() + != buffer_snapshot.diagnostics_update_count() + || snapshot.buffer.git_diff_update_count() + != buffer_snapshot.git_diff_update_count() + || snapshot.buffer.trailing_excerpt_update_count() + != buffer_snapshot.trailing_excerpt_update_count() + { + snapshot.version += 1; + } + + snapshot.buffer = buffer_snapshot; + (snapshot.clone(), Vec::new()) + } else { + let mut inlay_edits = Patch::default(); + let mut new_transforms = SumTree::new(); + let mut cursor = snapshot.transforms.cursor::<(usize, InlayOffset)>(); + let mut buffer_edits_iter = buffer_edits.iter().peekable(); + while let Some(buffer_edit) = buffer_edits_iter.next() { + new_transforms.append(cursor.slice(&buffer_edit.old.start, Bias::Left, &()), &()); + if let Some(Transform::Isomorphic(transform)) = cursor.item() { + if cursor.end(&()).0 == buffer_edit.old.start { + push_isomorphic(&mut new_transforms, transform.clone()); + cursor.next(&()); + } + } + + // Remove all the inlays and transforms contained by the edit. + let old_start = + cursor.start().1 + InlayOffset(buffer_edit.old.start - cursor.start().0); + cursor.seek(&buffer_edit.old.end, Bias::Right, &()); + let old_end = + cursor.start().1 + InlayOffset(buffer_edit.old.end - cursor.start().0); + + // Push the unchanged prefix. + let prefix_start = new_transforms.summary().input.len; + let prefix_end = buffer_edit.new.start; + push_isomorphic( + &mut new_transforms, + buffer_snapshot.text_summary_for_range(prefix_start..prefix_end), + ); + let new_start = InlayOffset(new_transforms.summary().output.len); + + let start_ix = match self.inlays.binary_search_by(|probe| { + probe + .position + .to_offset(&buffer_snapshot) + .cmp(&buffer_edit.new.start) + .then(std::cmp::Ordering::Greater) + }) { + Ok(ix) | Err(ix) => ix, + }; + + for inlay in &self.inlays[start_ix..] { + let buffer_offset = inlay.position.to_offset(&buffer_snapshot); + if buffer_offset > buffer_edit.new.end { + break; + } + + let prefix_start = new_transforms.summary().input.len; + let prefix_end = buffer_offset; + push_isomorphic( + &mut new_transforms, + buffer_snapshot.text_summary_for_range(prefix_start..prefix_end), + ); + + if inlay.position.is_valid(&buffer_snapshot) { + new_transforms.push(Transform::Inlay(inlay.clone()), &()); + } + } + + // Apply the rest of the edit. + let transform_start = new_transforms.summary().input.len; + push_isomorphic( + &mut new_transforms, + buffer_snapshot.text_summary_for_range(transform_start..buffer_edit.new.end), + ); + let new_end = InlayOffset(new_transforms.summary().output.len); + inlay_edits.push(Edit { + old: old_start..old_end, + new: new_start..new_end, + }); + + // If the next edit doesn't intersect the current isomorphic transform, then + // we can push its remainder. + if buffer_edits_iter + .peek() + .map_or(true, |edit| edit.old.start >= cursor.end(&()).0) + { + let transform_start = new_transforms.summary().input.len; + let transform_end = + buffer_edit.new.end + (cursor.end(&()).0 - buffer_edit.old.end); + push_isomorphic( + &mut new_transforms, + buffer_snapshot.text_summary_for_range(transform_start..transform_end), + ); + cursor.next(&()); + } + } + + new_transforms.append(cursor.suffix(&()), &()); + if new_transforms.is_empty() { + new_transforms.push(Transform::Isomorphic(Default::default()), &()); + } + + drop(cursor); + snapshot.transforms = new_transforms; + snapshot.version += 1; + snapshot.buffer = buffer_snapshot; + snapshot.check_invariants(); + + (snapshot.clone(), inlay_edits.into_inner()) + } + } + + pub fn splice( + &mut self, + to_remove: Vec, + to_insert: Vec, + ) -> (InlaySnapshot, Vec) { + let snapshot = &mut self.snapshot; + let mut edits = BTreeSet::new(); + + self.inlays.retain(|inlay| { + let retain = !to_remove.contains(&inlay.id); + if !retain { + let offset = inlay.position.to_offset(&snapshot.buffer); + edits.insert(offset); + } + retain + }); + + for inlay_to_insert in to_insert { + // Avoid inserting empty inlays. + if inlay_to_insert.text.is_empty() { + continue; + } + + let offset = inlay_to_insert.position.to_offset(&snapshot.buffer); + match self.inlays.binary_search_by(|probe| { + probe + .position + .cmp(&inlay_to_insert.position, &snapshot.buffer) + }) { + Ok(ix) | Err(ix) => { + self.inlays.insert(ix, inlay_to_insert); + } + } + + edits.insert(offset); + } + + let buffer_edits = edits + .into_iter() + .map(|offset| Edit { + old: offset..offset, + new: offset..offset, + }) + .collect(); + let buffer_snapshot = snapshot.buffer.clone(); + let (snapshot, edits) = self.sync(buffer_snapshot, buffer_edits); + (snapshot, edits) + } + + pub fn current_inlays(&self) -> impl Iterator { + self.inlays.iter() + } + + #[cfg(test)] + pub(crate) fn randomly_mutate( + &mut self, + next_inlay_id: &mut usize, + rng: &mut rand::rngs::StdRng, + ) -> (InlaySnapshot, Vec) { + use rand::prelude::*; + use util::post_inc; + + let mut to_remove = Vec::new(); + let mut to_insert = Vec::new(); + let snapshot = &mut self.snapshot; + for i in 0..rng.gen_range(1..=5) { + if self.inlays.is_empty() || rng.gen() { + let position = snapshot.buffer.random_byte_range(0, rng).start; + let bias = if rng.gen() { Bias::Left } else { Bias::Right }; + let len = if rng.gen_bool(0.01) { + 0 + } else { + rng.gen_range(1..=5) + }; + let text = util::RandomCharIter::new(&mut *rng) + .filter(|ch| *ch != '\r') + .take(len) + .collect::(); + + let inlay_id = if i % 2 == 0 { + InlayId::Hint(post_inc(next_inlay_id)) + } else { + InlayId::Suggestion(post_inc(next_inlay_id)) + }; + log::info!( + "creating inlay {:?} at buffer offset {} with bias {:?} and text {:?}", + inlay_id, + position, + bias, + text + ); + + to_insert.push(Inlay { + id: inlay_id, + position: snapshot.buffer.anchor_at(position, bias), + text: text.into(), + }); + } else { + to_remove.push( + self.inlays + .iter() + .choose(rng) + .map(|inlay| inlay.id) + .unwrap(), + ); + } + } + log::info!("removing inlays: {:?}", to_remove); + + let (snapshot, edits) = self.splice(to_remove, to_insert); + (snapshot, edits) + } +} + +impl InlaySnapshot { + pub fn to_point(&self, offset: InlayOffset) -> InlayPoint { + let mut cursor = self + .transforms + .cursor::<(InlayOffset, (InlayPoint, usize))>(); + cursor.seek(&offset, Bias::Right, &()); + let overshoot = offset.0 - cursor.start().0 .0; + match cursor.item() { + Some(Transform::Isomorphic(_)) => { + let buffer_offset_start = cursor.start().1 .1; + let buffer_offset_end = buffer_offset_start + overshoot; + let buffer_start = self.buffer.offset_to_point(buffer_offset_start); + let buffer_end = self.buffer.offset_to_point(buffer_offset_end); + InlayPoint(cursor.start().1 .0 .0 + (buffer_end - buffer_start)) + } + Some(Transform::Inlay(inlay)) => { + let overshoot = inlay.text.offset_to_point(overshoot); + InlayPoint(cursor.start().1 .0 .0 + overshoot) + } + None => self.max_point(), + } + } + + pub fn len(&self) -> InlayOffset { + InlayOffset(self.transforms.summary().output.len) + } + + pub fn max_point(&self) -> InlayPoint { + InlayPoint(self.transforms.summary().output.lines) + } + + pub fn to_offset(&self, point: InlayPoint) -> InlayOffset { + let mut cursor = self + .transforms + .cursor::<(InlayPoint, (InlayOffset, Point))>(); + cursor.seek(&point, Bias::Right, &()); + let overshoot = point.0 - cursor.start().0 .0; + match cursor.item() { + Some(Transform::Isomorphic(_)) => { + let buffer_point_start = cursor.start().1 .1; + let buffer_point_end = buffer_point_start + overshoot; + let buffer_offset_start = self.buffer.point_to_offset(buffer_point_start); + let buffer_offset_end = self.buffer.point_to_offset(buffer_point_end); + InlayOffset(cursor.start().1 .0 .0 + (buffer_offset_end - buffer_offset_start)) + } + Some(Transform::Inlay(inlay)) => { + let overshoot = inlay.text.point_to_offset(overshoot); + InlayOffset(cursor.start().1 .0 .0 + overshoot) + } + None => self.len(), + } + } + + pub fn to_buffer_point(&self, point: InlayPoint) -> Point { + let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(); + cursor.seek(&point, Bias::Right, &()); + match cursor.item() { + Some(Transform::Isomorphic(_)) => { + let overshoot = point.0 - cursor.start().0 .0; + cursor.start().1 + overshoot + } + Some(Transform::Inlay(_)) => cursor.start().1, + None => self.buffer.max_point(), + } + } + + pub fn to_buffer_offset(&self, offset: InlayOffset) -> usize { + let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(); + cursor.seek(&offset, Bias::Right, &()); + match cursor.item() { + Some(Transform::Isomorphic(_)) => { + let overshoot = offset - cursor.start().0; + cursor.start().1 + overshoot.0 + } + Some(Transform::Inlay(_)) => cursor.start().1, + None => self.buffer.len(), + } + } + + pub fn to_inlay_offset(&self, offset: usize) -> InlayOffset { + let mut cursor = self.transforms.cursor::<(usize, InlayOffset)>(); + cursor.seek(&offset, Bias::Left, &()); + loop { + match cursor.item() { + Some(Transform::Isomorphic(_)) => { + if offset == cursor.end(&()).0 { + while let Some(Transform::Inlay(inlay)) = cursor.next_item() { + if inlay.position.bias() == Bias::Right { + break; + } else { + cursor.next(&()); + } + } + return cursor.end(&()).1; + } else { + let overshoot = offset - cursor.start().0; + return InlayOffset(cursor.start().1 .0 + overshoot); + } + } + Some(Transform::Inlay(inlay)) => { + if inlay.position.bias() == Bias::Left { + cursor.next(&()); + } else { + return cursor.start().1; + } + } + None => { + return self.len(); + } + } + } + } + + pub fn to_inlay_point(&self, point: Point) -> InlayPoint { + let mut cursor = self.transforms.cursor::<(Point, InlayPoint)>(); + cursor.seek(&point, Bias::Left, &()); + loop { + match cursor.item() { + Some(Transform::Isomorphic(_)) => { + if point == cursor.end(&()).0 { + while let Some(Transform::Inlay(inlay)) = cursor.next_item() { + if inlay.position.bias() == Bias::Right { + break; + } else { + cursor.next(&()); + } + } + return cursor.end(&()).1; + } else { + let overshoot = point - cursor.start().0; + return InlayPoint(cursor.start().1 .0 + overshoot); + } + } + Some(Transform::Inlay(inlay)) => { + if inlay.position.bias() == Bias::Left { + cursor.next(&()); + } else { + return cursor.start().1; + } + } + None => { + return self.max_point(); + } + } + } + } + + pub fn clip_point(&self, mut point: InlayPoint, mut bias: Bias) -> InlayPoint { + let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(); + cursor.seek(&point, Bias::Left, &()); + loop { + match cursor.item() { + Some(Transform::Isomorphic(transform)) => { + if cursor.start().0 == point { + if let Some(Transform::Inlay(inlay)) = cursor.prev_item() { + if inlay.position.bias() == Bias::Left { + return point; + } else if bias == Bias::Left { + cursor.prev(&()); + } else if transform.first_line_chars == 0 { + point.0 += Point::new(1, 0); + } else { + point.0 += Point::new(0, 1); + } + } else { + return point; + } + } else if cursor.end(&()).0 == point { + if let Some(Transform::Inlay(inlay)) = cursor.next_item() { + if inlay.position.bias() == Bias::Right { + return point; + } else if bias == Bias::Right { + cursor.next(&()); + } else if point.0.column == 0 { + point.0.row -= 1; + point.0.column = self.line_len(point.0.row); + } else { + point.0.column -= 1; + } + } else { + return point; + } + } else { + let overshoot = point.0 - cursor.start().0 .0; + let buffer_point = cursor.start().1 + overshoot; + let clipped_buffer_point = self.buffer.clip_point(buffer_point, bias); + let clipped_overshoot = clipped_buffer_point - cursor.start().1; + let clipped_point = InlayPoint(cursor.start().0 .0 + clipped_overshoot); + if clipped_point == point { + return clipped_point; + } else { + point = clipped_point; + } + } + } + Some(Transform::Inlay(inlay)) => { + if point == cursor.start().0 && inlay.position.bias() == Bias::Right { + match cursor.prev_item() { + Some(Transform::Inlay(inlay)) => { + if inlay.position.bias() == Bias::Left { + return point; + } + } + _ => return point, + } + } else if point == cursor.end(&()).0 && inlay.position.bias() == Bias::Left { + match cursor.next_item() { + Some(Transform::Inlay(inlay)) => { + if inlay.position.bias() == Bias::Right { + return point; + } + } + _ => return point, + } + } + + if bias == Bias::Left { + point = cursor.start().0; + cursor.prev(&()); + } else { + cursor.next(&()); + point = cursor.start().0; + } + } + None => { + bias = bias.invert(); + if bias == Bias::Left { + point = cursor.start().0; + cursor.prev(&()); + } else { + cursor.next(&()); + point = cursor.start().0; + } + } + } + } + } + + pub fn text_summary(&self) -> TextSummary { + self.transforms.summary().output.clone() + } + + pub fn text_summary_for_range(&self, range: Range) -> TextSummary { + let mut summary = TextSummary::default(); + + let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(); + cursor.seek(&range.start, Bias::Right, &()); + + let overshoot = range.start.0 - cursor.start().0 .0; + match cursor.item() { + Some(Transform::Isomorphic(_)) => { + let buffer_start = cursor.start().1; + let suffix_start = buffer_start + overshoot; + let suffix_end = + buffer_start + (cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0 .0); + summary = self.buffer.text_summary_for_range(suffix_start..suffix_end); + cursor.next(&()); + } + Some(Transform::Inlay(inlay)) => { + let suffix_start = overshoot; + let suffix_end = cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0 .0; + summary = inlay.text.cursor(suffix_start).summary(suffix_end); + cursor.next(&()); + } + None => {} + } + + if range.end > cursor.start().0 { + summary += cursor + .summary::<_, TransformSummary>(&range.end, Bias::Right, &()) + .output; + + let overshoot = range.end.0 - cursor.start().0 .0; + match cursor.item() { + Some(Transform::Isomorphic(_)) => { + let prefix_start = cursor.start().1; + let prefix_end = prefix_start + overshoot; + summary += self + .buffer + .text_summary_for_range::(prefix_start..prefix_end); + } + Some(Transform::Inlay(inlay)) => { + let prefix_end = overshoot; + summary += inlay.text.cursor(0).summary::(prefix_end); + } + None => {} + } + } + + summary + } + + pub fn buffer_rows<'a>(&'a self, row: u32) -> InlayBufferRows<'a> { + let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(); + let inlay_point = InlayPoint::new(row, 0); + cursor.seek(&inlay_point, Bias::Left, &()); + + let max_buffer_row = self.buffer.max_point().row; + let mut buffer_point = cursor.start().1; + let buffer_row = if row == 0 { + 0 + } else { + match cursor.item() { + Some(Transform::Isomorphic(_)) => { + buffer_point += inlay_point.0 - cursor.start().0 .0; + buffer_point.row + } + _ => cmp::min(buffer_point.row + 1, max_buffer_row), + } + }; + + InlayBufferRows { + transforms: cursor, + inlay_row: inlay_point.row(), + buffer_rows: self.buffer.buffer_rows(buffer_row), + max_buffer_row, + } + } + + pub fn line_len(&self, row: u32) -> u32 { + let line_start = self.to_offset(InlayPoint::new(row, 0)).0; + let line_end = if row >= self.max_point().row() { + self.len().0 + } else { + self.to_offset(InlayPoint::new(row + 1, 0)).0 - 1 + }; + (line_end - line_start) as u32 + } + + pub fn chunks<'a>( + &'a self, + range: Range, + language_aware: bool, + highlights: Highlights<'a>, + ) -> InlayChunks<'a> { + let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(); + cursor.seek(&range.start, Bias::Right, &()); + + let mut highlight_endpoints = Vec::new(); + if let Some(text_highlights) = highlights.text_highlights { + if !text_highlights.is_empty() { + self.apply_text_highlights( + &mut cursor, + &range, + text_highlights, + &mut highlight_endpoints, + ); + cursor.seek(&range.start, Bias::Right, &()); + } + } + highlight_endpoints.sort(); + let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end); + let buffer_chunks = self.buffer.chunks(buffer_range, language_aware); + + InlayChunks { + transforms: cursor, + buffer_chunks, + inlay_chunks: None, + inlay_chunk: None, + buffer_chunk: None, + output_offset: range.start, + max_output_offset: range.end, + inlay_highlight_style: highlights.inlay_highlight_style, + suggestion_highlight_style: highlights.suggestion_highlight_style, + highlight_endpoints: highlight_endpoints.into_iter().peekable(), + active_highlights: Default::default(), + highlights, + snapshot: self, + } + } + + fn apply_text_highlights( + &self, + cursor: &mut Cursor<'_, Transform, (InlayOffset, usize)>, + range: &Range, + text_highlights: &TreeMap, Arc<(HighlightStyle, Vec>)>>, + highlight_endpoints: &mut Vec, + ) { + while cursor.start().0 < range.end { + let transform_start = self + .buffer + .anchor_after(self.to_buffer_offset(cmp::max(range.start, cursor.start().0))); + let transform_end = + { + let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); + self.buffer.anchor_before(self.to_buffer_offset(cmp::min( + cursor.end(&()).0, + cursor.start().0 + overshoot, + ))) + }; + + for (tag, text_highlights) in text_highlights.iter() { + let style = text_highlights.0; + let ranges = &text_highlights.1; + + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe.end.cmp(&transform_start, &self.buffer); + if cmp.is_gt() { + cmp::Ordering::Greater + } else { + cmp::Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + if range.start.cmp(&transform_end, &self.buffer).is_ge() { + break; + } + + highlight_endpoints.push(HighlightEndpoint { + offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)), + is_start: true, + tag: *tag, + style, + }); + highlight_endpoints.push(HighlightEndpoint { + offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), + is_start: false, + tag: *tag, + style, + }); + } + } + + cursor.next(&()); + } + } + + #[cfg(test)] + pub fn text(&self) -> String { + self.chunks(Default::default()..self.len(), false, Highlights::default()) + .map(|chunk| chunk.text) + .collect() + } + + fn check_invariants(&self) { + #[cfg(any(debug_assertions, feature = "test-support"))] + { + assert_eq!(self.transforms.summary().input, self.buffer.text_summary()); + let mut transforms = self.transforms.iter().peekable(); + while let Some(transform) = transforms.next() { + let transform_is_isomorphic = matches!(transform, Transform::Isomorphic(_)); + if let Some(next_transform) = transforms.peek() { + let next_transform_is_isomorphic = + matches!(next_transform, Transform::Isomorphic(_)); + assert!( + !transform_is_isomorphic || !next_transform_is_isomorphic, + "two adjacent isomorphic transforms" + ); + } + } + } + } +} + +fn push_isomorphic(sum_tree: &mut SumTree, summary: TextSummary) { + if summary.len == 0 { + return; + } + + let mut summary = Some(summary); + sum_tree.update_last( + |transform| { + if let Transform::Isomorphic(transform) = transform { + *transform += summary.take().unwrap(); + } + }, + &(), + ); + + if let Some(summary) = summary { + sum_tree.push(Transform::Isomorphic(summary), &()); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + display_map::{InlayHighlights, TextHighlights}, + link_go_to_definition::InlayHighlight, + InlayId, MultiBuffer, + }; + use gpui::AppContext; + use project::{InlayHint, InlayHintLabel, ResolveState}; + use rand::prelude::*; + use settings::SettingsStore; + use std::{cmp::Reverse, env, sync::Arc}; + use text::Patch; + use util::post_inc; + + #[test] + fn test_inlay_properties_label_padding() { + assert_eq!( + Inlay::hint( + 0, + Anchor::min(), + &InlayHint { + label: InlayHintLabel::String("a".to_string()), + position: text::Anchor::default(), + padding_left: false, + padding_right: false, + tooltip: None, + kind: None, + resolve_state: ResolveState::Resolved, + }, + ) + .text + .to_string(), + "a", + "Should not pad label if not requested" + ); + + assert_eq!( + Inlay::hint( + 0, + Anchor::min(), + &InlayHint { + label: InlayHintLabel::String("a".to_string()), + position: text::Anchor::default(), + padding_left: true, + padding_right: true, + tooltip: None, + kind: None, + resolve_state: ResolveState::Resolved, + }, + ) + .text + .to_string(), + " a ", + "Should pad label for every side requested" + ); + + assert_eq!( + Inlay::hint( + 0, + Anchor::min(), + &InlayHint { + label: InlayHintLabel::String(" a ".to_string()), + position: text::Anchor::default(), + padding_left: false, + padding_right: false, + tooltip: None, + kind: None, + resolve_state: ResolveState::Resolved, + }, + ) + .text + .to_string(), + " a ", + "Should not change already padded label" + ); + + assert_eq!( + Inlay::hint( + 0, + Anchor::min(), + &InlayHint { + label: InlayHintLabel::String(" a ".to_string()), + position: text::Anchor::default(), + padding_left: true, + padding_right: true, + tooltip: None, + kind: None, + resolve_state: ResolveState::Resolved, + }, + ) + .text + .to_string(), + " a ", + "Should not change already padded label" + ); + } + + #[gpui::test] + fn test_basic_inlays(cx: &mut AppContext) { + let buffer = MultiBuffer::build_simple("abcdefghi", cx); + let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx)); + assert_eq!(inlay_snapshot.text(), "abcdefghi"); + let mut next_inlay_id = 0; + + let (inlay_snapshot, _) = inlay_map.splice( + Vec::new(), + vec![Inlay { + id: InlayId::Hint(post_inc(&mut next_inlay_id)), + position: buffer.read(cx).snapshot(cx).anchor_after(3), + text: "|123|".into(), + }], + ); + assert_eq!(inlay_snapshot.text(), "abc|123|defghi"); + assert_eq!( + inlay_snapshot.to_inlay_point(Point::new(0, 0)), + InlayPoint::new(0, 0) + ); + assert_eq!( + inlay_snapshot.to_inlay_point(Point::new(0, 1)), + InlayPoint::new(0, 1) + ); + assert_eq!( + inlay_snapshot.to_inlay_point(Point::new(0, 2)), + InlayPoint::new(0, 2) + ); + assert_eq!( + inlay_snapshot.to_inlay_point(Point::new(0, 3)), + InlayPoint::new(0, 3) + ); + assert_eq!( + inlay_snapshot.to_inlay_point(Point::new(0, 4)), + InlayPoint::new(0, 9) + ); + assert_eq!( + inlay_snapshot.to_inlay_point(Point::new(0, 5)), + InlayPoint::new(0, 10) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Left), + InlayPoint::new(0, 0) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Right), + InlayPoint::new(0, 0) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Left), + InlayPoint::new(0, 3) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Right), + InlayPoint::new(0, 3) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Left), + InlayPoint::new(0, 3) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Right), + InlayPoint::new(0, 9) + ); + + // Edits before or after the inlay should not affect it. + buffer.update(cx, |buffer, cx| { + buffer.edit([(2..3, "x"), (3..3, "y"), (4..4, "z")], None, cx) + }); + let (inlay_snapshot, _) = inlay_map.sync( + buffer.read(cx).snapshot(cx), + buffer_edits.consume().into_inner(), + ); + assert_eq!(inlay_snapshot.text(), "abxy|123|dzefghi"); + + // An edit surrounding the inlay should invalidate it. + buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "D")], None, cx)); + let (inlay_snapshot, _) = inlay_map.sync( + buffer.read(cx).snapshot(cx), + buffer_edits.consume().into_inner(), + ); + assert_eq!(inlay_snapshot.text(), "abxyDzefghi"); + + let (inlay_snapshot, _) = inlay_map.splice( + Vec::new(), + vec![ + Inlay { + id: InlayId::Hint(post_inc(&mut next_inlay_id)), + position: buffer.read(cx).snapshot(cx).anchor_before(3), + text: "|123|".into(), + }, + Inlay { + id: InlayId::Suggestion(post_inc(&mut next_inlay_id)), + position: buffer.read(cx).snapshot(cx).anchor_after(3), + text: "|456|".into(), + }, + ], + ); + assert_eq!(inlay_snapshot.text(), "abx|123||456|yDzefghi"); + + // Edits ending where the inlay starts should not move it if it has a left bias. + buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "JKL")], None, cx)); + let (inlay_snapshot, _) = inlay_map.sync( + buffer.read(cx).snapshot(cx), + buffer_edits.consume().into_inner(), + ); + assert_eq!(inlay_snapshot.text(), "abx|123|JKL|456|yDzefghi"); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Left), + InlayPoint::new(0, 0) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Right), + InlayPoint::new(0, 0) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 1), Bias::Left), + InlayPoint::new(0, 1) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 1), Bias::Right), + InlayPoint::new(0, 1) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 2), Bias::Left), + InlayPoint::new(0, 2) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 2), Bias::Right), + InlayPoint::new(0, 2) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Left), + InlayPoint::new(0, 2) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Right), + InlayPoint::new(0, 8) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Left), + InlayPoint::new(0, 2) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Right), + InlayPoint::new(0, 8) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 5), Bias::Left), + InlayPoint::new(0, 2) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 5), Bias::Right), + InlayPoint::new(0, 8) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 6), Bias::Left), + InlayPoint::new(0, 2) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 6), Bias::Right), + InlayPoint::new(0, 8) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 7), Bias::Left), + InlayPoint::new(0, 2) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 7), Bias::Right), + InlayPoint::new(0, 8) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 8), Bias::Left), + InlayPoint::new(0, 8) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 8), Bias::Right), + InlayPoint::new(0, 8) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 9), Bias::Left), + InlayPoint::new(0, 9) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 9), Bias::Right), + InlayPoint::new(0, 9) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 10), Bias::Left), + InlayPoint::new(0, 10) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 10), Bias::Right), + InlayPoint::new(0, 10) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 11), Bias::Left), + InlayPoint::new(0, 11) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 11), Bias::Right), + InlayPoint::new(0, 11) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 12), Bias::Left), + InlayPoint::new(0, 11) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 12), Bias::Right), + InlayPoint::new(0, 17) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 13), Bias::Left), + InlayPoint::new(0, 11) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 13), Bias::Right), + InlayPoint::new(0, 17) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 14), Bias::Left), + InlayPoint::new(0, 11) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 14), Bias::Right), + InlayPoint::new(0, 17) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 15), Bias::Left), + InlayPoint::new(0, 11) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 15), Bias::Right), + InlayPoint::new(0, 17) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 16), Bias::Left), + InlayPoint::new(0, 11) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 16), Bias::Right), + InlayPoint::new(0, 17) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 17), Bias::Left), + InlayPoint::new(0, 17) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 17), Bias::Right), + InlayPoint::new(0, 17) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 18), Bias::Left), + InlayPoint::new(0, 18) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 18), Bias::Right), + InlayPoint::new(0, 18) + ); + + // The inlays can be manually removed. + let (inlay_snapshot, _) = inlay_map.splice( + inlay_map.inlays.iter().map(|inlay| inlay.id).collect(), + Vec::new(), + ); + assert_eq!(inlay_snapshot.text(), "abxJKLyDzefghi"); + } + + #[gpui::test] + fn test_inlay_buffer_rows(cx: &mut AppContext) { + let buffer = MultiBuffer::build_simple("abc\ndef\nghi", cx); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx)); + assert_eq!(inlay_snapshot.text(), "abc\ndef\nghi"); + let mut next_inlay_id = 0; + + let (inlay_snapshot, _) = inlay_map.splice( + Vec::new(), + vec![ + Inlay { + id: InlayId::Hint(post_inc(&mut next_inlay_id)), + position: buffer.read(cx).snapshot(cx).anchor_before(0), + text: "|123|\n".into(), + }, + Inlay { + id: InlayId::Hint(post_inc(&mut next_inlay_id)), + position: buffer.read(cx).snapshot(cx).anchor_before(4), + text: "|456|".into(), + }, + Inlay { + id: InlayId::Suggestion(post_inc(&mut next_inlay_id)), + position: buffer.read(cx).snapshot(cx).anchor_before(7), + text: "\n|567|\n".into(), + }, + ], + ); + assert_eq!(inlay_snapshot.text(), "|123|\nabc\n|456|def\n|567|\n\nghi"); + assert_eq!( + inlay_snapshot.buffer_rows(0).collect::>(), + vec![Some(0), None, Some(1), None, None, Some(2)] + ); + } + + #[gpui::test(iterations = 100)] + fn test_random_inlays(cx: &mut AppContext, mut rng: StdRng) { + init_test(cx); + + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + + let len = rng.gen_range(0..30); + let buffer = if rng.gen() { + let text = util::RandomCharIter::new(&mut rng) + .take(len) + .collect::(); + MultiBuffer::build_simple(&text, cx) + } else { + MultiBuffer::build_random(&mut rng, cx) + }; + let mut buffer_snapshot = buffer.read(cx).snapshot(cx); + let mut next_inlay_id = 0; + log::info!("buffer text: {:?}", buffer_snapshot.text()); + let (mut inlay_map, mut inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + for _ in 0..operations { + let mut inlay_edits = Patch::default(); + + let mut prev_inlay_text = inlay_snapshot.text(); + let mut buffer_edits = Vec::new(); + match rng.gen_range(0..=100) { + 0..=50 => { + let (snapshot, edits) = inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng); + log::info!("mutated text: {:?}", snapshot.text()); + inlay_edits = Patch::new(edits); + } + _ => buffer.update(cx, |buffer, cx| { + let subscription = buffer.subscribe(); + let edit_count = rng.gen_range(1..=5); + buffer.randomly_mutate(&mut rng, edit_count, cx); + buffer_snapshot = buffer.snapshot(cx); + let edits = subscription.consume().into_inner(); + log::info!("editing {:?}", edits); + buffer_edits.extend(edits); + }), + }; + + let (new_inlay_snapshot, new_inlay_edits) = + inlay_map.sync(buffer_snapshot.clone(), buffer_edits); + inlay_snapshot = new_inlay_snapshot; + inlay_edits = inlay_edits.compose(new_inlay_edits); + + log::info!("buffer text: {:?}", buffer_snapshot.text()); + log::info!("inlay text: {:?}", inlay_snapshot.text()); + + let inlays = inlay_map + .inlays + .iter() + .filter(|inlay| inlay.position.is_valid(&buffer_snapshot)) + .map(|inlay| { + let offset = inlay.position.to_offset(&buffer_snapshot); + (offset, inlay.clone()) + }) + .collect::>(); + let mut expected_text = Rope::from(buffer_snapshot.text()); + for (offset, inlay) in inlays.iter().rev() { + expected_text.replace(*offset..*offset, &inlay.text.to_string()); + } + assert_eq!(inlay_snapshot.text(), expected_text.to_string()); + + let expected_buffer_rows = inlay_snapshot.buffer_rows(0).collect::>(); + assert_eq!( + expected_buffer_rows.len() as u32, + expected_text.max_point().row + 1 + ); + for row_start in 0..expected_buffer_rows.len() { + assert_eq!( + inlay_snapshot + .buffer_rows(row_start as u32) + .collect::>(), + &expected_buffer_rows[row_start..], + "incorrect buffer rows starting at {}", + row_start + ); + } + + let mut text_highlights = TextHighlights::default(); + let text_highlight_count = rng.gen_range(0_usize..10); + let mut text_highlight_ranges = (0..text_highlight_count) + .map(|_| buffer_snapshot.random_byte_range(0, &mut rng)) + .collect::>(); + text_highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end))); + log::info!("highlighting text ranges {text_highlight_ranges:?}"); + text_highlights.insert( + Some(TypeId::of::<()>()), + Arc::new(( + HighlightStyle::default(), + text_highlight_ranges + .into_iter() + .map(|range| { + buffer_snapshot.anchor_before(range.start) + ..buffer_snapshot.anchor_after(range.end) + }) + .collect(), + )), + ); + + let mut inlay_highlights = InlayHighlights::default(); + if !inlays.is_empty() { + let inlay_highlight_count = rng.gen_range(0..inlays.len()); + let mut inlay_indices = BTreeSet::default(); + while inlay_indices.len() < inlay_highlight_count { + inlay_indices.insert(rng.gen_range(0..inlays.len())); + } + let new_highlights = inlay_indices + .into_iter() + .filter_map(|i| { + let (_, inlay) = &inlays[i]; + let inlay_text_len = inlay.text.len(); + match inlay_text_len { + 0 => None, + 1 => Some(InlayHighlight { + inlay: inlay.id, + inlay_position: inlay.position, + range: 0..1, + }), + n => { + let inlay_text = inlay.text.to_string(); + let mut highlight_end = rng.gen_range(1..n); + let mut highlight_start = rng.gen_range(0..highlight_end); + while !inlay_text.is_char_boundary(highlight_end) { + highlight_end += 1; + } + while !inlay_text.is_char_boundary(highlight_start) { + highlight_start -= 1; + } + Some(InlayHighlight { + inlay: inlay.id, + inlay_position: inlay.position, + range: highlight_start..highlight_end, + }) + } + } + }) + .map(|highlight| (highlight.inlay, (HighlightStyle::default(), highlight))) + .collect(); + log::info!("highlighting inlay ranges {new_highlights:?}"); + inlay_highlights.insert(TypeId::of::<()>(), new_highlights); + } + + for _ in 0..5 { + let mut end = rng.gen_range(0..=inlay_snapshot.len().0); + end = expected_text.clip_offset(end, Bias::Right); + let mut start = rng.gen_range(0..=end); + start = expected_text.clip_offset(start, Bias::Right); + + let range = InlayOffset(start)..InlayOffset(end); + log::info!("calling inlay_snapshot.chunks({range:?})"); + let actual_text = inlay_snapshot + .chunks( + range, + false, + Highlights { + text_highlights: Some(&text_highlights), + inlay_highlights: Some(&inlay_highlights), + ..Highlights::default() + }, + ) + .map(|chunk| chunk.text) + .collect::(); + assert_eq!( + actual_text, + expected_text.slice(start..end).to_string(), + "incorrect text in range {:?}", + start..end + ); + + assert_eq!( + inlay_snapshot.text_summary_for_range(InlayOffset(start)..InlayOffset(end)), + expected_text.slice(start..end).summary() + ); + } + + for edit in inlay_edits { + prev_inlay_text.replace_range( + edit.new.start.0..edit.new.start.0 + edit.old_len().0, + &inlay_snapshot.text()[edit.new.start.0..edit.new.end.0], + ); + } + assert_eq!(prev_inlay_text, inlay_snapshot.text()); + + assert_eq!(expected_text.max_point(), inlay_snapshot.max_point().0); + assert_eq!(expected_text.len(), inlay_snapshot.len().0); + + let mut buffer_point = Point::default(); + let mut inlay_point = inlay_snapshot.to_inlay_point(buffer_point); + let mut buffer_chars = buffer_snapshot.chars_at(0); + loop { + // Ensure conversion from buffer coordinates to inlay coordinates + // is consistent. + let buffer_offset = buffer_snapshot.point_to_offset(buffer_point); + assert_eq!( + inlay_snapshot.to_point(inlay_snapshot.to_inlay_offset(buffer_offset)), + inlay_point + ); + + // No matter which bias we clip an inlay point with, it doesn't move + // because it was constructed from a buffer point. + assert_eq!( + inlay_snapshot.clip_point(inlay_point, Bias::Left), + inlay_point, + "invalid inlay point for buffer point {:?} when clipped left", + buffer_point + ); + assert_eq!( + inlay_snapshot.clip_point(inlay_point, Bias::Right), + inlay_point, + "invalid inlay point for buffer point {:?} when clipped right", + buffer_point + ); + + if let Some(ch) = buffer_chars.next() { + if ch == '\n' { + buffer_point += Point::new(1, 0); + } else { + buffer_point += Point::new(0, ch.len_utf8() as u32); + } + + // Ensure that moving forward in the buffer always moves the inlay point forward as well. + let new_inlay_point = inlay_snapshot.to_inlay_point(buffer_point); + assert!(new_inlay_point > inlay_point); + inlay_point = new_inlay_point; + } else { + break; + } + } + + let mut inlay_point = InlayPoint::default(); + let mut inlay_offset = InlayOffset::default(); + for ch in expected_text.chars() { + assert_eq!( + inlay_snapshot.to_offset(inlay_point), + inlay_offset, + "invalid to_offset({:?})", + inlay_point + ); + assert_eq!( + inlay_snapshot.to_point(inlay_offset), + inlay_point, + "invalid to_point({:?})", + inlay_offset + ); + + let mut bytes = [0; 4]; + for byte in ch.encode_utf8(&mut bytes).as_bytes() { + inlay_offset.0 += 1; + if *byte == b'\n' { + inlay_point.0 += Point::new(1, 0); + } else { + inlay_point.0 += Point::new(0, 1); + } + + let clipped_left_point = inlay_snapshot.clip_point(inlay_point, Bias::Left); + let clipped_right_point = inlay_snapshot.clip_point(inlay_point, Bias::Right); + assert!( + clipped_left_point <= clipped_right_point, + "inlay point {:?} when clipped left is greater than when clipped right ({:?} > {:?})", + inlay_point, + clipped_left_point, + clipped_right_point + ); + + // Ensure the clipped points are at valid text locations. + assert_eq!( + clipped_left_point.0, + expected_text.clip_point(clipped_left_point.0, Bias::Left) + ); + assert_eq!( + clipped_right_point.0, + expected_text.clip_point(clipped_right_point.0, Bias::Right) + ); + + // Ensure the clipped points never overshoot the end of the map. + assert!(clipped_left_point <= inlay_snapshot.max_point()); + assert!(clipped_right_point <= inlay_snapshot.max_point()); + + // Ensure the clipped points are at valid buffer locations. + assert_eq!( + inlay_snapshot + .to_inlay_point(inlay_snapshot.to_buffer_point(clipped_left_point)), + clipped_left_point, + "to_buffer_point({:?}) = {:?}", + clipped_left_point, + inlay_snapshot.to_buffer_point(clipped_left_point), + ); + assert_eq!( + inlay_snapshot + .to_inlay_point(inlay_snapshot.to_buffer_point(clipped_right_point)), + clipped_right_point, + "to_buffer_point({:?}) = {:?}", + clipped_right_point, + inlay_snapshot.to_buffer_point(clipped_right_point), + ); + } + } + } + } + + fn init_test(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); + theme::init((), cx); + } +} diff --git a/crates/editor2/src/display_map/tab_map.rs b/crates/editor2/src/display_map/tab_map.rs new file mode 100644 index 0000000000..6b38ea2d24 --- /dev/null +++ b/crates/editor2/src/display_map/tab_map.rs @@ -0,0 +1,765 @@ +use super::{ + fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot}, + Highlights, +}; +use crate::MultiBufferSnapshot; +use language::{Chunk, Point}; +use std::{cmp, mem, num::NonZeroU32, ops::Range}; +use sum_tree::Bias; + +const MAX_EXPANSION_COLUMN: u32 = 256; + +pub struct TabMap(TabSnapshot); + +impl TabMap { + pub fn new(fold_snapshot: FoldSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) { + let snapshot = TabSnapshot { + fold_snapshot, + tab_size, + max_expansion_column: MAX_EXPANSION_COLUMN, + version: 0, + }; + (Self(snapshot.clone()), snapshot) + } + + #[cfg(test)] + pub fn set_max_expansion_column(&mut self, column: u32) -> TabSnapshot { + self.0.max_expansion_column = column; + self.0.clone() + } + + pub fn sync( + &mut self, + fold_snapshot: FoldSnapshot, + mut fold_edits: Vec, + tab_size: NonZeroU32, + ) -> (TabSnapshot, Vec) { + let old_snapshot = &mut self.0; + let mut new_snapshot = TabSnapshot { + fold_snapshot, + tab_size, + max_expansion_column: old_snapshot.max_expansion_column, + version: old_snapshot.version, + }; + + if old_snapshot.fold_snapshot.version != new_snapshot.fold_snapshot.version { + new_snapshot.version += 1; + } + + let mut tab_edits = Vec::with_capacity(fold_edits.len()); + + if old_snapshot.tab_size == new_snapshot.tab_size { + // Expand each edit to include the next tab on the same line as the edit, + // and any subsequent tabs on that line that moved across the tab expansion + // boundary. + for fold_edit in &mut fold_edits { + let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot); + let old_end_row_successor_offset = cmp::min( + FoldPoint::new(old_end.row() + 1, 0), + old_snapshot.fold_snapshot.max_point(), + ) + .to_offset(&old_snapshot.fold_snapshot); + let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot); + + let mut offset_from_edit = 0; + let mut first_tab_offset = None; + let mut last_tab_with_changed_expansion_offset = None; + 'outer: for chunk in old_snapshot.fold_snapshot.chunks( + fold_edit.old.end..old_end_row_successor_offset, + false, + Highlights::default(), + ) { + for (ix, _) in chunk.text.match_indices('\t') { + let offset_from_edit = offset_from_edit + (ix as u32); + if first_tab_offset.is_none() { + first_tab_offset = Some(offset_from_edit); + } + + let old_column = old_end.column() + offset_from_edit; + let new_column = new_end.column() + offset_from_edit; + let was_expanded = old_column < old_snapshot.max_expansion_column; + let is_expanded = new_column < new_snapshot.max_expansion_column; + if was_expanded != is_expanded { + last_tab_with_changed_expansion_offset = Some(offset_from_edit); + } else if !was_expanded && !is_expanded { + break 'outer; + } + } + + offset_from_edit += chunk.text.len() as u32; + if old_end.column() + offset_from_edit >= old_snapshot.max_expansion_column + && new_end.column() + offset_from_edit >= new_snapshot.max_expansion_column + { + break; + } + } + + if let Some(offset) = last_tab_with_changed_expansion_offset.or(first_tab_offset) { + fold_edit.old.end.0 += offset as usize + 1; + fold_edit.new.end.0 += offset as usize + 1; + } + } + + // Combine any edits that overlap due to the expansion. + let mut ix = 1; + while ix < fold_edits.len() { + let (prev_edits, next_edits) = fold_edits.split_at_mut(ix); + let prev_edit = prev_edits.last_mut().unwrap(); + let edit = &next_edits[0]; + if prev_edit.old.end >= edit.old.start { + prev_edit.old.end = edit.old.end; + prev_edit.new.end = edit.new.end; + fold_edits.remove(ix); + } else { + ix += 1; + } + } + + for fold_edit in fold_edits { + let old_start = fold_edit.old.start.to_point(&old_snapshot.fold_snapshot); + let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot); + let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot); + let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot); + tab_edits.push(TabEdit { + old: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end), + new: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end), + }); + } + } else { + new_snapshot.version += 1; + tab_edits.push(TabEdit { + old: TabPoint::zero()..old_snapshot.max_point(), + new: TabPoint::zero()..new_snapshot.max_point(), + }); + } + + *old_snapshot = new_snapshot; + (old_snapshot.clone(), tab_edits) + } +} + +#[derive(Clone)] +pub struct TabSnapshot { + pub fold_snapshot: FoldSnapshot, + pub tab_size: NonZeroU32, + pub max_expansion_column: u32, + pub version: usize, +} + +impl TabSnapshot { + pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot { + &self.fold_snapshot.inlay_snapshot.buffer + } + + pub fn line_len(&self, row: u32) -> u32 { + let max_point = self.max_point(); + if row < max_point.row() { + self.to_tab_point(FoldPoint::new(row, self.fold_snapshot.line_len(row))) + .0 + .column + } else { + max_point.column() + } + } + + pub fn text_summary(&self) -> TextSummary { + self.text_summary_for_range(TabPoint::zero()..self.max_point()) + } + + pub fn text_summary_for_range(&self, range: Range) -> TextSummary { + let input_start = self.to_fold_point(range.start, Bias::Left).0; + let input_end = self.to_fold_point(range.end, Bias::Right).0; + let input_summary = self + .fold_snapshot + .text_summary_for_range(input_start..input_end); + + let mut first_line_chars = 0; + let line_end = if range.start.row() == range.end.row() { + range.end + } else { + self.max_point() + }; + for c in self + .chunks(range.start..line_end, false, Highlights::default()) + .flat_map(|chunk| chunk.text.chars()) + { + if c == '\n' { + break; + } + first_line_chars += 1; + } + + let mut last_line_chars = 0; + if range.start.row() == range.end.row() { + last_line_chars = first_line_chars; + } else { + for _ in self + .chunks( + TabPoint::new(range.end.row(), 0)..range.end, + false, + Highlights::default(), + ) + .flat_map(|chunk| chunk.text.chars()) + { + last_line_chars += 1; + } + } + + TextSummary { + lines: range.end.0 - range.start.0, + first_line_chars, + last_line_chars, + longest_row: input_summary.longest_row, + longest_row_chars: input_summary.longest_row_chars, + } + } + + pub fn chunks<'a>( + &'a self, + range: Range, + language_aware: bool, + highlights: Highlights<'a>, + ) -> TabChunks<'a> { + let (input_start, expanded_char_column, to_next_stop) = + self.to_fold_point(range.start, Bias::Left); + let input_column = input_start.column(); + let input_start = input_start.to_offset(&self.fold_snapshot); + let input_end = self + .to_fold_point(range.end, Bias::Right) + .0 + .to_offset(&self.fold_snapshot); + let to_next_stop = if range.start.0 + Point::new(0, to_next_stop) > range.end.0 { + range.end.column() - range.start.column() + } else { + to_next_stop + }; + + TabChunks { + fold_chunks: self.fold_snapshot.chunks( + input_start..input_end, + language_aware, + highlights, + ), + input_column, + column: expanded_char_column, + max_expansion_column: self.max_expansion_column, + output_position: range.start.0, + max_output_position: range.end.0, + tab_size: self.tab_size, + chunk: Chunk { + text: &SPACES[0..(to_next_stop as usize)], + is_tab: true, + ..Default::default() + }, + inside_leading_tab: to_next_stop > 0, + } + } + + pub fn buffer_rows(&self, row: u32) -> fold_map::FoldBufferRows<'_> { + self.fold_snapshot.buffer_rows(row) + } + + #[cfg(test)] + pub fn text(&self) -> String { + self.chunks( + TabPoint::zero()..self.max_point(), + false, + Highlights::default(), + ) + .map(|chunk| chunk.text) + .collect() + } + + pub fn max_point(&self) -> TabPoint { + self.to_tab_point(self.fold_snapshot.max_point()) + } + + pub fn clip_point(&self, point: TabPoint, bias: Bias) -> TabPoint { + self.to_tab_point( + self.fold_snapshot + .clip_point(self.to_fold_point(point, bias).0, bias), + ) + } + + pub fn to_tab_point(&self, input: FoldPoint) -> TabPoint { + let chars = self.fold_snapshot.chars_at(FoldPoint::new(input.row(), 0)); + let expanded = self.expand_tabs(chars, input.column()); + TabPoint::new(input.row(), expanded) + } + + pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, u32, u32) { + let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0)); + let expanded = output.column(); + let (collapsed, expanded_char_column, to_next_stop) = + self.collapse_tabs(chars, expanded, bias); + ( + FoldPoint::new(output.row(), collapsed as u32), + expanded_char_column, + to_next_stop, + ) + } + + pub fn make_tab_point(&self, point: Point, bias: Bias) -> TabPoint { + let inlay_point = self.fold_snapshot.inlay_snapshot.to_inlay_point(point); + let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias); + self.to_tab_point(fold_point) + } + + pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point { + let fold_point = self.to_fold_point(point, bias).0; + let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot); + self.fold_snapshot + .inlay_snapshot + .to_buffer_point(inlay_point) + } + + fn expand_tabs(&self, chars: impl Iterator, column: u32) -> u32 { + let tab_size = self.tab_size.get(); + + let mut expanded_chars = 0; + let mut expanded_bytes = 0; + let mut collapsed_bytes = 0; + let end_column = column.min(self.max_expansion_column); + for c in chars { + if collapsed_bytes >= end_column { + break; + } + if c == '\t' { + let tab_len = tab_size - expanded_chars % tab_size; + expanded_bytes += tab_len; + expanded_chars += tab_len; + } else { + expanded_bytes += c.len_utf8() as u32; + expanded_chars += 1; + } + collapsed_bytes += c.len_utf8() as u32; + } + expanded_bytes + column.saturating_sub(collapsed_bytes) + } + + fn collapse_tabs( + &self, + chars: impl Iterator, + column: u32, + bias: Bias, + ) -> (u32, u32, u32) { + let tab_size = self.tab_size.get(); + + let mut expanded_bytes = 0; + let mut expanded_chars = 0; + let mut collapsed_bytes = 0; + for c in chars { + if expanded_bytes >= column { + break; + } + if collapsed_bytes >= self.max_expansion_column { + break; + } + + if c == '\t' { + let tab_len = tab_size - (expanded_chars % tab_size); + expanded_chars += tab_len; + expanded_bytes += tab_len; + if expanded_bytes > column { + expanded_chars -= expanded_bytes - column; + return match bias { + Bias::Left => (collapsed_bytes, expanded_chars, expanded_bytes - column), + Bias::Right => (collapsed_bytes + 1, expanded_chars, 0), + }; + } + } else { + expanded_chars += 1; + expanded_bytes += c.len_utf8() as u32; + } + + if expanded_bytes > column && matches!(bias, Bias::Left) { + expanded_chars -= 1; + break; + } + + collapsed_bytes += c.len_utf8() as u32; + } + ( + collapsed_bytes + column.saturating_sub(expanded_bytes), + expanded_chars, + 0, + ) + } +} + +#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] +pub struct TabPoint(pub Point); + +impl TabPoint { + pub fn new(row: u32, column: u32) -> Self { + Self(Point::new(row, column)) + } + + pub fn zero() -> Self { + Self::new(0, 0) + } + + pub fn row(self) -> u32 { + self.0.row + } + + pub fn column(self) -> u32 { + self.0.column + } +} + +impl From for TabPoint { + fn from(point: Point) -> Self { + Self(point) + } +} + +pub type TabEdit = text::Edit; + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct TextSummary { + pub lines: Point, + pub first_line_chars: u32, + pub last_line_chars: u32, + pub longest_row: u32, + pub longest_row_chars: u32, +} + +impl<'a> From<&'a str> for TextSummary { + fn from(text: &'a str) -> Self { + let sum = text::TextSummary::from(text); + + TextSummary { + lines: sum.lines, + first_line_chars: sum.first_line_chars, + last_line_chars: sum.last_line_chars, + longest_row: sum.longest_row, + longest_row_chars: sum.longest_row_chars, + } + } +} + +impl<'a> std::ops::AddAssign<&'a Self> for TextSummary { + fn add_assign(&mut self, other: &'a Self) { + let joined_chars = self.last_line_chars + other.first_line_chars; + if joined_chars > self.longest_row_chars { + self.longest_row = self.lines.row; + self.longest_row_chars = joined_chars; + } + if other.longest_row_chars > self.longest_row_chars { + self.longest_row = self.lines.row + other.longest_row; + self.longest_row_chars = other.longest_row_chars; + } + + if self.lines.row == 0 { + self.first_line_chars += other.first_line_chars; + } + + if other.lines.row == 0 { + self.last_line_chars += other.first_line_chars; + } else { + self.last_line_chars = other.last_line_chars; + } + + self.lines += &other.lines; + } +} + +// Handles a tab width <= 16 +const SPACES: &str = " "; + +pub struct TabChunks<'a> { + fold_chunks: FoldChunks<'a>, + chunk: Chunk<'a>, + column: u32, + max_expansion_column: u32, + output_position: Point, + input_column: u32, + max_output_position: Point, + tab_size: NonZeroU32, + inside_leading_tab: bool, +} + +impl<'a> Iterator for TabChunks<'a> { + type Item = Chunk<'a>; + + fn next(&mut self) -> Option { + if self.chunk.text.is_empty() { + if let Some(chunk) = self.fold_chunks.next() { + self.chunk = chunk; + if self.inside_leading_tab { + self.chunk.text = &self.chunk.text[1..]; + self.inside_leading_tab = false; + self.input_column += 1; + } + } else { + return None; + } + } + + for (ix, c) in self.chunk.text.char_indices() { + match c { + '\t' => { + if ix > 0 { + let (prefix, suffix) = self.chunk.text.split_at(ix); + self.chunk.text = suffix; + return Some(Chunk { + text: prefix, + ..self.chunk + }); + } else { + self.chunk.text = &self.chunk.text[1..]; + let tab_size = if self.input_column < self.max_expansion_column { + self.tab_size.get() as u32 + } else { + 1 + }; + let mut len = tab_size - self.column % tab_size; + let next_output_position = cmp::min( + self.output_position + Point::new(0, len), + self.max_output_position, + ); + len = next_output_position.column - self.output_position.column; + self.column += len; + self.input_column += 1; + self.output_position = next_output_position; + return Some(Chunk { + text: &SPACES[..len as usize], + is_tab: true, + ..self.chunk + }); + } + } + '\n' => { + self.column = 0; + self.input_column = 0; + self.output_position += Point::new(1, 0); + } + _ => { + self.column += 1; + if !self.inside_leading_tab { + self.input_column += c.len_utf8() as u32; + } + self.output_position.column += c.len_utf8() as u32; + } + } + } + + Some(mem::take(&mut self.chunk)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + display_map::{fold_map::FoldMap, inlay_map::InlayMap}, + MultiBuffer, + }; + use rand::{prelude::StdRng, Rng}; + + #[gpui::test] + fn test_expand_tabs(cx: &mut gpui::AppContext) { + let buffer = MultiBuffer::build_simple("", cx); + let buffer_snapshot = buffer.read(cx).snapshot(cx); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); + let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); + + assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 0), 0); + assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 1), 4); + assert_eq!(tab_snapshot.expand_tabs("\ta".chars(), 2), 5); + } + + #[gpui::test] + fn test_long_lines(cx: &mut gpui::AppContext) { + let max_expansion_column = 12; + let input = "A\tBC\tDEF\tG\tHI\tJ\tK\tL\tM"; + let output = "A BC DEF G HI J K L M"; + + let buffer = MultiBuffer::build_simple(input, cx); + let buffer_snapshot = buffer.read(cx).snapshot(cx); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); + let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); + + tab_snapshot.max_expansion_column = max_expansion_column; + assert_eq!(tab_snapshot.text(), output); + + for (ix, c) in input.char_indices() { + assert_eq!( + tab_snapshot + .chunks( + TabPoint::new(0, ix as u32)..tab_snapshot.max_point(), + false, + Highlights::default(), + ) + .map(|c| c.text) + .collect::(), + &output[ix..], + "text from index {ix}" + ); + + if c != '\t' { + let input_point = Point::new(0, ix as u32); + let output_point = Point::new(0, output.find(c).unwrap() as u32); + assert_eq!( + tab_snapshot.to_tab_point(FoldPoint(input_point)), + TabPoint(output_point), + "to_tab_point({input_point:?})" + ); + assert_eq!( + tab_snapshot + .to_fold_point(TabPoint(output_point), Bias::Left) + .0, + FoldPoint(input_point), + "to_fold_point({output_point:?})" + ); + } + } + } + + #[gpui::test] + fn test_long_lines_with_character_spanning_max_expansion_column(cx: &mut gpui::AppContext) { + let max_expansion_column = 8; + let input = "abcdefg⋯hij"; + + let buffer = MultiBuffer::build_simple(input, cx); + let buffer_snapshot = buffer.read(cx).snapshot(cx); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); + let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); + + tab_snapshot.max_expansion_column = max_expansion_column; + assert_eq!(tab_snapshot.text(), input); + } + + #[gpui::test] + fn test_marking_tabs(cx: &mut gpui::AppContext) { + let input = "\t \thello"; + + let buffer = MultiBuffer::build_simple(&input, cx); + let buffer_snapshot = buffer.read(cx).snapshot(cx); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); + let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); + + assert_eq!( + chunks(&tab_snapshot, TabPoint::zero()), + vec![ + (" ".to_string(), true), + (" ".to_string(), false), + (" ".to_string(), true), + ("hello".to_string(), false), + ] + ); + assert_eq!( + chunks(&tab_snapshot, TabPoint::new(0, 2)), + vec![ + (" ".to_string(), true), + (" ".to_string(), false), + (" ".to_string(), true), + ("hello".to_string(), false), + ] + ); + + fn chunks(snapshot: &TabSnapshot, start: TabPoint) -> Vec<(String, bool)> { + let mut chunks = Vec::new(); + let mut was_tab = false; + let mut text = String::new(); + for chunk in snapshot.chunks(start..snapshot.max_point(), false, Highlights::default()) + { + if chunk.is_tab != was_tab { + if !text.is_empty() { + chunks.push((mem::take(&mut text), was_tab)); + } + was_tab = chunk.is_tab; + } + text.push_str(chunk.text); + } + + if !text.is_empty() { + chunks.push((text, was_tab)); + } + chunks + } + } + + #[gpui::test(iterations = 100)] + fn test_random_tabs(cx: &mut gpui::AppContext, mut rng: StdRng) { + let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap(); + let len = rng.gen_range(0..30); + let buffer = if rng.gen() { + let text = util::RandomCharIter::new(&mut rng) + .take(len) + .collect::(); + MultiBuffer::build_simple(&text, cx) + } else { + MultiBuffer::build_random(&mut rng, cx) + }; + let buffer_snapshot = buffer.read(cx).snapshot(cx); + log::info!("Buffer text: {:?}", buffer_snapshot.text()); + + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + log::info!("InlayMap text: {:?}", inlay_snapshot.text()); + let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone()); + fold_map.randomly_mutate(&mut rng); + let (fold_snapshot, _) = fold_map.read(inlay_snapshot, vec![]); + log::info!("FoldMap text: {:?}", fold_snapshot.text()); + let (inlay_snapshot, _) = inlay_map.randomly_mutate(&mut 0, &mut rng); + log::info!("InlayMap text: {:?}", inlay_snapshot.text()); + + let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size); + let tabs_snapshot = tab_map.set_max_expansion_column(32); + + let text = text::Rope::from(tabs_snapshot.text().as_str()); + log::info!( + "TabMap text (tab size: {}): {:?}", + tab_size, + tabs_snapshot.text(), + ); + + for _ in 0..5 { + let end_row = rng.gen_range(0..=text.max_point().row); + let end_column = rng.gen_range(0..=text.line_len(end_row)); + let mut end = TabPoint(text.clip_point(Point::new(end_row, end_column), Bias::Right)); + let start_row = rng.gen_range(0..=text.max_point().row); + let start_column = rng.gen_range(0..=text.line_len(start_row)); + let mut start = + TabPoint(text.clip_point(Point::new(start_row, start_column), Bias::Left)); + if start > end { + mem::swap(&mut start, &mut end); + } + + let expected_text = text + .chunks_in_range(text.point_to_offset(start.0)..text.point_to_offset(end.0)) + .collect::(); + let expected_summary = TextSummary::from(expected_text.as_str()); + assert_eq!( + tabs_snapshot + .chunks(start..end, false, Highlights::default()) + .map(|c| c.text) + .collect::(), + expected_text, + "chunks({:?}..{:?})", + start, + end + ); + + let mut actual_summary = tabs_snapshot.text_summary_for_range(start..end); + if tab_size.get() > 1 && inlay_snapshot.text().contains('\t') { + actual_summary.longest_row = expected_summary.longest_row; + actual_summary.longest_row_chars = expected_summary.longest_row_chars; + } + assert_eq!(actual_summary, expected_summary); + } + + for row in 0..=text.max_point().row { + assert_eq!( + tabs_snapshot.line_len(row), + text.line_len(row), + "line_len({row})" + ); + } + } +} diff --git a/crates/editor2/src/display_map/wrap_map.rs b/crates/editor2/src/display_map/wrap_map.rs new file mode 100644 index 0000000000..60337661c1 --- /dev/null +++ b/crates/editor2/src/display_map/wrap_map.rs @@ -0,0 +1,1355 @@ +use super::{ + fold_map::FoldBufferRows, + tab_map::{self, TabEdit, TabPoint, TabSnapshot}, + Highlights, +}; +use crate::MultiBufferSnapshot; +use gpui::{ + fonts::FontId, text_layout::LineWrapper, AppContext, Entity, ModelContext, ModelHandle, Task, +}; +use language::{Chunk, Point}; +use lazy_static::lazy_static; +use smol::future::yield_now; +use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration}; +use sum_tree::{Bias, Cursor, SumTree}; +use text::Patch; + +pub use super::tab_map::TextSummary; +pub type WrapEdit = text::Edit; + +pub struct WrapMap { + snapshot: WrapSnapshot, + pending_edits: VecDeque<(TabSnapshot, Vec)>, + interpolated_edits: Patch, + edits_since_sync: Patch, + wrap_width: Option, + background_task: Option>, + font: (FontId, f32), +} + +impl Entity for WrapMap { + type Event = (); +} + +#[derive(Clone)] +pub struct WrapSnapshot { + tab_snapshot: TabSnapshot, + transforms: SumTree, + interpolated: bool, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +struct Transform { + summary: TransformSummary, + display_text: Option<&'static str>, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +struct TransformSummary { + input: TextSummary, + output: TextSummary, +} + +#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] +pub struct WrapPoint(pub Point); + +pub struct WrapChunks<'a> { + input_chunks: tab_map::TabChunks<'a>, + input_chunk: Chunk<'a>, + output_position: WrapPoint, + max_output_row: u32, + transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>, +} + +#[derive(Clone)] +pub struct WrapBufferRows<'a> { + input_buffer_rows: FoldBufferRows<'a>, + input_buffer_row: Option, + output_row: u32, + soft_wrapped: bool, + max_output_row: u32, + transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>, +} + +impl WrapMap { + pub fn new( + tab_snapshot: TabSnapshot, + font_id: FontId, + font_size: f32, + wrap_width: Option, + cx: &mut AppContext, + ) -> (ModelHandle, WrapSnapshot) { + let handle = cx.add_model(|cx| { + let mut this = Self { + font: (font_id, font_size), + wrap_width: None, + pending_edits: Default::default(), + interpolated_edits: Default::default(), + edits_since_sync: Default::default(), + snapshot: WrapSnapshot::new(tab_snapshot), + background_task: None, + }; + this.set_wrap_width(wrap_width, cx); + mem::take(&mut this.edits_since_sync); + this + }); + let snapshot = handle.read(cx).snapshot.clone(); + (handle, snapshot) + } + + #[cfg(test)] + pub fn is_rewrapping(&self) -> bool { + self.background_task.is_some() + } + + pub fn sync( + &mut self, + tab_snapshot: TabSnapshot, + edits: Vec, + cx: &mut ModelContext, + ) -> (WrapSnapshot, Patch) { + if self.wrap_width.is_some() { + self.pending_edits.push_back((tab_snapshot, edits)); + self.flush_edits(cx); + } else { + self.edits_since_sync = self + .edits_since_sync + .compose(&self.snapshot.interpolate(tab_snapshot, &edits)); + self.snapshot.interpolated = false; + } + + (self.snapshot.clone(), mem::take(&mut self.edits_since_sync)) + } + + pub fn set_font( + &mut self, + font_id: FontId, + font_size: f32, + cx: &mut ModelContext, + ) -> bool { + if (font_id, font_size) != self.font { + self.font = (font_id, font_size); + self.rewrap(cx); + true + } else { + false + } + } + + pub fn set_wrap_width(&mut self, wrap_width: Option, cx: &mut ModelContext) -> bool { + if wrap_width == self.wrap_width { + return false; + } + + self.wrap_width = wrap_width; + self.rewrap(cx); + true + } + + fn rewrap(&mut self, cx: &mut ModelContext) { + self.background_task.take(); + self.interpolated_edits.clear(); + self.pending_edits.clear(); + + if let Some(wrap_width) = self.wrap_width { + let mut new_snapshot = self.snapshot.clone(); + let font_cache = cx.font_cache().clone(); + let (font_id, font_size) = self.font; + let task = cx.background().spawn(async move { + let mut line_wrapper = font_cache.line_wrapper(font_id, font_size); + let tab_snapshot = new_snapshot.tab_snapshot.clone(); + let range = TabPoint::zero()..tab_snapshot.max_point(); + let edits = new_snapshot + .update( + tab_snapshot, + &[TabEdit { + old: range.clone(), + new: range.clone(), + }], + wrap_width, + &mut line_wrapper, + ) + .await; + (new_snapshot, edits) + }); + + match cx + .background() + .block_with_timeout(Duration::from_millis(5), task) + { + Ok((snapshot, edits)) => { + self.snapshot = snapshot; + self.edits_since_sync = self.edits_since_sync.compose(&edits); + cx.notify(); + } + Err(wrap_task) => { + self.background_task = Some(cx.spawn(|this, mut cx| async move { + let (snapshot, edits) = wrap_task.await; + this.update(&mut cx, |this, cx| { + this.snapshot = snapshot; + this.edits_since_sync = this + .edits_since_sync + .compose(mem::take(&mut this.interpolated_edits).invert()) + .compose(&edits); + this.background_task = None; + this.flush_edits(cx); + cx.notify(); + }); + })); + } + } + } else { + let old_rows = self.snapshot.transforms.summary().output.lines.row + 1; + self.snapshot.transforms = SumTree::new(); + let summary = self.snapshot.tab_snapshot.text_summary(); + if !summary.lines.is_zero() { + self.snapshot + .transforms + .push(Transform::isomorphic(summary), &()); + } + let new_rows = self.snapshot.transforms.summary().output.lines.row + 1; + self.snapshot.interpolated = false; + self.edits_since_sync = self.edits_since_sync.compose(&Patch::new(vec![WrapEdit { + old: 0..old_rows, + new: 0..new_rows, + }])); + } + } + + fn flush_edits(&mut self, cx: &mut ModelContext) { + if !self.snapshot.interpolated { + let mut to_remove_len = 0; + for (tab_snapshot, _) in &self.pending_edits { + if tab_snapshot.version <= self.snapshot.tab_snapshot.version { + to_remove_len += 1; + } else { + break; + } + } + self.pending_edits.drain(..to_remove_len); + } + + if self.pending_edits.is_empty() { + return; + } + + if let Some(wrap_width) = self.wrap_width { + if self.background_task.is_none() { + let pending_edits = self.pending_edits.clone(); + let mut snapshot = self.snapshot.clone(); + let font_cache = cx.font_cache().clone(); + let (font_id, font_size) = self.font; + let update_task = cx.background().spawn(async move { + let mut line_wrapper = font_cache.line_wrapper(font_id, font_size); + + let mut edits = Patch::default(); + for (tab_snapshot, tab_edits) in pending_edits { + let wrap_edits = snapshot + .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper) + .await; + edits = edits.compose(&wrap_edits); + } + (snapshot, edits) + }); + + match cx + .background() + .block_with_timeout(Duration::from_millis(1), update_task) + { + Ok((snapshot, output_edits)) => { + self.snapshot = snapshot; + self.edits_since_sync = self.edits_since_sync.compose(&output_edits); + } + Err(update_task) => { + self.background_task = Some(cx.spawn(|this, mut cx| async move { + let (snapshot, edits) = update_task.await; + this.update(&mut cx, |this, cx| { + this.snapshot = snapshot; + this.edits_since_sync = this + .edits_since_sync + .compose(mem::take(&mut this.interpolated_edits).invert()) + .compose(&edits); + this.background_task = None; + this.flush_edits(cx); + cx.notify(); + }); + })); + } + } + } + } + + let was_interpolated = self.snapshot.interpolated; + let mut to_remove_len = 0; + for (tab_snapshot, edits) in &self.pending_edits { + if tab_snapshot.version <= self.snapshot.tab_snapshot.version { + to_remove_len += 1; + } else { + let interpolated_edits = self.snapshot.interpolate(tab_snapshot.clone(), edits); + self.edits_since_sync = self.edits_since_sync.compose(&interpolated_edits); + self.interpolated_edits = self.interpolated_edits.compose(&interpolated_edits); + } + } + + if !was_interpolated { + self.pending_edits.drain(..to_remove_len); + } + } +} + +impl WrapSnapshot { + fn new(tab_snapshot: TabSnapshot) -> Self { + let mut transforms = SumTree::new(); + let extent = tab_snapshot.text_summary(); + if !extent.lines.is_zero() { + transforms.push(Transform::isomorphic(extent), &()); + } + Self { + transforms, + tab_snapshot, + interpolated: true, + } + } + + pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot { + self.tab_snapshot.buffer_snapshot() + } + + fn interpolate(&mut self, new_tab_snapshot: TabSnapshot, tab_edits: &[TabEdit]) -> Patch { + let mut new_transforms; + if tab_edits.is_empty() { + new_transforms = self.transforms.clone(); + } else { + let mut old_cursor = self.transforms.cursor::(); + + let mut tab_edits_iter = tab_edits.iter().peekable(); + new_transforms = + old_cursor.slice(&tab_edits_iter.peek().unwrap().old.start, Bias::Right, &()); + + while let Some(edit) = tab_edits_iter.next() { + if edit.new.start > TabPoint::from(new_transforms.summary().input.lines) { + let summary = new_tab_snapshot.text_summary_for_range( + TabPoint::from(new_transforms.summary().input.lines)..edit.new.start, + ); + new_transforms.push_or_extend(Transform::isomorphic(summary)); + } + + if !edit.new.is_empty() { + new_transforms.push_or_extend(Transform::isomorphic( + new_tab_snapshot.text_summary_for_range(edit.new.clone()), + )); + } + + old_cursor.seek_forward(&edit.old.end, Bias::Right, &()); + if let Some(next_edit) = tab_edits_iter.peek() { + if next_edit.old.start > old_cursor.end(&()) { + if old_cursor.end(&()) > edit.old.end { + let summary = self + .tab_snapshot + .text_summary_for_range(edit.old.end..old_cursor.end(&())); + new_transforms.push_or_extend(Transform::isomorphic(summary)); + } + + old_cursor.next(&()); + new_transforms.append( + old_cursor.slice(&next_edit.old.start, Bias::Right, &()), + &(), + ); + } + } else { + if old_cursor.end(&()) > edit.old.end { + let summary = self + .tab_snapshot + .text_summary_for_range(edit.old.end..old_cursor.end(&())); + new_transforms.push_or_extend(Transform::isomorphic(summary)); + } + old_cursor.next(&()); + new_transforms.append(old_cursor.suffix(&()), &()); + } + } + } + + let old_snapshot = mem::replace( + self, + WrapSnapshot { + tab_snapshot: new_tab_snapshot, + transforms: new_transforms, + interpolated: true, + }, + ); + self.check_invariants(); + old_snapshot.compute_edits(tab_edits, self) + } + + async fn update( + &mut self, + new_tab_snapshot: TabSnapshot, + tab_edits: &[TabEdit], + wrap_width: f32, + line_wrapper: &mut LineWrapper, + ) -> Patch { + #[derive(Debug)] + struct RowEdit { + old_rows: Range, + new_rows: Range, + } + + let mut tab_edits_iter = tab_edits.iter().peekable(); + let mut row_edits = Vec::new(); + while let Some(edit) = tab_edits_iter.next() { + let mut row_edit = RowEdit { + old_rows: edit.old.start.row()..edit.old.end.row() + 1, + new_rows: edit.new.start.row()..edit.new.end.row() + 1, + }; + + while let Some(next_edit) = tab_edits_iter.peek() { + if next_edit.old.start.row() <= row_edit.old_rows.end { + row_edit.old_rows.end = next_edit.old.end.row() + 1; + row_edit.new_rows.end = next_edit.new.end.row() + 1; + tab_edits_iter.next(); + } else { + break; + } + } + + row_edits.push(row_edit); + } + + let mut new_transforms; + if row_edits.is_empty() { + new_transforms = self.transforms.clone(); + } else { + let mut row_edits = row_edits.into_iter().peekable(); + let mut old_cursor = self.transforms.cursor::(); + + new_transforms = old_cursor.slice( + &TabPoint::new(row_edits.peek().unwrap().old_rows.start, 0), + Bias::Right, + &(), + ); + + while let Some(edit) = row_edits.next() { + if edit.new_rows.start > new_transforms.summary().input.lines.row { + let summary = new_tab_snapshot.text_summary_for_range( + TabPoint(new_transforms.summary().input.lines) + ..TabPoint::new(edit.new_rows.start, 0), + ); + new_transforms.push_or_extend(Transform::isomorphic(summary)); + } + + let mut line = String::new(); + let mut remaining = None; + let mut chunks = new_tab_snapshot.chunks( + TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point(), + false, + Highlights::default(), + ); + let mut edit_transforms = Vec::::new(); + for _ in edit.new_rows.start..edit.new_rows.end { + while let Some(chunk) = + remaining.take().or_else(|| chunks.next().map(|c| c.text)) + { + if let Some(ix) = chunk.find('\n') { + line.push_str(&chunk[..ix + 1]); + remaining = Some(&chunk[ix + 1..]); + break; + } else { + line.push_str(chunk) + } + } + + if line.is_empty() { + break; + } + + let mut prev_boundary_ix = 0; + for boundary in line_wrapper.wrap_line(&line, wrap_width) { + let wrapped = &line[prev_boundary_ix..boundary.ix]; + push_isomorphic(&mut edit_transforms, TextSummary::from(wrapped)); + edit_transforms.push(Transform::wrap(boundary.next_indent)); + prev_boundary_ix = boundary.ix; + } + + if prev_boundary_ix < line.len() { + push_isomorphic( + &mut edit_transforms, + TextSummary::from(&line[prev_boundary_ix..]), + ); + } + + line.clear(); + yield_now().await; + } + + let mut edit_transforms = edit_transforms.into_iter(); + if let Some(transform) = edit_transforms.next() { + new_transforms.push_or_extend(transform); + } + new_transforms.extend(edit_transforms, &()); + + old_cursor.seek_forward(&TabPoint::new(edit.old_rows.end, 0), Bias::Right, &()); + if let Some(next_edit) = row_edits.peek() { + if next_edit.old_rows.start > old_cursor.end(&()).row() { + if old_cursor.end(&()) > TabPoint::new(edit.old_rows.end, 0) { + let summary = self.tab_snapshot.text_summary_for_range( + TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()), + ); + new_transforms.push_or_extend(Transform::isomorphic(summary)); + } + old_cursor.next(&()); + new_transforms.append( + old_cursor.slice( + &TabPoint::new(next_edit.old_rows.start, 0), + Bias::Right, + &(), + ), + &(), + ); + } + } else { + if old_cursor.end(&()) > TabPoint::new(edit.old_rows.end, 0) { + let summary = self.tab_snapshot.text_summary_for_range( + TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()), + ); + new_transforms.push_or_extend(Transform::isomorphic(summary)); + } + old_cursor.next(&()); + new_transforms.append(old_cursor.suffix(&()), &()); + } + } + } + + let old_snapshot = mem::replace( + self, + WrapSnapshot { + tab_snapshot: new_tab_snapshot, + transforms: new_transforms, + interpolated: false, + }, + ); + self.check_invariants(); + old_snapshot.compute_edits(tab_edits, self) + } + + fn compute_edits(&self, tab_edits: &[TabEdit], new_snapshot: &WrapSnapshot) -> Patch { + let mut wrap_edits = Vec::new(); + let mut old_cursor = self.transforms.cursor::(); + let mut new_cursor = new_snapshot.transforms.cursor::(); + for mut tab_edit in tab_edits.iter().cloned() { + tab_edit.old.start.0.column = 0; + tab_edit.old.end.0 += Point::new(1, 0); + tab_edit.new.start.0.column = 0; + tab_edit.new.end.0 += Point::new(1, 0); + + old_cursor.seek(&tab_edit.old.start, Bias::Right, &()); + let mut old_start = old_cursor.start().output.lines; + old_start += tab_edit.old.start.0 - old_cursor.start().input.lines; + + old_cursor.seek(&tab_edit.old.end, Bias::Right, &()); + let mut old_end = old_cursor.start().output.lines; + old_end += tab_edit.old.end.0 - old_cursor.start().input.lines; + + new_cursor.seek(&tab_edit.new.start, Bias::Right, &()); + let mut new_start = new_cursor.start().output.lines; + new_start += tab_edit.new.start.0 - new_cursor.start().input.lines; + + new_cursor.seek(&tab_edit.new.end, Bias::Right, &()); + let mut new_end = new_cursor.start().output.lines; + new_end += tab_edit.new.end.0 - new_cursor.start().input.lines; + + wrap_edits.push(WrapEdit { + old: old_start.row..old_end.row, + new: new_start.row..new_end.row, + }); + } + + consolidate_wrap_edits(&mut wrap_edits); + Patch::new(wrap_edits) + } + + pub fn chunks<'a>( + &'a self, + rows: Range, + language_aware: bool, + highlights: Highlights<'a>, + ) -> WrapChunks<'a> { + let output_start = WrapPoint::new(rows.start, 0); + let output_end = WrapPoint::new(rows.end, 0); + let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(); + transforms.seek(&output_start, Bias::Right, &()); + let mut input_start = TabPoint(transforms.start().1 .0); + if transforms.item().map_or(false, |t| t.is_isomorphic()) { + input_start.0 += output_start.0 - transforms.start().0 .0; + } + let input_end = self + .to_tab_point(output_end) + .min(self.tab_snapshot.max_point()); + WrapChunks { + input_chunks: self.tab_snapshot.chunks( + input_start..input_end, + language_aware, + highlights, + ), + input_chunk: Default::default(), + output_position: output_start, + max_output_row: rows.end, + transforms, + } + } + + pub fn max_point(&self) -> WrapPoint { + WrapPoint(self.transforms.summary().output.lines) + } + + pub fn line_len(&self, row: u32) -> u32 { + let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(); + cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Left, &()); + if cursor + .item() + .map_or(false, |transform| transform.is_isomorphic()) + { + let overshoot = row - cursor.start().0.row(); + let tab_row = cursor.start().1.row() + overshoot; + let tab_line_len = self.tab_snapshot.line_len(tab_row); + if overshoot == 0 { + cursor.start().0.column() + (tab_line_len - cursor.start().1.column()) + } else { + tab_line_len + } + } else { + cursor.start().0.column() + } + } + + pub fn soft_wrap_indent(&self, row: u32) -> Option { + let mut cursor = self.transforms.cursor::(); + cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Right, &()); + cursor.item().and_then(|transform| { + if transform.is_isomorphic() { + None + } else { + Some(transform.summary.output.lines.column) + } + }) + } + + pub fn longest_row(&self) -> u32 { + self.transforms.summary().output.longest_row + } + + pub fn buffer_rows(&self, start_row: u32) -> WrapBufferRows { + let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(); + transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left, &()); + let mut input_row = transforms.start().1.row(); + if transforms.item().map_or(false, |t| t.is_isomorphic()) { + input_row += start_row - transforms.start().0.row(); + } + let soft_wrapped = transforms.item().map_or(false, |t| !t.is_isomorphic()); + let mut input_buffer_rows = self.tab_snapshot.buffer_rows(input_row); + let input_buffer_row = input_buffer_rows.next().unwrap(); + WrapBufferRows { + transforms, + input_buffer_row, + input_buffer_rows, + output_row: start_row, + soft_wrapped, + max_output_row: self.max_point().row(), + } + } + + pub fn to_tab_point(&self, point: WrapPoint) -> TabPoint { + let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(); + cursor.seek(&point, Bias::Right, &()); + let mut tab_point = cursor.start().1 .0; + if cursor.item().map_or(false, |t| t.is_isomorphic()) { + tab_point += point.0 - cursor.start().0 .0; + } + TabPoint(tab_point) + } + + pub fn to_point(&self, point: WrapPoint, bias: Bias) -> Point { + self.tab_snapshot.to_point(self.to_tab_point(point), bias) + } + + pub fn make_wrap_point(&self, point: Point, bias: Bias) -> WrapPoint { + self.tab_point_to_wrap_point(self.tab_snapshot.make_tab_point(point, bias)) + } + + pub fn tab_point_to_wrap_point(&self, point: TabPoint) -> WrapPoint { + let mut cursor = self.transforms.cursor::<(TabPoint, WrapPoint)>(); + cursor.seek(&point, Bias::Right, &()); + WrapPoint(cursor.start().1 .0 + (point.0 - cursor.start().0 .0)) + } + + pub fn clip_point(&self, mut point: WrapPoint, bias: Bias) -> WrapPoint { + if bias == Bias::Left { + let mut cursor = self.transforms.cursor::(); + cursor.seek(&point, Bias::Right, &()); + if cursor.item().map_or(false, |t| !t.is_isomorphic()) { + point = *cursor.start(); + *point.column_mut() -= 1; + } + } + + self.tab_point_to_wrap_point(self.tab_snapshot.clip_point(self.to_tab_point(point), bias)) + } + + pub fn prev_row_boundary(&self, mut point: WrapPoint) -> u32 { + if self.transforms.is_empty() { + return 0; + } + + *point.column_mut() = 0; + + let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(); + cursor.seek(&point, Bias::Right, &()); + if cursor.item().is_none() { + cursor.prev(&()); + } + + while let Some(transform) = cursor.item() { + if transform.is_isomorphic() && cursor.start().1.column() == 0 { + return cmp::min(cursor.end(&()).0.row(), point.row()); + } else { + cursor.prev(&()); + } + } + + unreachable!() + } + + pub fn next_row_boundary(&self, mut point: WrapPoint) -> Option { + point.0 += Point::new(1, 0); + + let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(); + cursor.seek(&point, Bias::Right, &()); + while let Some(transform) = cursor.item() { + if transform.is_isomorphic() && cursor.start().1.column() == 0 { + return Some(cmp::max(cursor.start().0.row(), point.row())); + } else { + cursor.next(&()); + } + } + + None + } + + fn check_invariants(&self) { + #[cfg(test)] + { + assert_eq!( + TabPoint::from(self.transforms.summary().input.lines), + self.tab_snapshot.max_point() + ); + + { + let mut transforms = self.transforms.cursor::<()>().peekable(); + while let Some(transform) = transforms.next() { + if let Some(next_transform) = transforms.peek() { + assert!(transform.is_isomorphic() != next_transform.is_isomorphic()); + } + } + } + + let text = language::Rope::from(self.text().as_str()); + let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0); + let mut expected_buffer_rows = Vec::new(); + let mut prev_tab_row = 0; + for display_row in 0..=self.max_point().row() { + let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0)); + if tab_point.row() == prev_tab_row && display_row != 0 { + expected_buffer_rows.push(None); + } else { + expected_buffer_rows.push(input_buffer_rows.next().unwrap()); + } + + prev_tab_row = tab_point.row(); + assert_eq!(self.line_len(display_row), text.line_len(display_row)); + } + + for start_display_row in 0..expected_buffer_rows.len() { + assert_eq!( + self.buffer_rows(start_display_row as u32) + .collect::>(), + &expected_buffer_rows[start_display_row..], + "invalid buffer_rows({}..)", + start_display_row + ); + } + } + } +} + +impl<'a> Iterator for WrapChunks<'a> { + type Item = Chunk<'a>; + + fn next(&mut self) -> Option { + if self.output_position.row() >= self.max_output_row { + return None; + } + + let transform = self.transforms.item()?; + if let Some(display_text) = transform.display_text { + let mut start_ix = 0; + let mut end_ix = display_text.len(); + let mut summary = transform.summary.output.lines; + + if self.output_position > self.transforms.start().0 { + // Exclude newline starting prior to the desired row. + start_ix = 1; + summary.row = 0; + } else if self.output_position.row() + 1 >= self.max_output_row { + // Exclude soft indentation ending after the desired row. + end_ix = 1; + summary.column = 0; + } + + self.output_position.0 += summary; + self.transforms.next(&()); + return Some(Chunk { + text: &display_text[start_ix..end_ix], + ..self.input_chunk + }); + } + + if self.input_chunk.text.is_empty() { + self.input_chunk = self.input_chunks.next().unwrap(); + } + + let mut input_len = 0; + let transform_end = self.transforms.end(&()).0; + for c in self.input_chunk.text.chars() { + let char_len = c.len_utf8(); + input_len += char_len; + if c == '\n' { + *self.output_position.row_mut() += 1; + *self.output_position.column_mut() = 0; + } else { + *self.output_position.column_mut() += char_len as u32; + } + + if self.output_position >= transform_end { + self.transforms.next(&()); + break; + } + } + + let (prefix, suffix) = self.input_chunk.text.split_at(input_len); + self.input_chunk.text = suffix; + Some(Chunk { + text: prefix, + ..self.input_chunk + }) + } +} + +impl<'a> Iterator for WrapBufferRows<'a> { + type Item = Option; + + fn next(&mut self) -> Option { + if self.output_row > self.max_output_row { + return None; + } + + let buffer_row = self.input_buffer_row; + let soft_wrapped = self.soft_wrapped; + + self.output_row += 1; + self.transforms + .seek_forward(&WrapPoint::new(self.output_row, 0), Bias::Left, &()); + if self.transforms.item().map_or(false, |t| t.is_isomorphic()) { + self.input_buffer_row = self.input_buffer_rows.next().unwrap(); + self.soft_wrapped = false; + } else { + self.soft_wrapped = true; + } + + Some(if soft_wrapped { None } else { buffer_row }) + } +} + +impl Transform { + fn isomorphic(summary: TextSummary) -> Self { + #[cfg(test)] + assert!(!summary.lines.is_zero()); + + Self { + summary: TransformSummary { + input: summary.clone(), + output: summary, + }, + display_text: None, + } + } + + fn wrap(indent: u32) -> Self { + lazy_static! { + static ref WRAP_TEXT: String = { + let mut wrap_text = String::new(); + wrap_text.push('\n'); + wrap_text.extend((0..LineWrapper::MAX_INDENT as usize).map(|_| ' ')); + wrap_text + }; + } + + Self { + summary: TransformSummary { + input: TextSummary::default(), + output: TextSummary { + lines: Point::new(1, indent), + first_line_chars: 0, + last_line_chars: indent, + longest_row: 1, + longest_row_chars: indent, + }, + }, + display_text: Some(&WRAP_TEXT[..1 + indent as usize]), + } + } + + fn is_isomorphic(&self) -> bool { + self.display_text.is_none() + } +} + +impl sum_tree::Item for Transform { + type Summary = TransformSummary; + + fn summary(&self) -> Self::Summary { + self.summary.clone() + } +} + +fn push_isomorphic(transforms: &mut Vec, summary: TextSummary) { + if let Some(last_transform) = transforms.last_mut() { + if last_transform.is_isomorphic() { + last_transform.summary.input += &summary; + last_transform.summary.output += &summary; + return; + } + } + transforms.push(Transform::isomorphic(summary)); +} + +trait SumTreeExt { + fn push_or_extend(&mut self, transform: Transform); +} + +impl SumTreeExt for SumTree { + fn push_or_extend(&mut self, transform: Transform) { + let mut transform = Some(transform); + self.update_last( + |last_transform| { + if last_transform.is_isomorphic() && transform.as_ref().unwrap().is_isomorphic() { + let transform = transform.take().unwrap(); + last_transform.summary.input += &transform.summary.input; + last_transform.summary.output += &transform.summary.output; + } + }, + &(), + ); + + if let Some(transform) = transform { + self.push(transform, &()); + } + } +} + +impl WrapPoint { + pub fn new(row: u32, column: u32) -> Self { + Self(Point::new(row, column)) + } + + pub fn row(self) -> u32 { + self.0.row + } + + pub fn row_mut(&mut self) -> &mut u32 { + &mut self.0.row + } + + pub fn column(self) -> u32 { + self.0.column + } + + pub fn column_mut(&mut self) -> &mut u32 { + &mut self.0.column + } +} + +impl sum_tree::Summary for TransformSummary { + type Context = (); + + fn add_summary(&mut self, other: &Self, _: &()) { + self.input += &other.input; + self.output += &other.output; + } +} + +impl<'a> sum_tree::Dimension<'a, TransformSummary> for TabPoint { + fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { + self.0 += summary.input.lines; + } +} + +impl<'a> sum_tree::SeekTarget<'a, TransformSummary, TransformSummary> for TabPoint { + fn cmp(&self, cursor_location: &TransformSummary, _: &()) -> std::cmp::Ordering { + Ord::cmp(&self.0, &cursor_location.input.lines) + } +} + +impl<'a> sum_tree::Dimension<'a, TransformSummary> for WrapPoint { + fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { + self.0 += summary.output.lines; + } +} + +fn consolidate_wrap_edits(edits: &mut Vec) { + let mut i = 1; + while i < edits.len() { + let edit = edits[i].clone(); + let prev_edit = &mut edits[i - 1]; + if prev_edit.old.end >= edit.old.start { + prev_edit.old.end = edit.old.end; + prev_edit.new.end = edit.new.end; + edits.remove(i); + continue; + } + i += 1; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap}, + MultiBuffer, + }; + use gpui::test::observe; + use rand::prelude::*; + use settings::SettingsStore; + use smol::stream::StreamExt; + use std::{cmp, env, num::NonZeroU32}; + use text::Rope; + + #[gpui::test(iterations = 100)] + async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) { + init_test(cx); + + cx.foreground().set_block_on_ticks(0..=50); + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + + let font_cache = cx.font_cache().clone(); + let font_system = cx.platform().fonts(); + let mut wrap_width = if rng.gen_bool(0.1) { + None + } else { + Some(rng.gen_range(0.0..=1000.0)) + }; + let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap(); + let family_id = font_cache + .load_family(&["Helvetica"], &Default::default()) + .unwrap(); + let font_id = font_cache + .select_font(family_id, &Default::default()) + .unwrap(); + let font_size = 14.0; + + log::info!("Tab size: {}", tab_size); + log::info!("Wrap width: {:?}", wrap_width); + + let buffer = cx.update(|cx| { + if rng.gen() { + MultiBuffer::build_random(&mut rng, cx) + } else { + let len = rng.gen_range(0..10); + let text = util::RandomCharIter::new(&mut rng) + .take(len) + .collect::(); + MultiBuffer::build_simple(&text, cx) + } + }); + let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); + log::info!("Buffer text: {:?}", buffer_snapshot.text()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + log::info!("InlayMap text: {:?}", inlay_snapshot.text()); + let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone()); + log::info!("FoldMap text: {:?}", fold_snapshot.text()); + let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size); + let tabs_snapshot = tab_map.set_max_expansion_column(32); + log::info!("TabMap text: {:?}", tabs_snapshot.text()); + + let mut line_wrapper = LineWrapper::new(font_id, font_size, font_system); + let unwrapped_text = tabs_snapshot.text(); + let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); + + let (wrap_map, _) = + cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx)); + let mut notifications = observe(&wrap_map, cx); + + if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { + notifications.next().await.unwrap(); + } + + let (initial_snapshot, _) = wrap_map.update(cx, |map, cx| { + assert!(!map.is_rewrapping()); + map.sync(tabs_snapshot.clone(), Vec::new(), cx) + }); + + let actual_text = initial_snapshot.text(); + assert_eq!( + actual_text, expected_text, + "unwrapped text is: {:?}", + unwrapped_text + ); + log::info!("Wrapped text: {:?}", actual_text); + + let mut next_inlay_id = 0; + let mut edits = Vec::new(); + for _i in 0..operations { + log::info!("{} ==============================================", _i); + + let mut buffer_edits = Vec::new(); + match rng.gen_range(0..=100) { + 0..=19 => { + wrap_width = if rng.gen_bool(0.2) { + None + } else { + Some(rng.gen_range(0.0..=1000.0)) + }; + log::info!("Setting wrap width to {:?}", wrap_width); + wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); + } + 20..=39 => { + for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) { + let (tabs_snapshot, tab_edits) = + tab_map.sync(fold_snapshot, fold_edits, tab_size); + let (mut snapshot, wrap_edits) = + wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); + snapshot.check_invariants(); + snapshot.verify_chunks(&mut rng); + edits.push((snapshot, wrap_edits)); + } + } + 40..=59 => { + let (inlay_snapshot, inlay_edits) = + inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng); + let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); + let (tabs_snapshot, tab_edits) = + tab_map.sync(fold_snapshot, fold_edits, tab_size); + let (mut snapshot, wrap_edits) = + wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); + snapshot.check_invariants(); + snapshot.verify_chunks(&mut rng); + edits.push((snapshot, wrap_edits)); + } + _ => { + buffer.update(cx, |buffer, cx| { + let subscription = buffer.subscribe(); + let edit_count = rng.gen_range(1..=5); + buffer.randomly_mutate(&mut rng, edit_count, cx); + buffer_snapshot = buffer.snapshot(cx); + buffer_edits.extend(subscription.consume()); + }); + } + } + + log::info!("Buffer text: {:?}", buffer_snapshot.text()); + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot.clone(), buffer_edits); + log::info!("InlayMap text: {:?}", inlay_snapshot.text()); + let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); + log::info!("FoldMap text: {:?}", fold_snapshot.text()); + let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size); + log::info!("TabMap text: {:?}", tabs_snapshot.text()); + + let unwrapped_text = tabs_snapshot.text(); + let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); + let (mut snapshot, wrap_edits) = + wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), tab_edits, cx)); + snapshot.check_invariants(); + snapshot.verify_chunks(&mut rng); + edits.push((snapshot, wrap_edits)); + + if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) { + log::info!("Waiting for wrapping to finish"); + while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { + notifications.next().await.unwrap(); + } + wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); + } + + if !wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { + let (mut wrapped_snapshot, wrap_edits) = + wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx)); + let actual_text = wrapped_snapshot.text(); + let actual_longest_row = wrapped_snapshot.longest_row(); + log::info!("Wrapping finished: {:?}", actual_text); + wrapped_snapshot.check_invariants(); + wrapped_snapshot.verify_chunks(&mut rng); + edits.push((wrapped_snapshot.clone(), wrap_edits)); + assert_eq!( + actual_text, expected_text, + "unwrapped text is: {:?}", + unwrapped_text + ); + + let mut summary = TextSummary::default(); + for (ix, item) in wrapped_snapshot + .transforms + .items(&()) + .into_iter() + .enumerate() + { + summary += &item.summary.output; + log::info!("{} summary: {:?}", ix, item.summary.output,); + } + + if tab_size.get() == 1 + || !wrapped_snapshot + .tab_snapshot + .fold_snapshot + .text() + .contains('\t') + { + let mut expected_longest_rows = Vec::new(); + let mut longest_line_len = -1; + for (row, line) in expected_text.split('\n').enumerate() { + let line_char_count = line.chars().count() as isize; + if line_char_count > longest_line_len { + expected_longest_rows.clear(); + longest_line_len = line_char_count; + } + if line_char_count >= longest_line_len { + expected_longest_rows.push(row as u32); + } + } + + assert!( + expected_longest_rows.contains(&actual_longest_row), + "incorrect longest row {}. expected {:?} with length {}", + actual_longest_row, + expected_longest_rows, + longest_line_len, + ) + } + } + } + + let mut initial_text = Rope::from(initial_snapshot.text().as_str()); + for (snapshot, patch) in edits { + let snapshot_text = Rope::from(snapshot.text().as_str()); + for edit in &patch { + let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0)); + let old_end = initial_text.point_to_offset(cmp::min( + Point::new(edit.new.start + edit.old.len() as u32, 0), + initial_text.max_point(), + )); + let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start, 0)); + let new_end = snapshot_text.point_to_offset(cmp::min( + Point::new(edit.new.end, 0), + snapshot_text.max_point(), + )); + let new_text = snapshot_text + .chunks_in_range(new_start..new_end) + .collect::(); + + initial_text.replace(old_start..old_end, &new_text); + } + assert_eq!(initial_text.to_string(), snapshot_text.to_string()); + } + + if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { + log::info!("Waiting for wrapping to finish"); + while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { + notifications.next().await.unwrap(); + } + } + wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); + } + + fn init_test(cx: &mut gpui::TestAppContext) { + cx.foreground().forbid_parking(); + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + theme::init((), cx); + }); + } + + fn wrap_text( + unwrapped_text: &str, + wrap_width: Option, + line_wrapper: &mut LineWrapper, + ) -> String { + if let Some(wrap_width) = wrap_width { + let mut wrapped_text = String::new(); + for (row, line) in unwrapped_text.split('\n').enumerate() { + if row > 0 { + wrapped_text.push('\n') + } + + let mut prev_ix = 0; + for boundary in line_wrapper.wrap_line(line, wrap_width) { + wrapped_text.push_str(&line[prev_ix..boundary.ix]); + wrapped_text.push('\n'); + wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize)); + prev_ix = boundary.ix; + } + wrapped_text.push_str(&line[prev_ix..]); + } + wrapped_text + } else { + unwrapped_text.to_string() + } + } + + impl WrapSnapshot { + pub fn text(&self) -> String { + self.text_chunks(0).collect() + } + + pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator { + self.chunks( + wrap_row..self.max_point().row() + 1, + false, + Highlights::default(), + ) + .map(|h| h.text) + } + + fn verify_chunks(&mut self, rng: &mut impl Rng) { + for _ in 0..5 { + let mut end_row = rng.gen_range(0..=self.max_point().row()); + let start_row = rng.gen_range(0..=end_row); + end_row += 1; + + let mut expected_text = self.text_chunks(start_row).collect::(); + if expected_text.ends_with('\n') { + expected_text.push('\n'); + } + let mut expected_text = expected_text + .lines() + .take((end_row - start_row) as usize) + .collect::>() + .join("\n"); + if end_row <= self.max_point().row() { + expected_text.push('\n'); + } + + let actual_text = self + .chunks(start_row..end_row, true, Highlights::default()) + .map(|c| c.text) + .collect::(); + assert_eq!( + expected_text, + actual_text, + "chunks != highlighted_chunks for rows {:?}", + start_row..end_row + ); + } + } + } +} diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs new file mode 100644 index 0000000000..4e449bb7f7 --- /dev/null +++ b/crates/editor2/src/editor.rs @@ -0,0 +1,10095 @@ +mod blink_manager; +pub mod display_map; +mod editor_settings; +mod element; +mod inlay_hint_cache; + +mod git; +mod highlight_matching_bracket; +mod hover_popover; +pub mod items; +mod link_go_to_definition; +mod mouse_context_menu; +pub mod movement; +mod persistence; +pub mod scroll; +pub mod selections_collection; + +#[cfg(test)] +mod editor_tests; +#[cfg(any(test, feature = "test-support"))] +pub mod test; + +use ::git::diff::DiffHunk; +use aho_corasick::AhoCorasick; +use anyhow::{anyhow, Context, Result}; +use blink_manager::BlinkManager; +use client::{ClickhouseEvent, Client, Collaborator, ParticipantIndex, TelemetrySettings}; +use clock::{Global, ReplicaId}; +use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; +use convert_case::{Case, Casing}; +use copilot::Copilot; +pub use display_map::DisplayPoint; +use display_map::*; +pub use editor_settings::EditorSettings; +pub use element::{ + Cursor, EditorElement, HighlightedRange, HighlightedRangeLine, LineWithInvisibles, +}; +use futures::FutureExt; +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{ + actions, + color::Color, + elements::*, + executor, + fonts::{self, HighlightStyle, TextStyle}, + geometry::vector::{vec2f, Vector2F}, + impl_actions, + keymap_matcher::KeymapContext, + platform::{CursorStyle, MouseButton}, + serde_json, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, + CursorRegion, Element, Entity, ModelHandle, MouseRegion, Subscription, Task, View, ViewContext, + ViewHandle, WeakViewHandle, WindowContext, +}; +use highlight_matching_bracket::refresh_matching_bracket_highlights; +use hover_popover::{hide_hover, HoverState}; +use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; +pub use items::MAX_TAB_TITLE_LEN; +use itertools::Itertools; +pub use language::{char_kind, CharKind}; +use language::{ + language_settings::{self, all_language_settings, InlayHintSettings}, + markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, + Completion, CursorShape, Diagnostic, DiagnosticSeverity, Documentation, File, IndentKind, + IndentSize, Language, LanguageRegistry, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, + Selection, SelectionGoal, TransactionId, +}; +use link_go_to_definition::{ + hide_link_definition, show_link_definition, GoToDefinitionLink, InlayHighlight, + LinkGoToDefinitionState, +}; +use log::error; +use lsp::LanguageServerId; +use movement::TextLayoutDetails; +use multi_buffer::ToOffsetUtf16; +pub use multi_buffer::{ + Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset, + ToPoint, +}; +use ordered_float::OrderedFloat; +use parking_lot::RwLock; +use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction}; +use rand::{seq::SliceRandom, thread_rng}; +use rpc::proto::{self, PeerId}; +use scroll::{ + autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide, +}; +use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection}; +use serde::{Deserialize, Serialize}; +use settings::SettingsStore; +use smallvec::SmallVec; +use snippet::Snippet; +use std::{ + any::TypeId, + borrow::Cow, + cmp::{self, Ordering, Reverse}, + mem, + num::NonZeroU32, + ops::{ControlFlow, Deref, DerefMut, Range, RangeInclusive}, + path::Path, + sync::Arc, + time::{Duration, Instant}, +}; +pub use sum_tree::Bias; +use sum_tree::TreeMap; +use text::Rope; +use theme::{DiagnosticStyle, Theme, ThemeSettings}; +use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; +use workspace::{ItemNavHistory, SplitDirection, ViewId, Workspace}; + +use crate::git::diff_hunk_to_display; + +const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); +const MAX_LINE_LEN: usize = 1024; +const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; +const MAX_SELECTION_HISTORY_LEN: usize = 1024; +const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); +pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250); +pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); + +pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); + +pub fn render_parsed_markdown( + parsed: &language::ParsedMarkdown, + editor_style: &EditorStyle, + workspace: Option>, + cx: &mut ViewContext, +) -> Text { + enum RenderedMarkdown {} + + let parsed = parsed.clone(); + let view_id = cx.view_id(); + let code_span_background_color = editor_style.document_highlight_read_background; + + let mut region_id = 0; + + Text::new(parsed.text, editor_style.text.clone()) + .with_highlights( + parsed + .highlights + .iter() + .filter_map(|(range, highlight)| { + let highlight = highlight.to_highlight_style(&editor_style.syntax)?; + Some((range.clone(), highlight)) + }) + .collect::>(), + ) + .with_custom_runs(parsed.region_ranges, move |ix, bounds, cx| { + region_id += 1; + let region = parsed.regions[ix].clone(); + + if let Some(link) = region.link { + cx.scene().push_cursor_region(CursorRegion { + bounds, + style: CursorStyle::PointingHand, + }); + cx.scene().push_mouse_region( + MouseRegion::new::<(RenderedMarkdown, Tag)>(view_id, region_id, bounds) + .on_down::(MouseButton::Left, move |_, _, cx| match &link { + markdown::Link::Web { url } => cx.platform().open_url(url), + markdown::Link::Path { path } => { + if let Some(workspace) = &workspace { + _ = workspace.update(cx, |workspace, cx| { + workspace.open_abs_path(path.clone(), false, cx).detach(); + }); + } + } + }), + ); + } + + if region.code { + cx.scene().push_quad(gpui::Quad { + bounds, + background: Some(code_span_background_color), + border: Default::default(), + corner_radii: (2.0).into(), + }); + } + }) + .with_soft_wrap(true) +} + +#[derive(Clone, Deserialize, PartialEq, Default)] +pub struct SelectNext { + #[serde(default)] + pub replace_newest: bool, +} + +#[derive(Clone, Deserialize, PartialEq, Default)] +pub struct SelectPrevious { + #[serde(default)] + pub replace_newest: bool, +} + +#[derive(Clone, Deserialize, PartialEq, Default)] +pub struct SelectAllMatches { + #[serde(default)] + pub replace_newest: bool, +} + +#[derive(Clone, Deserialize, PartialEq)] +pub struct SelectToBeginningOfLine { + #[serde(default)] + stop_at_soft_wraps: bool, +} + +#[derive(Clone, Default, Deserialize, PartialEq)] +pub struct MovePageUp { + #[serde(default)] + center_cursor: bool, +} + +#[derive(Clone, Default, Deserialize, PartialEq)] +pub struct MovePageDown { + #[serde(default)] + center_cursor: bool, +} + +#[derive(Clone, Deserialize, PartialEq)] +pub struct SelectToEndOfLine { + #[serde(default)] + stop_at_soft_wraps: bool, +} + +#[derive(Clone, Deserialize, PartialEq)] +pub struct ToggleCodeActions { + #[serde(default)] + pub deployed_from_indicator: bool, +} + +#[derive(Clone, Default, Deserialize, PartialEq)] +pub struct ConfirmCompletion { + #[serde(default)] + pub item_ix: Option, +} + +#[derive(Clone, Default, Deserialize, PartialEq)] +pub struct ConfirmCodeAction { + #[serde(default)] + pub item_ix: Option, +} + +#[derive(Clone, Default, Deserialize, PartialEq)] +pub struct ToggleComments { + #[serde(default)] + pub advance_downwards: bool, +} + +#[derive(Clone, Default, Deserialize, PartialEq)] +pub struct FoldAt { + pub buffer_row: u32, +} + +#[derive(Clone, Default, Deserialize, PartialEq)] +pub struct UnfoldAt { + pub buffer_row: u32, +} + +#[derive(Clone, Default, Deserialize, PartialEq)] +pub struct GutterHover { + pub hovered: bool, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum InlayId { + Suggestion(usize), + Hint(usize), +} + +impl InlayId { + fn id(&self) -> usize { + match self { + Self::Suggestion(id) => *id, + Self::Hint(id) => *id, + } + } +} + +actions!( + editor, + [ + Cancel, + Backspace, + Delete, + Newline, + NewlineAbove, + NewlineBelow, + GoToDiagnostic, + GoToPrevDiagnostic, + GoToHunk, + GoToPrevHunk, + Indent, + Outdent, + DeleteLine, + DeleteToPreviousWordStart, + DeleteToPreviousSubwordStart, + DeleteToNextWordEnd, + DeleteToNextSubwordEnd, + DeleteToBeginningOfLine, + DeleteToEndOfLine, + CutToEndOfLine, + DuplicateLine, + MoveLineUp, + MoveLineDown, + JoinLines, + SortLinesCaseSensitive, + SortLinesCaseInsensitive, + ReverseLines, + ShuffleLines, + ConvertToUpperCase, + ConvertToLowerCase, + ConvertToTitleCase, + ConvertToSnakeCase, + ConvertToKebabCase, + ConvertToUpperCamelCase, + ConvertToLowerCamelCase, + Transpose, + Cut, + Copy, + Paste, + Undo, + Redo, + MoveUp, + PageUp, + MoveDown, + PageDown, + MoveLeft, + MoveRight, + MoveToPreviousWordStart, + MoveToPreviousSubwordStart, + MoveToNextWordEnd, + MoveToNextSubwordEnd, + MoveToBeginningOfLine, + MoveToEndOfLine, + MoveToStartOfParagraph, + MoveToEndOfParagraph, + MoveToBeginning, + MoveToEnd, + SelectUp, + SelectDown, + SelectLeft, + SelectRight, + SelectToPreviousWordStart, + SelectToPreviousSubwordStart, + SelectToNextWordEnd, + SelectToNextSubwordEnd, + SelectToStartOfParagraph, + SelectToEndOfParagraph, + SelectToBeginning, + SelectToEnd, + SelectAll, + SelectLine, + SplitSelectionIntoLines, + AddSelectionAbove, + AddSelectionBelow, + Tab, + TabPrev, + ShowCharacterPalette, + SelectLargerSyntaxNode, + SelectSmallerSyntaxNode, + GoToDefinition, + GoToDefinitionSplit, + GoToTypeDefinition, + GoToTypeDefinitionSplit, + MoveToEnclosingBracket, + UndoSelection, + RedoSelection, + FindAllReferences, + Rename, + ConfirmRename, + Fold, + UnfoldLines, + FoldSelectedRanges, + ShowCompletions, + OpenExcerpts, + RestartLanguageServer, + Hover, + Format, + ToggleSoftWrap, + ToggleInlayHints, + RevealInFinder, + CopyPath, + CopyRelativePath, + CopyHighlightJson, + ContextMenuFirst, + ContextMenuPrev, + ContextMenuNext, + ContextMenuLast, + ] +); + +impl_actions!( + editor, + [ + SelectNext, + SelectPrevious, + SelectAllMatches, + SelectToBeginningOfLine, + SelectToEndOfLine, + ToggleCodeActions, + MovePageUp, + MovePageDown, + ConfirmCompletion, + ConfirmCodeAction, + ToggleComments, + FoldAt, + UnfoldAt, + GutterHover + ] +); + +enum DocumentHighlightRead {} +enum DocumentHighlightWrite {} +enum InputComposition {} + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum Direction { + Prev, + Next, +} + +pub fn init_settings(cx: &mut AppContext) { + settings::register::(cx); +} + +pub fn init(cx: &mut AppContext) { + init_settings(cx); + cx.add_action(Editor::new_file); + cx.add_action(Editor::new_file_in_direction); + cx.add_action(Editor::cancel); + cx.add_action(Editor::newline); + cx.add_action(Editor::newline_above); + cx.add_action(Editor::newline_below); + cx.add_action(Editor::backspace); + cx.add_action(Editor::delete); + cx.add_action(Editor::tab); + cx.add_action(Editor::tab_prev); + cx.add_action(Editor::indent); + cx.add_action(Editor::outdent); + cx.add_action(Editor::delete_line); + cx.add_action(Editor::join_lines); + cx.add_action(Editor::sort_lines_case_sensitive); + cx.add_action(Editor::sort_lines_case_insensitive); + cx.add_action(Editor::reverse_lines); + cx.add_action(Editor::shuffle_lines); + cx.add_action(Editor::convert_to_upper_case); + cx.add_action(Editor::convert_to_lower_case); + cx.add_action(Editor::convert_to_title_case); + cx.add_action(Editor::convert_to_snake_case); + cx.add_action(Editor::convert_to_kebab_case); + cx.add_action(Editor::convert_to_upper_camel_case); + cx.add_action(Editor::convert_to_lower_camel_case); + cx.add_action(Editor::delete_to_previous_word_start); + cx.add_action(Editor::delete_to_previous_subword_start); + cx.add_action(Editor::delete_to_next_word_end); + cx.add_action(Editor::delete_to_next_subword_end); + cx.add_action(Editor::delete_to_beginning_of_line); + cx.add_action(Editor::delete_to_end_of_line); + cx.add_action(Editor::cut_to_end_of_line); + cx.add_action(Editor::duplicate_line); + cx.add_action(Editor::move_line_up); + cx.add_action(Editor::move_line_down); + cx.add_action(Editor::transpose); + cx.add_action(Editor::cut); + cx.add_action(Editor::copy); + cx.add_action(Editor::paste); + cx.add_action(Editor::undo); + cx.add_action(Editor::redo); + cx.add_action(Editor::move_up); + cx.add_action(Editor::move_page_up); + cx.add_action(Editor::move_down); + cx.add_action(Editor::move_page_down); + cx.add_action(Editor::next_screen); + cx.add_action(Editor::move_left); + cx.add_action(Editor::move_right); + cx.add_action(Editor::move_to_previous_word_start); + cx.add_action(Editor::move_to_previous_subword_start); + cx.add_action(Editor::move_to_next_word_end); + cx.add_action(Editor::move_to_next_subword_end); + cx.add_action(Editor::move_to_beginning_of_line); + cx.add_action(Editor::move_to_end_of_line); + cx.add_action(Editor::move_to_start_of_paragraph); + cx.add_action(Editor::move_to_end_of_paragraph); + cx.add_action(Editor::move_to_beginning); + cx.add_action(Editor::move_to_end); + cx.add_action(Editor::select_up); + cx.add_action(Editor::select_down); + cx.add_action(Editor::select_left); + cx.add_action(Editor::select_right); + cx.add_action(Editor::select_to_previous_word_start); + cx.add_action(Editor::select_to_previous_subword_start); + cx.add_action(Editor::select_to_next_word_end); + cx.add_action(Editor::select_to_next_subword_end); + cx.add_action(Editor::select_to_beginning_of_line); + cx.add_action(Editor::select_to_end_of_line); + cx.add_action(Editor::select_to_start_of_paragraph); + cx.add_action(Editor::select_to_end_of_paragraph); + cx.add_action(Editor::select_to_beginning); + cx.add_action(Editor::select_to_end); + cx.add_action(Editor::select_all); + cx.add_action(Editor::select_all_matches); + cx.add_action(Editor::select_line); + cx.add_action(Editor::split_selection_into_lines); + cx.add_action(Editor::add_selection_above); + cx.add_action(Editor::add_selection_below); + cx.add_action(Editor::select_next); + cx.add_action(Editor::select_previous); + cx.add_action(Editor::toggle_comments); + cx.add_action(Editor::select_larger_syntax_node); + cx.add_action(Editor::select_smaller_syntax_node); + cx.add_action(Editor::move_to_enclosing_bracket); + cx.add_action(Editor::undo_selection); + cx.add_action(Editor::redo_selection); + cx.add_action(Editor::go_to_diagnostic); + cx.add_action(Editor::go_to_prev_diagnostic); + cx.add_action(Editor::go_to_hunk); + cx.add_action(Editor::go_to_prev_hunk); + cx.add_action(Editor::go_to_definition); + cx.add_action(Editor::go_to_definition_split); + cx.add_action(Editor::go_to_type_definition); + cx.add_action(Editor::go_to_type_definition_split); + cx.add_action(Editor::fold); + cx.add_action(Editor::fold_at); + cx.add_action(Editor::unfold_lines); + cx.add_action(Editor::unfold_at); + cx.add_action(Editor::gutter_hover); + cx.add_action(Editor::fold_selected_ranges); + cx.add_action(Editor::show_completions); + cx.add_action(Editor::toggle_code_actions); + cx.add_action(Editor::open_excerpts); + cx.add_action(Editor::toggle_soft_wrap); + cx.add_action(Editor::toggle_inlay_hints); + cx.add_action(Editor::reveal_in_finder); + cx.add_action(Editor::copy_path); + cx.add_action(Editor::copy_relative_path); + cx.add_action(Editor::copy_highlight_json); + cx.add_async_action(Editor::format); + cx.add_action(Editor::restart_language_server); + cx.add_action(Editor::show_character_palette); + cx.add_async_action(Editor::confirm_completion); + cx.add_async_action(Editor::confirm_code_action); + cx.add_async_action(Editor::rename); + cx.add_async_action(Editor::confirm_rename); + cx.add_async_action(Editor::find_all_references); + cx.add_action(Editor::next_copilot_suggestion); + cx.add_action(Editor::previous_copilot_suggestion); + cx.add_action(Editor::copilot_suggest); + cx.add_action(Editor::context_menu_first); + cx.add_action(Editor::context_menu_prev); + cx.add_action(Editor::context_menu_next); + cx.add_action(Editor::context_menu_last); + + hover_popover::init(cx); + scroll::actions::init(cx); + + workspace::register_project_item::(cx); + workspace::register_followable_item::(cx); + workspace::register_deserializable_item::(cx); +} + +trait InvalidationRegion { + fn ranges(&self) -> &[Range]; +} + +#[derive(Clone, Debug, PartialEq)] +pub enum SelectPhase { + Begin { + position: DisplayPoint, + add: bool, + click_count: usize, + }, + BeginColumnar { + position: DisplayPoint, + goal_column: u32, + }, + Extend { + position: DisplayPoint, + click_count: usize, + }, + Update { + position: DisplayPoint, + goal_column: u32, + scroll_position: Vector2F, + }, + End, +} + +#[derive(Clone, Debug)] +pub enum SelectMode { + Character, + Word(Range), + Line(Range), + All, +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum EditorMode { + SingleLine, + AutoHeight { max_lines: usize }, + Full, +} + +#[derive(Clone, Debug)] +pub enum SoftWrap { + None, + EditorWidth, + Column(u32), +} + +#[derive(Clone)] +pub struct EditorStyle { + pub text: TextStyle, + pub line_height_scalar: f32, + pub placeholder_text: Option, + pub theme: theme::Editor, + pub theme_id: usize, +} + +type CompletionId = usize; + +type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor; +type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option; + +type BackgroundHighlight = (fn(&Theme) -> Color, Vec>); +type InlayBackgroundHighlight = (fn(&Theme) -> Color, Vec); + +pub struct Editor { + handle: WeakViewHandle, + buffer: ModelHandle, + display_map: ModelHandle, + pub selections: SelectionsCollection, + pub scroll_manager: ScrollManager, + columnar_selection_tail: Option, + add_selections_state: Option, + select_next_state: Option, + select_prev_state: Option, + selection_history: SelectionHistory, + autoclose_regions: Vec, + snippet_stack: InvalidationStack, + select_larger_syntax_node_stack: Vec]>>, + ime_transaction: Option, + active_diagnostics: Option, + soft_wrap_mode_override: Option, + get_field_editor_theme: Option>, + override_text_style: Option>, + project: Option>, + collaboration_hub: Option>, + focused: bool, + blink_manager: ModelHandle, + pub show_local_selections: bool, + mode: EditorMode, + show_gutter: bool, + show_wrap_guides: Option, + placeholder_text: Option>, + highlighted_rows: Option>, + background_highlights: BTreeMap, + inlay_background_highlights: TreeMap, InlayBackgroundHighlight>, + nav_history: Option, + context_menu: RwLock>, + mouse_context_menu: ViewHandle, + completion_tasks: Vec<(CompletionId, Task>)>, + next_completion_id: CompletionId, + available_code_actions: Option<(ModelHandle, Arc<[CodeAction]>)>, + code_actions_task: Option>, + document_highlights_task: Option>, + pending_rename: Option, + searchable: bool, + cursor_shape: CursorShape, + collapse_matches: bool, + autoindent_mode: Option, + workspace: Option<(WeakViewHandle, i64)>, + keymap_context_layers: BTreeMap, + input_enabled: bool, + read_only: bool, + leader_peer_id: Option, + remote_id: Option, + hover_state: HoverState, + gutter_hovered: bool, + link_go_to_definition_state: LinkGoToDefinitionState, + copilot_state: CopilotState, + inlay_hint_cache: InlayHintCache, + next_inlay_id: usize, + _subscriptions: Vec, + pixel_position_of_newest_cursor: Option, +} + +pub struct EditorSnapshot { + pub mode: EditorMode, + pub show_gutter: bool, + pub display_snapshot: DisplaySnapshot, + pub placeholder_text: Option>, + is_focused: bool, + scroll_anchor: ScrollAnchor, + ongoing_scroll: OngoingScroll, +} + +pub struct RemoteSelection { + pub replica_id: ReplicaId, + pub selection: Selection, + pub cursor_shape: CursorShape, + pub peer_id: PeerId, + pub line_mode: bool, + pub participant_index: Option, +} + +#[derive(Clone, Debug)] +struct SelectionHistoryEntry { + selections: Arc<[Selection]>, + select_next_state: Option, + select_prev_state: Option, + add_selections_state: Option, +} + +enum SelectionHistoryMode { + Normal, + Undoing, + Redoing, +} + +impl Default for SelectionHistoryMode { + fn default() -> Self { + Self::Normal + } +} + +#[derive(Default)] +struct SelectionHistory { + #[allow(clippy::type_complexity)] + selections_by_transaction: + HashMap]>, Option]>>)>, + mode: SelectionHistoryMode, + undo_stack: VecDeque, + redo_stack: VecDeque, +} + +impl SelectionHistory { + fn insert_transaction( + &mut self, + transaction_id: TransactionId, + selections: Arc<[Selection]>, + ) { + self.selections_by_transaction + .insert(transaction_id, (selections, None)); + } + + #[allow(clippy::type_complexity)] + fn transaction( + &self, + transaction_id: TransactionId, + ) -> Option<&(Arc<[Selection]>, Option]>>)> { + self.selections_by_transaction.get(&transaction_id) + } + + #[allow(clippy::type_complexity)] + fn transaction_mut( + &mut self, + transaction_id: TransactionId, + ) -> Option<&mut (Arc<[Selection]>, Option]>>)> { + self.selections_by_transaction.get_mut(&transaction_id) + } + + fn push(&mut self, entry: SelectionHistoryEntry) { + if !entry.selections.is_empty() { + match self.mode { + SelectionHistoryMode::Normal => { + self.push_undo(entry); + self.redo_stack.clear(); + } + SelectionHistoryMode::Undoing => self.push_redo(entry), + SelectionHistoryMode::Redoing => self.push_undo(entry), + } + } + } + + fn push_undo(&mut self, entry: SelectionHistoryEntry) { + if self + .undo_stack + .back() + .map_or(true, |e| e.selections != entry.selections) + { + self.undo_stack.push_back(entry); + if self.undo_stack.len() > MAX_SELECTION_HISTORY_LEN { + self.undo_stack.pop_front(); + } + } + } + + fn push_redo(&mut self, entry: SelectionHistoryEntry) { + if self + .redo_stack + .back() + .map_or(true, |e| e.selections != entry.selections) + { + self.redo_stack.push_back(entry); + if self.redo_stack.len() > MAX_SELECTION_HISTORY_LEN { + self.redo_stack.pop_front(); + } + } + } +} + +#[derive(Clone, Debug)] +struct AddSelectionsState { + above: bool, + stack: Vec, +} + +#[derive(Clone)] +struct SelectNextState { + query: AhoCorasick, + wordwise: bool, + done: bool, +} + +impl std::fmt::Debug for SelectNextState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct(std::any::type_name::()) + .field("wordwise", &self.wordwise) + .field("done", &self.done) + .finish() + } +} + +#[derive(Debug)] +struct AutocloseRegion { + selection_id: usize, + range: Range, + pair: BracketPair, +} + +#[derive(Debug)] +struct SnippetState { + ranges: Vec>>, + active_index: usize, +} + +pub struct RenameState { + pub range: Range, + pub old_name: Arc, + pub editor: ViewHandle, + block_id: BlockId, +} + +struct InvalidationStack(Vec); + +enum ContextMenu { + Completions(CompletionsMenu), + CodeActions(CodeActionsMenu), +} + +impl ContextMenu { + fn select_first( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) -> bool { + if self.visible() { + match self { + ContextMenu::Completions(menu) => menu.select_first(project, cx), + ContextMenu::CodeActions(menu) => menu.select_first(cx), + } + true + } else { + false + } + } + + fn select_prev( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) -> bool { + if self.visible() { + match self { + ContextMenu::Completions(menu) => menu.select_prev(project, cx), + ContextMenu::CodeActions(menu) => menu.select_prev(cx), + } + true + } else { + false + } + } + + fn select_next( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) -> bool { + if self.visible() { + match self { + ContextMenu::Completions(menu) => menu.select_next(project, cx), + ContextMenu::CodeActions(menu) => menu.select_next(cx), + } + true + } else { + false + } + } + + fn select_last( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) -> bool { + if self.visible() { + match self { + ContextMenu::Completions(menu) => menu.select_last(project, cx), + ContextMenu::CodeActions(menu) => menu.select_last(cx), + } + true + } else { + false + } + } + + fn visible(&self) -> bool { + match self { + ContextMenu::Completions(menu) => menu.visible(), + ContextMenu::CodeActions(menu) => menu.visible(), + } + } + + fn render( + &self, + cursor_position: DisplayPoint, + style: EditorStyle, + workspace: Option>, + cx: &mut ViewContext, + ) -> (DisplayPoint, AnyElement) { + match self { + ContextMenu::Completions(menu) => (cursor_position, menu.render(style, workspace, cx)), + ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx), + } + } +} + +#[derive(Clone)] +struct CompletionsMenu { + id: CompletionId, + initial_position: Anchor, + buffer: ModelHandle, + completions: Arc>>, + match_candidates: Arc<[StringMatchCandidate]>, + matches: Arc<[StringMatch]>, + selected_item: usize, + list: UniformListState, +} + +impl CompletionsMenu { + fn select_first( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) { + self.selected_item = 0; + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.attempt_resolve_selected_completion_documentation(project, cx); + cx.notify(); + } + + fn select_prev( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) { + if self.selected_item > 0 { + self.selected_item -= 1; + } else { + self.selected_item = self.matches.len() - 1; + } + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.attempt_resolve_selected_completion_documentation(project, cx); + cx.notify(); + } + + fn select_next( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) { + if self.selected_item + 1 < self.matches.len() { + self.selected_item += 1; + } else { + self.selected_item = 0; + } + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.attempt_resolve_selected_completion_documentation(project, cx); + cx.notify(); + } + + fn select_last( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) { + self.selected_item = self.matches.len() - 1; + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.attempt_resolve_selected_completion_documentation(project, cx); + cx.notify(); + } + + fn pre_resolve_completion_documentation( + &self, + project: Option>, + cx: &mut ViewContext, + ) { + let settings = settings::get::(cx); + if !settings.show_completion_documentation { + return; + } + + let Some(project) = project else { + return; + }; + let client = project.read(cx).client(); + let language_registry = project.read(cx).languages().clone(); + + let is_remote = project.read(cx).is_remote(); + let project_id = project.read(cx).remote_id(); + + let completions = self.completions.clone(); + let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect(); + + cx.spawn(move |this, mut cx| async move { + if is_remote { + let Some(project_id) = project_id else { + log::error!("Remote project without remote_id"); + return; + }; + + for completion_index in completion_indices { + let completions_guard = completions.read(); + let completion = &completions_guard[completion_index]; + if completion.documentation.is_some() { + continue; + } + + let server_id = completion.server_id; + let completion = completion.lsp_completion.clone(); + drop(completions_guard); + + Self::resolve_completion_documentation_remote( + project_id, + server_id, + completions.clone(), + completion_index, + completion, + client.clone(), + language_registry.clone(), + ) + .await; + + _ = this.update(&mut cx, |_, cx| cx.notify()); + } + } else { + for completion_index in completion_indices { + let completions_guard = completions.read(); + let completion = &completions_guard[completion_index]; + if completion.documentation.is_some() { + continue; + } + + let server_id = completion.server_id; + let completion = completion.lsp_completion.clone(); + drop(completions_guard); + + let server = project.read_with(&mut cx, |project, _| { + project.language_server_for_id(server_id) + }); + let Some(server) = server else { + return; + }; + + Self::resolve_completion_documentation_local( + server, + completions.clone(), + completion_index, + completion, + language_registry.clone(), + ) + .await; + + _ = this.update(&mut cx, |_, cx| cx.notify()); + } + } + }) + .detach(); + } + + fn attempt_resolve_selected_completion_documentation( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) { + let settings = settings::get::(cx); + if !settings.show_completion_documentation { + return; + } + + let completion_index = self.matches[self.selected_item].candidate_id; + let Some(project) = project else { + return; + }; + let language_registry = project.read(cx).languages().clone(); + + let completions = self.completions.clone(); + let completions_guard = completions.read(); + let completion = &completions_guard[completion_index]; + if completion.documentation.is_some() { + return; + } + + let server_id = completion.server_id; + let completion = completion.lsp_completion.clone(); + drop(completions_guard); + + if project.read(cx).is_remote() { + let Some(project_id) = project.read(cx).remote_id() else { + log::error!("Remote project without remote_id"); + return; + }; + + let client = project.read(cx).client(); + + cx.spawn(move |this, mut cx| async move { + Self::resolve_completion_documentation_remote( + project_id, + server_id, + completions.clone(), + completion_index, + completion, + client, + language_registry.clone(), + ) + .await; + + _ = this.update(&mut cx, |_, cx| cx.notify()); + }) + .detach(); + } else { + let Some(server) = project.read(cx).language_server_for_id(server_id) else { + return; + }; + + cx.spawn(move |this, mut cx| async move { + Self::resolve_completion_documentation_local( + server, + completions, + completion_index, + completion, + language_registry, + ) + .await; + + _ = this.update(&mut cx, |_, cx| cx.notify()); + }) + .detach(); + } + } + + async fn resolve_completion_documentation_remote( + project_id: u64, + server_id: LanguageServerId, + completions: Arc>>, + completion_index: usize, + completion: lsp::CompletionItem, + client: Arc, + language_registry: Arc, + ) { + let request = proto::ResolveCompletionDocumentation { + project_id, + language_server_id: server_id.0 as u64, + lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(), + }; + + let Some(response) = client + .request(request) + .await + .context("completion documentation resolve proto request") + .log_err() + else { + return; + }; + + if response.text.is_empty() { + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(Documentation::Undocumented); + } + + let documentation = if response.is_markdown { + Documentation::MultiLineMarkdown( + markdown::parse_markdown(&response.text, &language_registry, None).await, + ) + } else if response.text.lines().count() <= 1 { + Documentation::SingleLine(response.text) + } else { + Documentation::MultiLinePlainText(response.text) + }; + + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(documentation); + } + + async fn resolve_completion_documentation_local( + server: Arc, + completions: Arc>>, + completion_index: usize, + completion: lsp::CompletionItem, + language_registry: Arc, + ) { + let can_resolve = server + .capabilities() + .completion_provider + .as_ref() + .and_then(|options| options.resolve_provider) + .unwrap_or(false); + if !can_resolve { + return; + } + + let request = server.request::(completion); + let Some(completion_item) = request.await.log_err() else { + return; + }; + + if let Some(lsp_documentation) = completion_item.documentation { + let documentation = language::prepare_completion_documentation( + &lsp_documentation, + &language_registry, + None, // TODO: Try to reasonably work out which language the completion is for + ) + .await; + + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(documentation); + } else { + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(Documentation::Undocumented); + } + } + + fn visible(&self) -> bool { + !self.matches.is_empty() + } + + fn render( + &self, + style: EditorStyle, + workspace: Option>, + cx: &mut ViewContext, + ) -> AnyElement { + enum CompletionTag {} + + let settings = settings::get::(cx); + let show_completion_documentation = settings.show_completion_documentation; + + let widest_completion_ix = self + .matches + .iter() + .enumerate() + .max_by_key(|(_, mat)| { + let completions = self.completions.read(); + let completion = &completions[mat.candidate_id]; + let documentation = &completion.documentation; + + let mut len = completion.label.text.chars().count(); + if let Some(Documentation::SingleLine(text)) = documentation { + if show_completion_documentation { + len += text.chars().count(); + } + } + + len + }) + .map(|(ix, _)| ix); + + let completions = self.completions.clone(); + let matches = self.matches.clone(); + let selected_item = self.selected_item; + + let list = UniformList::new(self.list.clone(), matches.len(), cx, { + let style = style.clone(); + move |_, range, items, cx| { + let start_ix = range.start; + let completions_guard = completions.read(); + + for (ix, mat) in matches[range].iter().enumerate() { + let item_ix = start_ix + ix; + let candidate_id = mat.candidate_id; + let completion = &completions_guard[candidate_id]; + + let documentation = if show_completion_documentation { + &completion.documentation + } else { + &None + }; + + items.push( + MouseEventHandler::new::( + mat.candidate_id, + cx, + |state, _| { + let item_style = if item_ix == selected_item { + style.autocomplete.selected_item + } else if state.hovered() { + style.autocomplete.hovered_item + } else { + style.autocomplete.item + }; + + let completion_label = + Text::new(completion.label.text.clone(), style.text.clone()) + .with_soft_wrap(false) + .with_highlights( + combine_syntax_and_fuzzy_match_highlights( + &completion.label.text, + style.text.color.into(), + styled_runs_for_code_label( + &completion.label, + &style.syntax, + ), + &mat.positions, + ), + ); + + if let Some(Documentation::SingleLine(text)) = documentation { + Flex::row() + .with_child(completion_label) + .with_children((|| { + let text_style = TextStyle { + color: style.autocomplete.inline_docs_color, + font_size: style.text.font_size + * style.autocomplete.inline_docs_size_percent, + ..style.text.clone() + }; + + let label = Text::new(text.clone(), text_style) + .aligned() + .constrained() + .dynamically(move |constraint, _, _| { + gpui::SizeConstraint { + min: constraint.min, + max: vec2f( + constraint.max.x(), + constraint.min.y(), + ), + } + }); + + if Some(item_ix) == widest_completion_ix { + Some( + label + .contained() + .with_style( + style + .autocomplete + .inline_docs_container, + ) + .into_any(), + ) + } else { + Some(label.flex_float().into_any()) + } + })()) + .into_any() + } else { + completion_label.into_any() + } + .contained() + .with_style(item_style) + .constrained() + .dynamically( + move |constraint, _, _| { + if Some(item_ix) == widest_completion_ix { + constraint + } else { + gpui::SizeConstraint { + min: constraint.min, + max: constraint.min, + } + } + }, + ) + }, + ) + .with_cursor_style(CursorStyle::PointingHand) + .on_down(MouseButton::Left, move |_, this, cx| { + this.confirm_completion( + &ConfirmCompletion { + item_ix: Some(item_ix), + }, + cx, + ) + .map(|task| task.detach()); + }) + .constrained() + .with_min_width(style.autocomplete.completion_min_width) + .with_max_width(style.autocomplete.completion_max_width) + .into_any(), + ); + } + } + }) + .with_width_from_item(widest_completion_ix); + + enum MultiLineDocumentation {} + + Flex::row() + .with_child(list.flex(1., false)) + .with_children({ + let mat = &self.matches[selected_item]; + let completions = self.completions.read(); + let completion = &completions[mat.candidate_id]; + let documentation = &completion.documentation; + + match documentation { + Some(Documentation::MultiLinePlainText(text)) => Some( + Flex::column() + .scrollable::(0, None, cx) + .with_child( + Text::new(text.clone(), style.text.clone()).with_soft_wrap(true), + ) + .contained() + .with_style(style.autocomplete.alongside_docs_container) + .constrained() + .with_max_width(style.autocomplete.alongside_docs_max_width) + .flex(1., false), + ), + + Some(Documentation::MultiLineMarkdown(parsed)) => Some( + Flex::column() + .scrollable::(0, None, cx) + .with_child(render_parsed_markdown::( + parsed, &style, workspace, cx, + )) + .contained() + .with_style(style.autocomplete.alongside_docs_container) + .constrained() + .with_max_width(style.autocomplete.alongside_docs_max_width) + .flex(1., false), + ), + + _ => None, + } + }) + .contained() + .with_style(style.autocomplete.container) + .into_any() + } + + pub async fn filter(&mut self, query: Option<&str>, executor: Arc) { + let mut matches = if let Some(query) = query { + fuzzy::match_strings( + &self.match_candidates, + query, + query.chars().any(|c| c.is_uppercase()), + 100, + &Default::default(), + executor, + ) + .await + } else { + self.match_candidates + .iter() + .enumerate() + .map(|(candidate_id, candidate)| StringMatch { + candidate_id, + score: Default::default(), + positions: Default::default(), + string: candidate.string.clone(), + }) + .collect() + }; + + // Remove all candidates where the query's start does not match the start of any word in the candidate + if let Some(query) = query { + if let Some(query_start) = query.chars().next() { + matches.retain(|string_match| { + split_words(&string_match.string).any(|word| { + // Check that the first codepoint of the word as lowercase matches the first + // codepoint of the query as lowercase + word.chars() + .flat_map(|codepoint| codepoint.to_lowercase()) + .zip(query_start.to_lowercase()) + .all(|(word_cp, query_cp)| word_cp == query_cp) + }) + }); + } + } + + let completions = self.completions.read(); + matches.sort_unstable_by_key(|mat| { + let completion = &completions[mat.candidate_id]; + ( + completion.lsp_completion.sort_text.as_ref(), + Reverse(OrderedFloat(mat.score)), + completion.sort_key(), + ) + }); + drop(completions); + + for mat in &mut matches { + let completions = self.completions.read(); + let filter_start = completions[mat.candidate_id].label.filter_range.start; + for position in &mut mat.positions { + *position += filter_start; + } + } + + self.matches = matches.into(); + self.selected_item = 0; + } +} + +#[derive(Clone)] +struct CodeActionsMenu { + actions: Arc<[CodeAction]>, + buffer: ModelHandle, + selected_item: usize, + list: UniformListState, + deployed_from_indicator: bool, +} + +impl CodeActionsMenu { + fn select_first(&mut self, cx: &mut ViewContext) { + self.selected_item = 0; + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + cx.notify() + } + + fn select_prev(&mut self, cx: &mut ViewContext) { + if self.selected_item > 0 { + self.selected_item -= 1; + } else { + self.selected_item = self.actions.len() - 1; + } + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + cx.notify(); + } + + fn select_next(&mut self, cx: &mut ViewContext) { + if self.selected_item + 1 < self.actions.len() { + self.selected_item += 1; + } else { + self.selected_item = 0; + } + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + cx.notify(); + } + + fn select_last(&mut self, cx: &mut ViewContext) { + self.selected_item = self.actions.len() - 1; + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + cx.notify() + } + + fn visible(&self) -> bool { + !self.actions.is_empty() + } + + fn render( + &self, + mut cursor_position: DisplayPoint, + style: EditorStyle, + cx: &mut ViewContext, + ) -> (DisplayPoint, AnyElement) { + enum ActionTag {} + + let container_style = style.autocomplete.container; + let actions = self.actions.clone(); + let selected_item = self.selected_item; + let element = UniformList::new( + self.list.clone(), + actions.len(), + cx, + move |_, range, items, cx| { + let start_ix = range.start; + for (ix, action) in actions[range].iter().enumerate() { + let item_ix = start_ix + ix; + items.push( + MouseEventHandler::new::(item_ix, cx, |state, _| { + let item_style = if item_ix == selected_item { + style.autocomplete.selected_item + } else if state.hovered() { + style.autocomplete.hovered_item + } else { + style.autocomplete.item + }; + + Text::new(action.lsp_action.title.clone(), style.text.clone()) + .with_soft_wrap(false) + .contained() + .with_style(item_style) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_down(MouseButton::Left, move |_, this, cx| { + let workspace = this + .workspace + .as_ref() + .and_then(|(workspace, _)| workspace.upgrade(cx)); + cx.window_context().defer(move |cx| { + if let Some(workspace) = workspace { + workspace.update(cx, |workspace, cx| { + if let Some(task) = Editor::confirm_code_action( + workspace, + &ConfirmCodeAction { + item_ix: Some(item_ix), + }, + cx, + ) { + task.detach_and_log_err(cx); + } + }); + } + }); + }) + .into_any(), + ); + } + }, + ) + .with_width_from_item( + self.actions + .iter() + .enumerate() + .max_by_key(|(_, action)| action.lsp_action.title.chars().count()) + .map(|(ix, _)| ix), + ) + .contained() + .with_style(container_style) + .into_any(); + + if self.deployed_from_indicator { + *cursor_position.column_mut() = 0; + } + + (cursor_position, element) + } +} + +pub struct CopilotState { + excerpt_id: Option, + pending_refresh: Task>, + pending_cycling_refresh: Task>, + cycled: bool, + completions: Vec, + active_completion_index: usize, + suggestion: Option, +} + +impl Default for CopilotState { + fn default() -> Self { + Self { + excerpt_id: None, + pending_cycling_refresh: Task::ready(Some(())), + pending_refresh: Task::ready(Some(())), + completions: Default::default(), + active_completion_index: 0, + cycled: false, + suggestion: None, + } + } +} + +impl CopilotState { + fn active_completion(&self) -> Option<&copilot::Completion> { + self.completions.get(self.active_completion_index) + } + + fn text_for_active_completion( + &self, + cursor: Anchor, + buffer: &MultiBufferSnapshot, + ) -> Option<&str> { + use language::ToOffset as _; + + let completion = self.active_completion()?; + let excerpt_id = self.excerpt_id?; + let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?; + if excerpt_id != cursor.excerpt_id + || !completion.range.start.is_valid(completion_buffer) + || !completion.range.end.is_valid(completion_buffer) + { + return None; + } + + let mut completion_range = completion.range.to_offset(&completion_buffer); + let prefix_len = Self::common_prefix( + completion_buffer.chars_for_range(completion_range.clone()), + completion.text.chars(), + ); + completion_range.start += prefix_len; + let suffix_len = Self::common_prefix( + completion_buffer.reversed_chars_for_range(completion_range.clone()), + completion.text[prefix_len..].chars().rev(), + ); + completion_range.end = completion_range.end.saturating_sub(suffix_len); + + if completion_range.is_empty() + && completion_range.start == cursor.text_anchor.to_offset(&completion_buffer) + { + Some(&completion.text[prefix_len..completion.text.len() - suffix_len]) + } else { + None + } + } + + fn cycle_completions(&mut self, direction: Direction) { + match direction { + Direction::Prev => { + self.active_completion_index = if self.active_completion_index == 0 { + self.completions.len().saturating_sub(1) + } else { + self.active_completion_index - 1 + }; + } + Direction::Next => { + if self.completions.len() == 0 { + self.active_completion_index = 0 + } else { + self.active_completion_index = + (self.active_completion_index + 1) % self.completions.len(); + } + } + } + } + + fn push_completion(&mut self, new_completion: copilot::Completion) { + for completion in &self.completions { + if completion.text == new_completion.text && completion.range == new_completion.range { + return; + } + } + self.completions.push(new_completion); + } + + fn common_prefix, T2: Iterator>(a: T1, b: T2) -> usize { + a.zip(b) + .take_while(|(a, b)| a == b) + .map(|(a, _)| a.len_utf8()) + .sum() + } +} + +#[derive(Debug)] +struct ActiveDiagnosticGroup { + primary_range: Range, + primary_message: String, + blocks: HashMap, + is_valid: bool, +} + +#[derive(Serialize, Deserialize)] +pub struct ClipboardSelection { + pub len: usize, + pub is_entire_line: bool, + pub first_line_indent: u32, +} + +#[derive(Debug)] +pub struct NavigationData { + cursor_anchor: Anchor, + cursor_position: Point, + scroll_anchor: ScrollAnchor, + scroll_top_row: u32, +} + +pub struct EditorCreated(pub ViewHandle); + +enum GotoDefinitionKind { + Symbol, + Type, +} + +#[derive(Debug, Clone)] +enum InlayHintRefreshReason { + Toggle(bool), + SettingsChange(InlayHintSettings), + NewLinesShown, + BufferEdited(HashSet>), + RefreshRequested, + ExcerptsRemoved(Vec), +} +impl InlayHintRefreshReason { + fn description(&self) -> &'static str { + match self { + Self::Toggle(_) => "toggle", + Self::SettingsChange(_) => "settings change", + Self::NewLinesShown => "new lines shown", + Self::BufferEdited(_) => "buffer edited", + Self::RefreshRequested => "refresh requested", + Self::ExcerptsRemoved(_) => "excerpts removed", + } + } +} + +impl Editor { + pub fn single_line( + field_editor_style: Option>, + cx: &mut ViewContext, + ) -> Self { + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new())); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new(EditorMode::SingleLine, buffer, None, field_editor_style, cx) + } + + pub fn multi_line( + field_editor_style: Option>, + cx: &mut ViewContext, + ) -> Self { + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new())); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new(EditorMode::Full, buffer, None, field_editor_style, cx) + } + + pub fn auto_height( + max_lines: usize, + field_editor_style: Option>, + cx: &mut ViewContext, + ) -> Self { + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new())); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new( + EditorMode::AutoHeight { max_lines }, + buffer, + None, + field_editor_style, + cx, + ) + } + + pub fn for_buffer( + buffer: ModelHandle, + project: Option>, + cx: &mut ViewContext, + ) -> Self { + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new(EditorMode::Full, buffer, project, None, cx) + } + + pub fn for_multibuffer( + buffer: ModelHandle, + project: Option>, + cx: &mut ViewContext, + ) -> Self { + Self::new(EditorMode::Full, buffer, project, None, cx) + } + + pub fn clone(&self, cx: &mut ViewContext) -> Self { + let mut clone = Self::new( + self.mode, + self.buffer.clone(), + self.project.clone(), + self.get_field_editor_theme.clone(), + cx, + ); + self.display_map.update(cx, |display_map, cx| { + let snapshot = display_map.snapshot(cx); + clone.display_map.update(cx, |display_map, cx| { + display_map.set_state(&snapshot, cx); + }); + }); + clone.selections.clone_state(&self.selections); + clone.scroll_manager.clone_state(&self.scroll_manager); + clone.searchable = self.searchable; + clone + } + + fn new( + mode: EditorMode, + buffer: ModelHandle, + project: Option>, + get_field_editor_theme: Option>, + cx: &mut ViewContext, + ) -> Self { + let editor_view_id = cx.view_id(); + let display_map = cx.add_model(|cx| { + let settings = settings::get::(cx); + let style = build_style(settings, get_field_editor_theme.as_deref(), None, cx); + DisplayMap::new( + buffer.clone(), + style.text.font_id, + style.text.font_size, + None, + 2, + 1, + cx, + ) + }); + + let selections = SelectionsCollection::new(display_map.clone(), buffer.clone()); + + let blink_manager = cx.add_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx)); + + let soft_wrap_mode_override = + (mode == EditorMode::SingleLine).then(|| language_settings::SoftWrap::None); + + let mut project_subscriptions = Vec::new(); + if mode == EditorMode::Full { + if let Some(project) = project.as_ref() { + if buffer.read(cx).is_singleton() { + project_subscriptions.push(cx.observe(project, |_, _, cx| { + cx.emit(Event::TitleChanged); + })); + } + project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| { + if let project::Event::RefreshInlayHints = event { + editor.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx); + }; + })); + } + } + + let inlay_hint_settings = inlay_hint_settings( + selections.newest_anchor().head(), + &buffer.read(cx).snapshot(cx), + cx, + ); + + let mut this = Self { + handle: cx.weak_handle(), + buffer: buffer.clone(), + display_map: display_map.clone(), + selections, + scroll_manager: ScrollManager::new(), + columnar_selection_tail: None, + add_selections_state: None, + select_next_state: None, + select_prev_state: None, + selection_history: Default::default(), + autoclose_regions: Default::default(), + snippet_stack: Default::default(), + select_larger_syntax_node_stack: Vec::new(), + ime_transaction: Default::default(), + active_diagnostics: None, + soft_wrap_mode_override, + get_field_editor_theme, + collaboration_hub: project.clone().map(|project| Box::new(project) as _), + project, + focused: false, + blink_manager: blink_manager.clone(), + show_local_selections: true, + mode, + show_gutter: mode == EditorMode::Full, + show_wrap_guides: None, + placeholder_text: None, + highlighted_rows: None, + background_highlights: Default::default(), + inlay_background_highlights: Default::default(), + nav_history: None, + context_menu: RwLock::new(None), + mouse_context_menu: cx + .add_view(|cx| context_menu::ContextMenu::new(editor_view_id, cx)), + completion_tasks: Default::default(), + next_completion_id: 0, + next_inlay_id: 0, + available_code_actions: Default::default(), + code_actions_task: Default::default(), + document_highlights_task: Default::default(), + pending_rename: Default::default(), + searchable: true, + override_text_style: None, + cursor_shape: Default::default(), + autoindent_mode: Some(AutoindentMode::EachLine), + collapse_matches: false, + workspace: None, + keymap_context_layers: Default::default(), + input_enabled: true, + read_only: false, + leader_peer_id: None, + remote_id: None, + hover_state: Default::default(), + link_go_to_definition_state: Default::default(), + copilot_state: Default::default(), + inlay_hint_cache: InlayHintCache::new(inlay_hint_settings), + gutter_hovered: false, + pixel_position_of_newest_cursor: None, + _subscriptions: vec![ + cx.observe(&buffer, Self::on_buffer_changed), + cx.subscribe(&buffer, Self::on_buffer_event), + cx.observe(&display_map, Self::on_display_map_changed), + cx.observe(&blink_manager, |_, _, cx| cx.notify()), + cx.observe_global::(Self::settings_changed), + cx.observe_window_activation(|editor, active, cx| { + editor.blink_manager.update(cx, |blink_manager, cx| { + if active { + blink_manager.enable(cx); + } else { + blink_manager.show_cursor(cx); + blink_manager.disable(cx); + } + }); + }), + ], + }; + + this._subscriptions.extend(project_subscriptions); + + this.end_selection(cx); + this.scroll_manager.show_scrollbar(cx); + + let editor_created_event = EditorCreated(cx.handle()); + cx.emit_global(editor_created_event); + + if mode == EditorMode::Full { + let should_auto_hide_scrollbars = cx.platform().should_auto_hide_scrollbars(); + cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars)); + } + + this.report_editor_event("open", None, cx); + this + } + + pub fn new_file( + workspace: &mut Workspace, + _: &workspace::NewFile, + cx: &mut ViewContext, + ) { + let project = workspace.project().clone(); + if project.read(cx).is_remote() { + cx.propagate_action(); + } else if let Some(buffer) = project + .update(cx, |project, cx| project.create_buffer("", None, cx)) + .log_err() + { + workspace.add_item( + Box::new(cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))), + cx, + ); + } + } + + pub fn new_file_in_direction( + workspace: &mut Workspace, + action: &workspace::NewFileInDirection, + cx: &mut ViewContext, + ) { + let project = workspace.project().clone(); + if project.read(cx).is_remote() { + cx.propagate_action(); + } else if let Some(buffer) = project + .update(cx, |project, cx| project.create_buffer("", None, cx)) + .log_err() + { + workspace.split_item( + action.0, + Box::new(cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))), + cx, + ); + } + } + + pub fn replica_id(&self, cx: &AppContext) -> ReplicaId { + self.buffer.read(cx).replica_id() + } + + pub fn leader_peer_id(&self) -> Option { + self.leader_peer_id + } + + pub fn buffer(&self) -> &ModelHandle { + &self.buffer + } + + fn workspace(&self, cx: &AppContext) -> Option> { + self.workspace.as_ref()?.0.upgrade(cx) + } + + pub fn title<'a>(&self, cx: &'a AppContext) -> Cow<'a, str> { + self.buffer().read(cx).title(cx) + } + + pub fn snapshot(&mut self, cx: &mut WindowContext) -> EditorSnapshot { + EditorSnapshot { + mode: self.mode, + show_gutter: self.show_gutter, + display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)), + scroll_anchor: self.scroll_manager.anchor(), + ongoing_scroll: self.scroll_manager.ongoing_scroll(), + placeholder_text: self.placeholder_text.clone(), + is_focused: self + .handle + .upgrade(cx) + .map_or(false, |handle| handle.is_focused(cx)), + } + } + + pub fn language_at<'a, T: ToOffset>( + &self, + point: T, + cx: &'a AppContext, + ) -> Option> { + self.buffer.read(cx).language_at(point, cx) + } + + pub fn file_at<'a, T: ToOffset>(&self, point: T, cx: &'a AppContext) -> Option> { + self.buffer.read(cx).read(cx).file_at(point).cloned() + } + + pub fn active_excerpt( + &self, + cx: &AppContext, + ) -> Option<(ExcerptId, ModelHandle, Range)> { + self.buffer + .read(cx) + .excerpt_containing(self.selections.newest_anchor().head(), cx) + } + + pub fn style(&self, cx: &AppContext) -> EditorStyle { + build_style( + settings::get::(cx), + self.get_field_editor_theme.as_deref(), + self.override_text_style.as_deref(), + cx, + ) + } + + pub fn mode(&self) -> EditorMode { + self.mode + } + + pub fn collaboration_hub(&self) -> Option<&dyn CollaborationHub> { + self.collaboration_hub.as_deref() + } + + pub fn set_collaboration_hub(&mut self, hub: Box) { + self.collaboration_hub = Some(hub); + } + + pub fn set_placeholder_text( + &mut self, + placeholder_text: impl Into>, + cx: &mut ViewContext, + ) { + self.placeholder_text = Some(placeholder_text.into()); + cx.notify(); + } + + pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext) { + self.cursor_shape = cursor_shape; + cx.notify(); + } + + pub fn set_collapse_matches(&mut self, collapse_matches: bool) { + self.collapse_matches = collapse_matches; + } + + pub fn range_for_match(&self, range: &Range) -> Range { + if self.collapse_matches { + return range.start..range.start; + } + range.clone() + } + + pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext) { + if self.display_map.read(cx).clip_at_line_ends != clip { + self.display_map + .update(cx, |map, _| map.clip_at_line_ends = clip); + } + } + + pub fn set_keymap_context_layer( + &mut self, + context: KeymapContext, + cx: &mut ViewContext, + ) { + self.keymap_context_layers + .insert(TypeId::of::(), context); + cx.notify(); + } + + pub fn remove_keymap_context_layer(&mut self, cx: &mut ViewContext) { + self.keymap_context_layers.remove(&TypeId::of::()); + cx.notify(); + } + + pub fn set_input_enabled(&mut self, input_enabled: bool) { + self.input_enabled = input_enabled; + } + + pub fn set_autoindent(&mut self, autoindent: bool) { + if autoindent { + self.autoindent_mode = Some(AutoindentMode::EachLine); + } else { + self.autoindent_mode = None; + } + } + + pub fn read_only(&self) -> bool { + self.read_only + } + + pub fn set_read_only(&mut self, read_only: bool) { + self.read_only = read_only; + } + + pub fn set_field_editor_style( + &mut self, + style: Option>, + cx: &mut ViewContext, + ) { + self.get_field_editor_theme = style; + cx.notify(); + } + + fn selections_did_change( + &mut self, + local: bool, + old_cursor_position: &Anchor, + cx: &mut ViewContext, + ) { + if self.focused && self.leader_peer_id.is_none() { + self.buffer.update(cx, |buffer, cx| { + buffer.set_active_selections( + &self.selections.disjoint_anchors(), + self.selections.line_mode, + self.cursor_shape, + cx, + ) + }); + } + + let display_map = self + .display_map + .update(cx, |display_map, cx| display_map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + self.add_selections_state = None; + self.select_next_state = None; + self.select_prev_state = None; + self.select_larger_syntax_node_stack.clear(); + self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer); + self.snippet_stack + .invalidate(&self.selections.disjoint_anchors(), buffer); + self.take_rename(false, cx); + + let new_cursor_position = self.selections.newest_anchor().head(); + + self.push_to_nav_history( + old_cursor_position.clone(), + Some(new_cursor_position.to_point(buffer)), + cx, + ); + + if local { + let new_cursor_position = self.selections.newest_anchor().head(); + let mut context_menu = self.context_menu.write(); + let completion_menu = match context_menu.as_ref() { + Some(ContextMenu::Completions(menu)) => Some(menu), + + _ => { + *context_menu = None; + None + } + }; + + if let Some(completion_menu) = completion_menu { + let cursor_position = new_cursor_position.to_offset(buffer); + let (word_range, kind) = + buffer.surrounding_word(completion_menu.initial_position.clone()); + if kind == Some(CharKind::Word) + && word_range.to_inclusive().contains(&cursor_position) + { + let mut completion_menu = completion_menu.clone(); + drop(context_menu); + + let query = Self::completion_query(buffer, cursor_position); + cx.spawn(move |this, mut cx| async move { + completion_menu + .filter(query.as_deref(), cx.background().clone()) + .await; + + this.update(&mut cx, |this, cx| { + let mut context_menu = this.context_menu.write(); + let Some(ContextMenu::Completions(menu)) = context_menu.as_ref() else { + return; + }; + + if menu.id > completion_menu.id { + return; + } + + *context_menu = Some(ContextMenu::Completions(completion_menu)); + drop(context_menu); + cx.notify(); + }) + }) + .detach(); + + self.show_completions(&ShowCompletions, cx); + } else { + drop(context_menu); + self.hide_context_menu(cx); + } + } else { + drop(context_menu); + } + + hide_hover(self, cx); + + if old_cursor_position.to_display_point(&display_map).row() + != new_cursor_position.to_display_point(&display_map).row() + { + self.available_code_actions.take(); + } + self.refresh_code_actions(cx); + self.refresh_document_highlights(cx); + refresh_matching_bracket_highlights(self, cx); + self.discard_copilot_suggestion(cx); + } + + self.blink_manager.update(cx, BlinkManager::pause_blinking); + cx.emit(Event::SelectionsChanged { local }); + cx.notify(); + } + + pub fn change_selections( + &mut self, + autoscroll: Option, + cx: &mut ViewContext, + change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R, + ) -> R { + let old_cursor_position = self.selections.newest_anchor().head(); + self.push_to_selection_history(); + + let (changed, result) = self.selections.change_with(cx, change); + + if changed { + if let Some(autoscroll) = autoscroll { + self.request_autoscroll(autoscroll, cx); + } + self.selections_did_change(true, &old_cursor_position, cx); + } + + result + } + + pub fn edit(&mut self, edits: I, cx: &mut ViewContext) + where + I: IntoIterator, T)>, + S: ToOffset, + T: Into>, + { + if self.read_only { + return; + } + + self.buffer + .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); + } + + pub fn edit_with_autoindent(&mut self, edits: I, cx: &mut ViewContext) + where + I: IntoIterator, T)>, + S: ToOffset, + T: Into>, + { + if self.read_only { + return; + } + + self.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, self.autoindent_mode.clone(), cx) + }); + } + + pub fn edit_with_block_indent( + &mut self, + edits: I, + original_indent_columns: Vec, + cx: &mut ViewContext, + ) where + I: IntoIterator, T)>, + S: ToOffset, + T: Into>, + { + if self.read_only { + return; + } + + self.buffer.update(cx, |buffer, cx| { + buffer.edit( + edits, + Some(AutoindentMode::Block { + original_indent_columns, + }), + cx, + ) + }); + } + + fn select(&mut self, phase: SelectPhase, cx: &mut ViewContext) { + self.hide_context_menu(cx); + + match phase { + SelectPhase::Begin { + position, + add, + click_count, + } => self.begin_selection(position, add, click_count, cx), + SelectPhase::BeginColumnar { + position, + goal_column, + } => self.begin_columnar_selection(position, goal_column, cx), + SelectPhase::Extend { + position, + click_count, + } => self.extend_selection(position, click_count, cx), + SelectPhase::Update { + position, + goal_column, + scroll_position, + } => self.update_selection(position, goal_column, scroll_position, cx), + SelectPhase::End => self.end_selection(cx), + } + } + + fn extend_selection( + &mut self, + position: DisplayPoint, + click_count: usize, + cx: &mut ViewContext, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let tail = self.selections.newest::(cx).tail(); + self.begin_selection(position, false, click_count, cx); + + let position = position.to_offset(&display_map, Bias::Left); + let tail_anchor = display_map.buffer_snapshot.anchor_before(tail); + + let mut pending_selection = self + .selections + .pending_anchor() + .expect("extend_selection not called with pending selection"); + if position >= tail { + pending_selection.start = tail_anchor; + } else { + pending_selection.end = tail_anchor; + pending_selection.reversed = true; + } + + let mut pending_mode = self.selections.pending_mode().unwrap(); + match &mut pending_mode { + SelectMode::Word(range) | SelectMode::Line(range) => *range = tail_anchor..tail_anchor, + _ => {} + } + + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.set_pending(pending_selection, pending_mode) + }); + } + + fn begin_selection( + &mut self, + position: DisplayPoint, + add: bool, + click_count: usize, + cx: &mut ViewContext, + ) { + if !self.focused { + cx.focus_self(); + } + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + let newest_selection = self.selections.newest_anchor().clone(); + let position = display_map.clip_point(position, Bias::Left); + + let start; + let end; + let mode; + let auto_scroll; + match click_count { + 1 => { + start = buffer.anchor_before(position.to_point(&display_map)); + end = start.clone(); + mode = SelectMode::Character; + auto_scroll = true; + } + 2 => { + let range = movement::surrounding_word(&display_map, position); + start = buffer.anchor_before(range.start.to_point(&display_map)); + end = buffer.anchor_before(range.end.to_point(&display_map)); + mode = SelectMode::Word(start.clone()..end.clone()); + auto_scroll = true; + } + 3 => { + let position = display_map + .clip_point(position, Bias::Left) + .to_point(&display_map); + let line_start = display_map.prev_line_boundary(position).0; + let next_line_start = buffer.clip_point( + display_map.next_line_boundary(position).0 + Point::new(1, 0), + Bias::Left, + ); + start = buffer.anchor_before(line_start); + end = buffer.anchor_before(next_line_start); + mode = SelectMode::Line(start.clone()..end.clone()); + auto_scroll = true; + } + _ => { + start = buffer.anchor_before(0); + end = buffer.anchor_before(buffer.len()); + mode = SelectMode::All; + auto_scroll = false; + } + } + + self.change_selections(auto_scroll.then(|| Autoscroll::newest()), cx, |s| { + if !add { + s.clear_disjoint(); + } else if click_count > 1 { + s.delete(newest_selection.id) + } + + s.set_pending_anchor_range(start..end, mode); + }); + } + + fn begin_columnar_selection( + &mut self, + position: DisplayPoint, + goal_column: u32, + cx: &mut ViewContext, + ) { + if !self.focused { + cx.focus_self(); + } + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let tail = self.selections.newest::(cx).tail(); + self.columnar_selection_tail = Some(display_map.buffer_snapshot.anchor_before(tail)); + + self.select_columns( + tail.to_display_point(&display_map), + position, + goal_column, + &display_map, + cx, + ); + } + + fn update_selection( + &mut self, + position: DisplayPoint, + goal_column: u32, + scroll_position: Vector2F, + cx: &mut ViewContext, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + + if let Some(tail) = self.columnar_selection_tail.as_ref() { + let tail = tail.to_display_point(&display_map); + self.select_columns(tail, position, goal_column, &display_map, cx); + } else if let Some(mut pending) = self.selections.pending_anchor() { + let buffer = self.buffer.read(cx).snapshot(cx); + let head; + let tail; + let mode = self.selections.pending_mode().unwrap(); + match &mode { + SelectMode::Character => { + head = position.to_point(&display_map); + tail = pending.tail().to_point(&buffer); + } + SelectMode::Word(original_range) => { + let original_display_range = original_range.start.to_display_point(&display_map) + ..original_range.end.to_display_point(&display_map); + let original_buffer_range = original_display_range.start.to_point(&display_map) + ..original_display_range.end.to_point(&display_map); + if movement::is_inside_word(&display_map, position) + || original_display_range.contains(&position) + { + let word_range = movement::surrounding_word(&display_map, position); + if word_range.start < original_display_range.start { + head = word_range.start.to_point(&display_map); + } else { + head = word_range.end.to_point(&display_map); + } + } else { + head = position.to_point(&display_map); + } + + if head <= original_buffer_range.start { + tail = original_buffer_range.end; + } else { + tail = original_buffer_range.start; + } + } + SelectMode::Line(original_range) => { + let original_range = original_range.to_point(&display_map.buffer_snapshot); + + let position = display_map + .clip_point(position, Bias::Left) + .to_point(&display_map); + let line_start = display_map.prev_line_boundary(position).0; + let next_line_start = buffer.clip_point( + display_map.next_line_boundary(position).0 + Point::new(1, 0), + Bias::Left, + ); + + if line_start < original_range.start { + head = line_start + } else { + head = next_line_start + } + + if head <= original_range.start { + tail = original_range.end; + } else { + tail = original_range.start; + } + } + SelectMode::All => { + return; + } + }; + + if head < tail { + pending.start = buffer.anchor_before(head); + pending.end = buffer.anchor_before(tail); + pending.reversed = true; + } else { + pending.start = buffer.anchor_before(tail); + pending.end = buffer.anchor_before(head); + pending.reversed = false; + } + + self.change_selections(None, cx, |s| { + s.set_pending(pending, mode); + }); + } else { + error!("update_selection dispatched with no pending selection"); + return; + } + + self.set_scroll_position(scroll_position, cx); + cx.notify(); + } + + fn end_selection(&mut self, cx: &mut ViewContext) { + self.columnar_selection_tail.take(); + if self.selections.pending_anchor().is_some() { + let selections = self.selections.all::(cx); + self.change_selections(None, cx, |s| { + s.select(selections); + s.clear_pending(); + }); + } + } + + fn select_columns( + &mut self, + tail: DisplayPoint, + head: DisplayPoint, + goal_column: u32, + display_map: &DisplaySnapshot, + cx: &mut ViewContext, + ) { + let start_row = cmp::min(tail.row(), head.row()); + let end_row = cmp::max(tail.row(), head.row()); + let start_column = cmp::min(tail.column(), goal_column); + let end_column = cmp::max(tail.column(), goal_column); + let reversed = start_column < tail.column(); + + let selection_ranges = (start_row..=end_row) + .filter_map(|row| { + if start_column <= display_map.line_len(row) && !display_map.is_block_line(row) { + let start = display_map + .clip_point(DisplayPoint::new(row, start_column), Bias::Left) + .to_point(display_map); + let end = display_map + .clip_point(DisplayPoint::new(row, end_column), Bias::Right) + .to_point(display_map); + if reversed { + Some(end..start) + } else { + Some(start..end) + } + } else { + None + } + }) + .collect::>(); + + self.change_selections(None, cx, |s| { + s.select_ranges(selection_ranges); + }); + cx.notify(); + } + + pub fn has_pending_nonempty_selection(&self) -> bool { + let pending_nonempty_selection = match self.selections.pending_anchor() { + Some(Selection { start, end, .. }) => start != end, + None => false, + }; + pending_nonempty_selection || self.columnar_selection_tail.is_some() + } + + pub fn has_pending_selection(&self) -> bool { + self.selections.pending_anchor().is_some() || self.columnar_selection_tail.is_some() + } + + pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + if self.take_rename(false, cx).is_some() { + return; + } + + if hide_hover(self, cx) { + return; + } + + if self.hide_context_menu(cx).is_some() { + return; + } + + if self.discard_copilot_suggestion(cx) { + return; + } + + if self.snippet_stack.pop().is_some() { + return; + } + + if self.mode == EditorMode::Full { + if self.active_diagnostics.is_some() { + self.dismiss_diagnostics(cx); + return; + } + + if self.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel()) { + return; + } + } + + cx.propagate_action(); + } + + pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext) { + let text: Arc = text.into(); + + if self.read_only { + return; + } + + let selections = self.selections.all_adjusted(cx); + let mut brace_inserted = false; + let mut edits = Vec::new(); + let mut new_selections = Vec::with_capacity(selections.len()); + let mut new_autoclose_regions = Vec::new(); + let snapshot = self.buffer.read(cx).read(cx); + + for (selection, autoclose_region) in + self.selections_with_autoclose_regions(selections, &snapshot) + { + if let Some(scope) = snapshot.language_scope_at(selection.head()) { + // Determine if the inserted text matches the opening or closing + // bracket of any of this language's bracket pairs. + let mut bracket_pair = None; + let mut is_bracket_pair_start = false; + if !text.is_empty() { + // `text` can be empty when an user is using IME (e.g. Chinese Wubi Simplified) + // and they are removing the character that triggered IME popup. + for (pair, enabled) in scope.brackets() { + if enabled && pair.close && pair.start.ends_with(text.as_ref()) { + bracket_pair = Some(pair.clone()); + is_bracket_pair_start = true; + break; + } else if pair.end.as_str() == text.as_ref() { + bracket_pair = Some(pair.clone()); + break; + } + } + } + + if let Some(bracket_pair) = bracket_pair { + if selection.is_empty() { + if is_bracket_pair_start { + let prefix_len = bracket_pair.start.len() - text.len(); + + // If the inserted text is a suffix of an opening bracket and the + // selection is preceded by the rest of the opening bracket, then + // insert the closing bracket. + let following_text_allows_autoclose = snapshot + .chars_at(selection.start) + .next() + .map_or(true, |c| scope.should_autoclose_before(c)); + let preceding_text_matches_prefix = prefix_len == 0 + || (selection.start.column >= (prefix_len as u32) + && snapshot.contains_str_at( + Point::new( + selection.start.row, + selection.start.column - (prefix_len as u32), + ), + &bracket_pair.start[..prefix_len], + )); + if following_text_allows_autoclose && preceding_text_matches_prefix { + let anchor = snapshot.anchor_before(selection.end); + new_selections.push((selection.map(|_| anchor), text.len())); + new_autoclose_regions.push(( + anchor, + text.len(), + selection.id, + bracket_pair.clone(), + )); + edits.push(( + selection.range(), + format!("{}{}", text, bracket_pair.end).into(), + )); + brace_inserted = true; + continue; + } + } + + if let Some(region) = autoclose_region { + // If the selection is followed by an auto-inserted closing bracket, + // then don't insert that closing bracket again; just move the selection + // past the closing bracket. + let should_skip = selection.end == region.range.end.to_point(&snapshot) + && text.as_ref() == region.pair.end.as_str(); + if should_skip { + let anchor = snapshot.anchor_after(selection.end); + new_selections + .push((selection.map(|_| anchor), region.pair.end.len())); + continue; + } + } + } + // If an opening bracket is 1 character long and is typed while + // text is selected, then surround that text with the bracket pair. + else if is_bracket_pair_start && bracket_pair.start.chars().count() == 1 { + edits.push((selection.start..selection.start, text.clone())); + edits.push(( + selection.end..selection.end, + bracket_pair.end.as_str().into(), + )); + brace_inserted = true; + new_selections.push(( + Selection { + id: selection.id, + start: snapshot.anchor_after(selection.start), + end: snapshot.anchor_before(selection.end), + reversed: selection.reversed, + goal: selection.goal, + }, + 0, + )); + continue; + } + } + } + + // If not handling any auto-close operation, then just replace the selected + // text with the given input and move the selection to the end of the + // newly inserted text. + let anchor = snapshot.anchor_after(selection.end); + new_selections.push((selection.map(|_| anchor), 0)); + edits.push((selection.start..selection.end, text.clone())); + } + + drop(snapshot); + self.transact(cx, |this, cx| { + this.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, this.autoindent_mode.clone(), cx); + }); + + let new_anchor_selections = new_selections.iter().map(|e| &e.0); + let new_selection_deltas = new_selections.iter().map(|e| e.1); + let snapshot = this.buffer.read(cx).read(cx); + let new_selections = resolve_multiple::(new_anchor_selections, &snapshot) + .zip(new_selection_deltas) + .map(|(selection, delta)| Selection { + id: selection.id, + start: selection.start + delta, + end: selection.end + delta, + reversed: selection.reversed, + goal: SelectionGoal::None, + }) + .collect::>(); + + let mut i = 0; + for (position, delta, selection_id, pair) in new_autoclose_regions { + let position = position.to_offset(&snapshot) + delta; + let start = snapshot.anchor_before(position); + let end = snapshot.anchor_after(position); + while let Some(existing_state) = this.autoclose_regions.get(i) { + match existing_state.range.start.cmp(&start, &snapshot) { + Ordering::Less => i += 1, + Ordering::Greater => break, + Ordering::Equal => match end.cmp(&existing_state.range.end, &snapshot) { + Ordering::Less => i += 1, + Ordering::Equal => break, + Ordering::Greater => break, + }, + } + } + this.autoclose_regions.insert( + i, + AutocloseRegion { + selection_id, + range: start..end, + pair, + }, + ); + } + + drop(snapshot); + let had_active_copilot_suggestion = this.has_active_copilot_suggestion(cx); + this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections)); + + if !brace_inserted && settings::get::(cx).use_on_type_format { + if let Some(on_type_format_task) = + this.trigger_on_type_formatting(text.to_string(), cx) + { + on_type_format_task.detach_and_log_err(cx); + } + } + + if had_active_copilot_suggestion { + this.refresh_copilot_suggestions(true, cx); + if !this.has_active_copilot_suggestion(cx) { + this.trigger_completion_on_input(&text, cx); + } + } else { + this.trigger_completion_on_input(&text, cx); + this.refresh_copilot_suggestions(true, cx); + } + }); + } + + pub fn newline(&mut self, _: &Newline, cx: &mut ViewContext) { + self.transact(cx, |this, cx| { + let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = { + let selections = this.selections.all::(cx); + let multi_buffer = this.buffer.read(cx); + let buffer = multi_buffer.snapshot(cx); + selections + .iter() + .map(|selection| { + let start_point = selection.start.to_point(&buffer); + let mut indent = buffer.indent_size_for_line(start_point.row); + indent.len = cmp::min(indent.len, start_point.column); + let start = selection.start; + let end = selection.end; + let is_cursor = start == end; + let language_scope = buffer.language_scope_at(start); + let (comment_delimiter, insert_extra_newline) = if let Some(language) = + &language_scope + { + let leading_whitespace_len = buffer + .reversed_chars_at(start) + .take_while(|c| c.is_whitespace() && *c != '\n') + .map(|c| c.len_utf8()) + .sum::(); + + let trailing_whitespace_len = buffer + .chars_at(end) + .take_while(|c| c.is_whitespace() && *c != '\n') + .map(|c| c.len_utf8()) + .sum::(); + + let insert_extra_newline = + language.brackets().any(|(pair, enabled)| { + let pair_start = pair.start.trim_end(); + let pair_end = pair.end.trim_start(); + + enabled + && pair.newline + && buffer.contains_str_at( + end + trailing_whitespace_len, + pair_end, + ) + && buffer.contains_str_at( + (start - leading_whitespace_len) + .saturating_sub(pair_start.len()), + pair_start, + ) + }); + // Comment extension on newline is allowed only for cursor selections + let comment_delimiter = language.line_comment_prefix().filter(|_| { + let is_comment_extension_enabled = + multi_buffer.settings_at(0, cx).extend_comment_on_newline; + is_cursor && is_comment_extension_enabled + }); + let comment_delimiter = if let Some(delimiter) = comment_delimiter { + buffer + .buffer_line_for_row(start_point.row) + .is_some_and(|(snapshot, range)| { + let mut index_of_first_non_whitespace = 0; + let line_starts_with_comment = snapshot + .chars_for_range(range) + .skip_while(|c| { + let should_skip = c.is_whitespace(); + if should_skip { + index_of_first_non_whitespace += 1; + } + should_skip + }) + .take(delimiter.len()) + .eq(delimiter.chars()); + let cursor_is_placed_after_comment_marker = + index_of_first_non_whitespace + delimiter.len() + <= start_point.column as usize; + line_starts_with_comment + && cursor_is_placed_after_comment_marker + }) + .then(|| delimiter.clone()) + } else { + None + }; + (comment_delimiter, insert_extra_newline) + } else { + (None, false) + }; + + let capacity_for_delimiter = comment_delimiter + .as_deref() + .map(str::len) + .unwrap_or_default(); + let mut new_text = + String::with_capacity(1 + capacity_for_delimiter + indent.len as usize); + new_text.push_str("\n"); + new_text.extend(indent.chars()); + if let Some(delimiter) = &comment_delimiter { + new_text.push_str(&delimiter); + } + if insert_extra_newline { + new_text = new_text.repeat(2); + } + + let anchor = buffer.anchor_after(end); + let new_selection = selection.map(|_| anchor); + ( + (start..end, new_text), + (insert_extra_newline, new_selection), + ) + }) + .unzip() + }; + + this.edit_with_autoindent(edits, cx); + let buffer = this.buffer.read(cx).snapshot(cx); + let new_selections = selection_fixup_info + .into_iter() + .map(|(extra_newline_inserted, new_selection)| { + let mut cursor = new_selection.end.to_point(&buffer); + if extra_newline_inserted { + cursor.row -= 1; + cursor.column = buffer.line_len(cursor.row); + } + new_selection.map(|_| cursor) + }) + .collect(); + + this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections)); + this.refresh_copilot_suggestions(true, cx); + }); + } + + pub fn newline_above(&mut self, _: &NewlineAbove, cx: &mut ViewContext) { + let buffer = self.buffer.read(cx); + let snapshot = buffer.snapshot(cx); + + let mut edits = Vec::new(); + let mut rows = Vec::new(); + let mut rows_inserted = 0; + + for selection in self.selections.all_adjusted(cx) { + let cursor = selection.head(); + let row = cursor.row; + + let start_of_line = snapshot.clip_point(Point::new(row, 0), Bias::Left); + + let newline = "\n".to_string(); + edits.push((start_of_line..start_of_line, newline)); + + rows.push(row + rows_inserted); + rows_inserted += 1; + } + + self.transact(cx, |editor, cx| { + editor.edit(edits, cx); + + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + let mut index = 0; + s.move_cursors_with(|map, _, _| { + let row = rows[index]; + index += 1; + + let point = Point::new(row, 0); + let boundary = map.next_line_boundary(point).1; + let clipped = map.clip_point(boundary, Bias::Left); + + (clipped, SelectionGoal::None) + }); + }); + + let mut indent_edits = Vec::new(); + let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx); + for row in rows { + let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx); + for (row, indent) in indents { + if indent.len == 0 { + continue; + } + + let text = match indent.kind { + IndentKind::Space => " ".repeat(indent.len as usize), + IndentKind::Tab => "\t".repeat(indent.len as usize), + }; + let point = Point::new(row, 0); + indent_edits.push((point..point, text)); + } + } + editor.edit(indent_edits, cx); + }); + } + + pub fn newline_below(&mut self, _: &NewlineBelow, cx: &mut ViewContext) { + let buffer = self.buffer.read(cx); + let snapshot = buffer.snapshot(cx); + + let mut edits = Vec::new(); + let mut rows = Vec::new(); + let mut rows_inserted = 0; + + for selection in self.selections.all_adjusted(cx) { + let cursor = selection.head(); + let row = cursor.row; + + let point = Point::new(row + 1, 0); + let start_of_line = snapshot.clip_point(point, Bias::Left); + + let newline = "\n".to_string(); + edits.push((start_of_line..start_of_line, newline)); + + rows_inserted += 1; + rows.push(row + rows_inserted); + } + + self.transact(cx, |editor, cx| { + editor.edit(edits, cx); + + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + let mut index = 0; + s.move_cursors_with(|map, _, _| { + let row = rows[index]; + index += 1; + + let point = Point::new(row, 0); + let boundary = map.next_line_boundary(point).1; + let clipped = map.clip_point(boundary, Bias::Left); + + (clipped, SelectionGoal::None) + }); + }); + + let mut indent_edits = Vec::new(); + let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx); + for row in rows { + let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx); + for (row, indent) in indents { + if indent.len == 0 { + continue; + } + + let text = match indent.kind { + IndentKind::Space => " ".repeat(indent.len as usize), + IndentKind::Tab => "\t".repeat(indent.len as usize), + }; + let point = Point::new(row, 0); + indent_edits.push((point..point, text)); + } + } + editor.edit(indent_edits, cx); + }); + } + + pub fn insert(&mut self, text: &str, cx: &mut ViewContext) { + self.insert_with_autoindent_mode( + text, + Some(AutoindentMode::Block { + original_indent_columns: Vec::new(), + }), + cx, + ); + } + + fn insert_with_autoindent_mode( + &mut self, + text: &str, + autoindent_mode: Option, + cx: &mut ViewContext, + ) { + if self.read_only { + return; + } + + let text: Arc = text.into(); + self.transact(cx, |this, cx| { + let old_selections = this.selections.all_adjusted(cx); + let selection_anchors = this.buffer.update(cx, |buffer, cx| { + let anchors = { + let snapshot = buffer.read(cx); + old_selections + .iter() + .map(|s| { + let anchor = snapshot.anchor_after(s.head()); + s.map(|_| anchor) + }) + .collect::>() + }; + buffer.edit( + old_selections + .iter() + .map(|s| (s.start..s.end, text.clone())), + autoindent_mode, + cx, + ); + anchors + }); + + this.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_anchors(selection_anchors); + }) + }); + } + + fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext) { + if !settings::get::(cx).show_completions_on_input { + return; + } + + let selection = self.selections.newest_anchor(); + if self + .buffer + .read(cx) + .is_completion_trigger(selection.head(), text, cx) + { + self.show_completions(&ShowCompletions, cx); + } else { + self.hide_context_menu(cx); + } + } + + /// If any empty selections is touching the start of its innermost containing autoclose + /// region, expand it to select the brackets. + fn select_autoclose_pair(&mut self, cx: &mut ViewContext) { + let selections = self.selections.all::(cx); + let buffer = self.buffer.read(cx).read(cx); + let mut new_selections = Vec::new(); + for (mut selection, region) in self.selections_with_autoclose_regions(selections, &buffer) { + if let (Some(region), true) = (region, selection.is_empty()) { + let mut range = region.range.to_offset(&buffer); + if selection.start == range.start { + if range.start >= region.pair.start.len() { + range.start -= region.pair.start.len(); + if buffer.contains_str_at(range.start, ®ion.pair.start) { + if buffer.contains_str_at(range.end, ®ion.pair.end) { + range.end += region.pair.end.len(); + selection.start = range.start; + selection.end = range.end; + } + } + } + } + } + new_selections.push(selection); + } + + drop(buffer); + self.change_selections(None, cx, |selections| selections.select(new_selections)); + } + + /// Iterate the given selections, and for each one, find the smallest surrounding + /// autoclose region. This uses the ordering of the selections and the autoclose + /// regions to avoid repeated comparisons. + fn selections_with_autoclose_regions<'a, D: ToOffset + Clone>( + &'a self, + selections: impl IntoIterator>, + buffer: &'a MultiBufferSnapshot, + ) -> impl Iterator, Option<&'a AutocloseRegion>)> { + let mut i = 0; + let mut regions = self.autoclose_regions.as_slice(); + selections.into_iter().map(move |selection| { + let range = selection.start.to_offset(buffer)..selection.end.to_offset(buffer); + + let mut enclosing = None; + while let Some(pair_state) = regions.get(i) { + if pair_state.range.end.to_offset(buffer) < range.start { + regions = ®ions[i + 1..]; + i = 0; + } else if pair_state.range.start.to_offset(buffer) > range.end { + break; + } else { + if pair_state.selection_id == selection.id { + enclosing = Some(pair_state); + } + i += 1; + } + } + + (selection.clone(), enclosing) + }) + } + + /// Remove any autoclose regions that no longer contain their selection. + fn invalidate_autoclose_regions( + &mut self, + mut selections: &[Selection], + buffer: &MultiBufferSnapshot, + ) { + self.autoclose_regions.retain(|state| { + let mut i = 0; + while let Some(selection) = selections.get(i) { + if selection.end.cmp(&state.range.start, buffer).is_lt() { + selections = &selections[1..]; + continue; + } + if selection.start.cmp(&state.range.end, buffer).is_gt() { + break; + } + if selection.id == state.selection_id { + return true; + } else { + i += 1; + } + } + false + }); + } + + fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option { + let offset = position.to_offset(buffer); + let (word_range, kind) = buffer.surrounding_word(offset); + if offset > word_range.start && kind == Some(CharKind::Word) { + Some( + buffer + .text_for_range(word_range.start..offset) + .collect::(), + ) + } else { + None + } + } + + pub fn toggle_inlay_hints(&mut self, _: &ToggleInlayHints, cx: &mut ViewContext) { + self.refresh_inlay_hints( + InlayHintRefreshReason::Toggle(!self.inlay_hint_cache.enabled), + cx, + ); + } + + pub fn inlay_hints_enabled(&self) -> bool { + self.inlay_hint_cache.enabled + } + + fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut ViewContext) { + if self.project.is_none() || self.mode != EditorMode::Full { + return; + } + + let reason_description = reason.description(); + let (invalidate_cache, required_languages) = match reason { + InlayHintRefreshReason::Toggle(enabled) => { + self.inlay_hint_cache.enabled = enabled; + if enabled { + (InvalidationStrategy::RefreshRequested, None) + } else { + self.inlay_hint_cache.clear(); + self.splice_inlay_hints( + self.visible_inlay_hints(cx) + .iter() + .map(|inlay| inlay.id) + .collect(), + Vec::new(), + cx, + ); + return; + } + } + InlayHintRefreshReason::SettingsChange(new_settings) => { + match self.inlay_hint_cache.update_settings( + &self.buffer, + new_settings, + self.visible_inlay_hints(cx), + cx, + ) { + ControlFlow::Break(Some(InlaySplice { + to_remove, + to_insert, + })) => { + self.splice_inlay_hints(to_remove, to_insert, cx); + return; + } + ControlFlow::Break(None) => return, + ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None), + } + } + InlayHintRefreshReason::ExcerptsRemoved(excerpts_removed) => { + if let Some(InlaySplice { + to_remove, + to_insert, + }) = self.inlay_hint_cache.remove_excerpts(excerpts_removed) + { + self.splice_inlay_hints(to_remove, to_insert, cx); + } + return; + } + InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None), + InlayHintRefreshReason::BufferEdited(buffer_languages) => { + (InvalidationStrategy::BufferEdited, Some(buffer_languages)) + } + InlayHintRefreshReason::RefreshRequested => { + (InvalidationStrategy::RefreshRequested, None) + } + }; + + if let Some(InlaySplice { + to_remove, + to_insert, + }) = self.inlay_hint_cache.spawn_hint_refresh( + reason_description, + self.excerpt_visible_offsets(required_languages.as_ref(), cx), + invalidate_cache, + cx, + ) { + self.splice_inlay_hints(to_remove, to_insert, cx); + } + } + + fn visible_inlay_hints(&self, cx: &ViewContext<'_, '_, Editor>) -> Vec { + self.display_map + .read(cx) + .current_inlays() + .filter(move |inlay| { + Some(inlay.id) != self.copilot_state.suggestion.as_ref().map(|h| h.id) + }) + .cloned() + .collect() + } + + pub fn excerpt_visible_offsets( + &self, + restrict_to_languages: Option<&HashSet>>, + cx: &mut ViewContext<'_, '_, Editor>, + ) -> HashMap, Global, Range)> { + let multi_buffer = self.buffer().read(cx); + let multi_buffer_snapshot = multi_buffer.snapshot(cx); + let multi_buffer_visible_start = self + .scroll_manager + .anchor() + .anchor + .to_point(&multi_buffer_snapshot); + let multi_buffer_visible_end = multi_buffer_snapshot.clip_point( + multi_buffer_visible_start + + Point::new(self.visible_line_count().unwrap_or(0.).ceil() as u32, 0), + Bias::Left, + ); + let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end; + multi_buffer + .range_to_buffer_ranges(multi_buffer_visible_range, cx) + .into_iter() + .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()) + .filter_map(|(buffer_handle, excerpt_visible_range, excerpt_id)| { + let buffer = buffer_handle.read(cx); + let language = buffer.language()?; + if let Some(restrict_to_languages) = restrict_to_languages { + if !restrict_to_languages.contains(language) { + return None; + } + } + Some(( + excerpt_id, + ( + buffer_handle, + buffer.version().clone(), + excerpt_visible_range, + ), + )) + }) + .collect() + } + + pub fn text_layout_details(&self, cx: &WindowContext) -> TextLayoutDetails { + TextLayoutDetails { + font_cache: cx.font_cache().clone(), + text_layout_cache: cx.text_layout_cache().clone(), + editor_style: self.style(cx), + } + } + + fn splice_inlay_hints( + &self, + to_remove: Vec, + to_insert: Vec, + cx: &mut ViewContext, + ) { + self.display_map.update(cx, |display_map, cx| { + display_map.splice_inlays(to_remove, to_insert, cx); + }); + cx.notify(); + } + + fn trigger_on_type_formatting( + &self, + input: String, + cx: &mut ViewContext, + ) -> Option>> { + if input.len() != 1 { + return None; + } + + let project = self.project.as_ref()?; + let position = self.selections.newest_anchor().head(); + let (buffer, buffer_position) = self + .buffer + .read(cx) + .text_anchor_for_position(position.clone(), cx)?; + + // OnTypeFormatting returns a list of edits, no need to pass them between Zed instances, + // hence we do LSP request & edit on host side only — add formats to host's history. + let push_to_lsp_host_history = true; + // If this is not the host, append its history with new edits. + let push_to_client_history = project.read(cx).is_remote(); + + let on_type_formatting = project.update(cx, |project, cx| { + project.on_type_format( + buffer.clone(), + buffer_position, + input, + push_to_lsp_host_history, + cx, + ) + }); + Some(cx.spawn(|editor, mut cx| async move { + if let Some(transaction) = on_type_formatting.await? { + if push_to_client_history { + buffer.update(&mut cx, |buffer, _| { + buffer.push_transaction(transaction, Instant::now()); + }); + } + editor.update(&mut cx, |editor, cx| { + editor.refresh_document_highlights(cx); + })?; + } + Ok(()) + })) + } + + fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext) { + if self.pending_rename.is_some() { + return; + } + + let project = if let Some(project) = self.project.clone() { + project + } else { + return; + }; + + let position = self.selections.newest_anchor().head(); + let (buffer, buffer_position) = if let Some(output) = self + .buffer + .read(cx) + .text_anchor_for_position(position.clone(), cx) + { + output + } else { + return; + }; + + let query = Self::completion_query(&self.buffer.read(cx).read(cx), position.clone()); + let completions = project.update(cx, |project, cx| { + project.completions(&buffer, buffer_position, cx) + }); + + let id = post_inc(&mut self.next_completion_id); + let task = cx.spawn(|this, mut cx| { + async move { + let menu = if let Some(completions) = completions.await.log_err() { + let mut menu = CompletionsMenu { + id, + initial_position: position, + match_candidates: completions + .iter() + .enumerate() + .map(|(id, completion)| { + StringMatchCandidate::new( + id, + completion.label.text[completion.label.filter_range.clone()] + .into(), + ) + }) + .collect(), + buffer, + completions: Arc::new(RwLock::new(completions.into())), + matches: Vec::new().into(), + selected_item: 0, + list: Default::default(), + }; + menu.filter(query.as_deref(), cx.background()).await; + if menu.matches.is_empty() { + None + } else { + _ = this.update(&mut cx, |editor, cx| { + menu.pre_resolve_completion_documentation(editor.project.clone(), cx); + }); + Some(menu) + } + } else { + None + }; + + this.update(&mut cx, |this, cx| { + this.completion_tasks.retain(|(task_id, _)| *task_id > id); + + let mut context_menu = this.context_menu.write(); + match context_menu.as_ref() { + None => {} + + Some(ContextMenu::Completions(prev_menu)) => { + if prev_menu.id > id { + return; + } + } + + _ => return, + } + + if this.focused && menu.is_some() { + let menu = menu.unwrap(); + *context_menu = Some(ContextMenu::Completions(menu)); + drop(context_menu); + this.discard_copilot_suggestion(cx); + cx.notify(); + } else if this.completion_tasks.is_empty() { + // If there are no more completion tasks and the last menu was + // empty, we should hide it. If it was already hidden, we should + // also show the copilot suggestion when available. + drop(context_menu); + if this.hide_context_menu(cx).is_none() { + this.update_visible_copilot_suggestion(cx); + } + } + })?; + + Ok::<_, anyhow::Error>(()) + } + .log_err() + }); + self.completion_tasks.push((id, task)); + } + + pub fn confirm_completion( + &mut self, + action: &ConfirmCompletion, + cx: &mut ViewContext, + ) -> Option>> { + use language::ToOffset as _; + + let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? { + menu + } else { + return None; + }; + + let mat = completions_menu + .matches + .get(action.item_ix.unwrap_or(completions_menu.selected_item))?; + let buffer_handle = completions_menu.buffer; + let completions = completions_menu.completions.read(); + let completion = completions.get(mat.candidate_id)?; + + let snippet; + let text; + if completion.is_snippet() { + snippet = Some(Snippet::parse(&completion.new_text).log_err()?); + text = snippet.as_ref().unwrap().text.clone(); + } else { + snippet = None; + text = completion.new_text.clone(); + }; + let selections = self.selections.all::(cx); + let buffer = buffer_handle.read(cx); + let old_range = completion.old_range.to_offset(buffer); + let old_text = buffer.text_for_range(old_range.clone()).collect::(); + + let newest_selection = self.selections.newest_anchor(); + if newest_selection.start.buffer_id != Some(buffer_handle.read(cx).remote_id()) { + return None; + } + + let lookbehind = newest_selection + .start + .text_anchor + .to_offset(buffer) + .saturating_sub(old_range.start); + let lookahead = old_range + .end + .saturating_sub(newest_selection.end.text_anchor.to_offset(buffer)); + let mut common_prefix_len = old_text + .bytes() + .zip(text.bytes()) + .take_while(|(a, b)| a == b) + .count(); + + let snapshot = self.buffer.read(cx).snapshot(cx); + let mut range_to_replace: Option> = None; + let mut ranges = Vec::new(); + for selection in &selections { + if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) { + let start = selection.start.saturating_sub(lookbehind); + let end = selection.end + lookahead; + if selection.id == newest_selection.id { + range_to_replace = Some( + ((start + common_prefix_len) as isize - selection.start as isize) + ..(end as isize - selection.start as isize), + ); + } + ranges.push(start + common_prefix_len..end); + } else { + common_prefix_len = 0; + ranges.clear(); + ranges.extend(selections.iter().map(|s| { + if s.id == newest_selection.id { + range_to_replace = Some( + old_range.start.to_offset_utf16(&snapshot).0 as isize + - selection.start as isize + ..old_range.end.to_offset_utf16(&snapshot).0 as isize + - selection.start as isize, + ); + old_range.clone() + } else { + s.start..s.end + } + })); + break; + } + } + let text = &text[common_prefix_len..]; + + cx.emit(Event::InputHandled { + utf16_range_to_replace: range_to_replace, + text: text.into(), + }); + + self.transact(cx, |this, cx| { + if let Some(mut snippet) = snippet { + snippet.text = text.to_string(); + for tabstop in snippet.tabstops.iter_mut().flatten() { + tabstop.start -= common_prefix_len as isize; + tabstop.end -= common_prefix_len as isize; + } + + this.insert_snippet(&ranges, snippet, cx).log_err(); + } else { + this.buffer.update(cx, |buffer, cx| { + buffer.edit( + ranges.iter().map(|range| (range.clone(), text)), + this.autoindent_mode.clone(), + cx, + ); + }); + } + + this.refresh_copilot_suggestions(true, cx); + }); + + let project = self.project.clone()?; + let apply_edits = project.update(cx, |project, cx| { + project.apply_additional_edits_for_completion( + buffer_handle, + completion.clone(), + true, + cx, + ) + }); + Some(cx.foreground().spawn(async move { + apply_edits.await?; + Ok(()) + })) + } + + pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext) { + let mut context_menu = self.context_menu.write(); + if matches!(context_menu.as_ref(), Some(ContextMenu::CodeActions(_))) { + *context_menu = None; + cx.notify(); + return; + } + drop(context_menu); + + let deployed_from_indicator = action.deployed_from_indicator; + let mut task = self.code_actions_task.take(); + cx.spawn(|this, mut cx| async move { + while let Some(prev_task) = task { + prev_task.await; + task = this.update(&mut cx, |this, _| this.code_actions_task.take())?; + } + + this.update(&mut cx, |this, cx| { + if this.focused { + if let Some((buffer, actions)) = this.available_code_actions.clone() { + this.completion_tasks.clear(); + this.discard_copilot_suggestion(cx); + *this.context_menu.write() = + Some(ContextMenu::CodeActions(CodeActionsMenu { + buffer, + actions, + selected_item: Default::default(), + list: Default::default(), + deployed_from_indicator, + })); + } + } + })?; + + Ok::<_, anyhow::Error>(()) + }) + .detach_and_log_err(cx); + } + + pub fn confirm_code_action( + workspace: &mut Workspace, + action: &ConfirmCodeAction, + cx: &mut ViewContext, + ) -> Option>> { + let editor = workspace.active_item(cx)?.act_as::(cx)?; + let actions_menu = if let ContextMenu::CodeActions(menu) = + editor.update(cx, |editor, cx| editor.hide_context_menu(cx))? + { + menu + } else { + return None; + }; + let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item); + let action = actions_menu.actions.get(action_ix)?.clone(); + let title = action.lsp_action.title.clone(); + let buffer = actions_menu.buffer; + + let apply_code_actions = workspace.project().clone().update(cx, |project, cx| { + project.apply_code_action(buffer, action, true, cx) + }); + let editor = editor.downgrade(); + Some(cx.spawn(|workspace, cx| async move { + let project_transaction = apply_code_actions.await?; + Self::open_project_transaction(&editor, workspace, project_transaction, title, cx).await + })) + } + + async fn open_project_transaction( + this: &WeakViewHandle, + workspace: WeakViewHandle, + transaction: ProjectTransaction, + title: String, + mut cx: AsyncAppContext, + ) -> Result<()> { + let replica_id = this.read_with(&cx, |this, cx| this.replica_id(cx))?; + + let mut entries = transaction.0.into_iter().collect::>(); + entries.sort_unstable_by_key(|(buffer, _)| { + buffer.read_with(&cx, |buffer, _| buffer.file().map(|f| f.path().clone())) + }); + + // If the project transaction's edits are all contained within this editor, then + // avoid opening a new editor to display them. + + if let Some((buffer, transaction)) = entries.first() { + if entries.len() == 1 { + let excerpt = this.read_with(&cx, |editor, cx| { + editor + .buffer() + .read(cx) + .excerpt_containing(editor.selections.newest_anchor().head(), cx) + })?; + if let Some((_, excerpted_buffer, excerpt_range)) = excerpt { + if excerpted_buffer == *buffer { + let all_edits_within_excerpt = buffer.read_with(&cx, |buffer, _| { + let excerpt_range = excerpt_range.to_offset(buffer); + buffer + .edited_ranges_for_transaction::(transaction) + .all(|range| { + excerpt_range.start <= range.start + && excerpt_range.end >= range.end + }) + }); + + if all_edits_within_excerpt { + return Ok(()); + } + } + } + } + } else { + return Ok(()); + } + + let mut ranges_to_highlight = Vec::new(); + let excerpt_buffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(replica_id).with_title(title); + for (buffer_handle, transaction) in &entries { + let buffer = buffer_handle.read(cx); + ranges_to_highlight.extend( + multibuffer.push_excerpts_with_context_lines( + buffer_handle.clone(), + buffer + .edited_ranges_for_transaction::(transaction) + .collect(), + 1, + cx, + ), + ); + } + multibuffer.push_transaction(entries.iter().map(|(b, t)| (b, t)), cx); + multibuffer + }); + + workspace.update(&mut cx, |workspace, cx| { + let project = workspace.project().clone(); + let editor = + cx.add_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), cx)); + workspace.add_item(Box::new(editor.clone()), cx); + editor.update(cx, |editor, cx| { + editor.highlight_background::( + ranges_to_highlight, + |theme| theme.editor.highlighted_line_background, + cx, + ); + }); + })?; + + Ok(()) + } + + fn refresh_code_actions(&mut self, cx: &mut ViewContext) -> Option<()> { + let project = self.project.clone()?; + let buffer = self.buffer.read(cx); + let newest_selection = self.selections.newest_anchor().clone(); + let (start_buffer, start) = buffer.text_anchor_for_position(newest_selection.start, cx)?; + let (end_buffer, end) = buffer.text_anchor_for_position(newest_selection.end, cx)?; + if start_buffer != end_buffer { + return None; + } + + self.code_actions_task = Some(cx.spawn(|this, mut cx| async move { + cx.background().timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT).await; + + let actions = project + .update(&mut cx, |project, cx| { + project.code_actions(&start_buffer, start..end, cx) + }) + .await; + + this.update(&mut cx, |this, cx| { + this.available_code_actions = actions.log_err().and_then(|actions| { + if actions.is_empty() { + None + } else { + Some((start_buffer, actions.into())) + } + }); + cx.notify(); + }) + .log_err(); + })); + None + } + + fn refresh_document_highlights(&mut self, cx: &mut ViewContext) -> Option<()> { + if self.pending_rename.is_some() { + return None; + } + + let project = self.project.clone()?; + let buffer = self.buffer.read(cx); + let newest_selection = self.selections.newest_anchor().clone(); + let cursor_position = newest_selection.head(); + let (cursor_buffer, cursor_buffer_position) = + buffer.text_anchor_for_position(cursor_position.clone(), cx)?; + let (tail_buffer, _) = buffer.text_anchor_for_position(newest_selection.tail(), cx)?; + if cursor_buffer != tail_buffer { + return None; + } + + self.document_highlights_task = Some(cx.spawn(|this, mut cx| async move { + cx.background() + .timer(DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT) + .await; + + let highlights = project + .update(&mut cx, |project, cx| { + project.document_highlights(&cursor_buffer, cursor_buffer_position, cx) + }) + .await + .log_err(); + + if let Some(highlights) = highlights { + this.update(&mut cx, |this, cx| { + if this.pending_rename.is_some() { + return; + } + + let buffer_id = cursor_position.buffer_id; + let buffer = this.buffer.read(cx); + if !buffer + .text_anchor_for_position(cursor_position, cx) + .map_or(false, |(buffer, _)| buffer == cursor_buffer) + { + return; + } + + let cursor_buffer_snapshot = cursor_buffer.read(cx); + let mut write_ranges = Vec::new(); + let mut read_ranges = Vec::new(); + for highlight in highlights { + for (excerpt_id, excerpt_range) in + buffer.excerpts_for_buffer(&cursor_buffer, cx) + { + let start = highlight + .range + .start + .max(&excerpt_range.context.start, cursor_buffer_snapshot); + let end = highlight + .range + .end + .min(&excerpt_range.context.end, cursor_buffer_snapshot); + if start.cmp(&end, cursor_buffer_snapshot).is_ge() { + continue; + } + + let range = Anchor { + buffer_id, + excerpt_id: excerpt_id.clone(), + text_anchor: start, + }..Anchor { + buffer_id, + excerpt_id, + text_anchor: end, + }; + if highlight.kind == lsp::DocumentHighlightKind::WRITE { + write_ranges.push(range); + } else { + read_ranges.push(range); + } + } + } + + this.highlight_background::( + read_ranges, + |theme| theme.editor.document_highlight_read_background, + cx, + ); + this.highlight_background::( + write_ranges, + |theme| theme.editor.document_highlight_write_background, + cx, + ); + cx.notify(); + }) + .log_err(); + } + })); + None + } + + fn refresh_copilot_suggestions( + &mut self, + debounce: bool, + cx: &mut ViewContext, + ) -> Option<()> { + let copilot = Copilot::global(cx)?; + if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() { + self.clear_copilot_suggestions(cx); + return None; + } + self.update_visible_copilot_suggestion(cx); + + let snapshot = self.buffer.read(cx).snapshot(cx); + let cursor = self.selections.newest_anchor().head(); + if !self.is_copilot_enabled_at(cursor, &snapshot, cx) { + self.clear_copilot_suggestions(cx); + return None; + } + + let (buffer, buffer_position) = + self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; + self.copilot_state.pending_refresh = cx.spawn(|this, mut cx| async move { + if debounce { + cx.background().timer(COPILOT_DEBOUNCE_TIMEOUT).await; + } + + let completions = copilot + .update(&mut cx, |copilot, cx| { + copilot.completions(&buffer, buffer_position, cx) + }) + .await + .log_err() + .into_iter() + .flatten() + .collect_vec(); + + this.update(&mut cx, |this, cx| { + if !completions.is_empty() { + this.copilot_state.cycled = false; + this.copilot_state.pending_cycling_refresh = Task::ready(None); + this.copilot_state.completions.clear(); + this.copilot_state.active_completion_index = 0; + this.copilot_state.excerpt_id = Some(cursor.excerpt_id); + for completion in completions { + this.copilot_state.push_completion(completion); + } + this.update_visible_copilot_suggestion(cx); + } + }) + .log_err()?; + Some(()) + }); + + Some(()) + } + + fn cycle_copilot_suggestions( + &mut self, + direction: Direction, + cx: &mut ViewContext, + ) -> Option<()> { + let copilot = Copilot::global(cx)?; + if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() { + return None; + } + + if self.copilot_state.cycled { + self.copilot_state.cycle_completions(direction); + self.update_visible_copilot_suggestion(cx); + } else { + let cursor = self.selections.newest_anchor().head(); + let (buffer, buffer_position) = + self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; + self.copilot_state.pending_cycling_refresh = cx.spawn(|this, mut cx| async move { + let completions = copilot + .update(&mut cx, |copilot, cx| { + copilot.completions_cycling(&buffer, buffer_position, cx) + }) + .await; + + this.update(&mut cx, |this, cx| { + this.copilot_state.cycled = true; + for completion in completions.log_err().into_iter().flatten() { + this.copilot_state.push_completion(completion); + } + this.copilot_state.cycle_completions(direction); + this.update_visible_copilot_suggestion(cx); + }) + .log_err()?; + + Some(()) + }); + } + + Some(()) + } + + fn copilot_suggest(&mut self, _: &copilot::Suggest, cx: &mut ViewContext) { + if !self.has_active_copilot_suggestion(cx) { + self.refresh_copilot_suggestions(false, cx); + return; + } + + self.update_visible_copilot_suggestion(cx); + } + + fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext) { + if self.has_active_copilot_suggestion(cx) { + self.cycle_copilot_suggestions(Direction::Next, cx); + } else { + let is_copilot_disabled = self.refresh_copilot_suggestions(false, cx).is_none(); + if is_copilot_disabled { + cx.propagate_action(); + } + } + } + + fn previous_copilot_suggestion( + &mut self, + _: &copilot::PreviousSuggestion, + cx: &mut ViewContext, + ) { + if self.has_active_copilot_suggestion(cx) { + self.cycle_copilot_suggestions(Direction::Prev, cx); + } else { + let is_copilot_disabled = self.refresh_copilot_suggestions(false, cx).is_none(); + if is_copilot_disabled { + cx.propagate_action(); + } + } + } + + fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext) -> bool { + if let Some(suggestion) = self.take_active_copilot_suggestion(cx) { + if let Some((copilot, completion)) = + Copilot::global(cx).zip(self.copilot_state.active_completion()) + { + copilot + .update(cx, |copilot, cx| copilot.accept_completion(completion, cx)) + .detach_and_log_err(cx); + + self.report_copilot_event(Some(completion.uuid.clone()), true, cx) + } + cx.emit(Event::InputHandled { + utf16_range_to_replace: None, + text: suggestion.text.to_string().into(), + }); + self.insert_with_autoindent_mode(&suggestion.text.to_string(), None, cx); + cx.notify(); + true + } else { + false + } + } + + fn discard_copilot_suggestion(&mut self, cx: &mut ViewContext) -> bool { + if let Some(suggestion) = self.take_active_copilot_suggestion(cx) { + if let Some(copilot) = Copilot::global(cx) { + copilot + .update(cx, |copilot, cx| { + copilot.discard_completions(&self.copilot_state.completions, cx) + }) + .detach_and_log_err(cx); + + self.report_copilot_event(None, false, cx) + } + + self.display_map.update(cx, |map, cx| { + map.splice_inlays(vec![suggestion.id], Vec::new(), cx) + }); + cx.notify(); + true + } else { + false + } + } + + fn is_copilot_enabled_at( + &self, + location: Anchor, + snapshot: &MultiBufferSnapshot, + cx: &mut ViewContext, + ) -> bool { + let file = snapshot.file_at(location); + let language = snapshot.language_at(location); + let settings = all_language_settings(file, cx); + settings.copilot_enabled(language, file.map(|f| f.path().as_ref())) + } + + fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool { + if let Some(suggestion) = self.copilot_state.suggestion.as_ref() { + let buffer = self.buffer.read(cx).read(cx); + suggestion.position.is_valid(&buffer) + } else { + false + } + } + + fn take_active_copilot_suggestion(&mut self, cx: &mut ViewContext) -> Option { + let suggestion = self.copilot_state.suggestion.take()?; + self.display_map.update(cx, |map, cx| { + map.splice_inlays(vec![suggestion.id], Default::default(), cx); + }); + let buffer = self.buffer.read(cx).read(cx); + + if suggestion.position.is_valid(&buffer) { + Some(suggestion) + } else { + None + } + } + + fn update_visible_copilot_suggestion(&mut self, cx: &mut ViewContext) { + let snapshot = self.buffer.read(cx).snapshot(cx); + let selection = self.selections.newest_anchor(); + let cursor = selection.head(); + + if self.context_menu.read().is_some() + || !self.completion_tasks.is_empty() + || selection.start != selection.end + { + self.discard_copilot_suggestion(cx); + } else if let Some(text) = self + .copilot_state + .text_for_active_completion(cursor, &snapshot) + { + let text = Rope::from(text); + let mut to_remove = Vec::new(); + if let Some(suggestion) = self.copilot_state.suggestion.take() { + to_remove.push(suggestion.id); + } + + let suggestion_inlay = + Inlay::suggestion(post_inc(&mut self.next_inlay_id), cursor, text); + self.copilot_state.suggestion = Some(suggestion_inlay.clone()); + self.display_map.update(cx, move |map, cx| { + map.splice_inlays(to_remove, vec![suggestion_inlay], cx) + }); + cx.notify(); + } else { + self.discard_copilot_suggestion(cx); + } + } + + fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext) { + self.copilot_state = Default::default(); + self.discard_copilot_suggestion(cx); + } + + pub fn render_code_actions_indicator( + &self, + style: &EditorStyle, + is_active: bool, + cx: &mut ViewContext, + ) -> Option> { + if self.available_code_actions.is_some() { + enum CodeActions {} + Some( + MouseEventHandler::new::(0, cx, |state, _| { + Svg::new("icons/bolt.svg").with_color( + style + .code_actions + .indicator + .in_state(is_active) + .style_for(state) + .color, + ) + }) + .with_cursor_style(CursorStyle::PointingHand) + .with_padding(Padding::uniform(3.)) + .on_down(MouseButton::Left, |_, this, cx| { + this.toggle_code_actions( + &ToggleCodeActions { + deployed_from_indicator: true, + }, + cx, + ); + }) + .into_any(), + ) + } else { + None + } + } + + pub fn render_fold_indicators( + &self, + fold_data: Vec>, + style: &EditorStyle, + gutter_hovered: bool, + line_height: f32, + gutter_margin: f32, + cx: &mut ViewContext, + ) -> Vec>> { + enum FoldIndicators {} + + let style = style.folds.clone(); + + fold_data + .iter() + .enumerate() + .map(|(ix, fold_data)| { + fold_data + .map(|(fold_status, buffer_row, active)| { + (active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| { + MouseEventHandler::new::( + ix as usize, + cx, + |mouse_state, _| { + Svg::new(match fold_status { + FoldStatus::Folded => style.folded_icon.clone(), + FoldStatus::Foldable => style.foldable_icon.clone(), + }) + .with_color( + style + .indicator + .in_state(fold_status == FoldStatus::Folded) + .style_for(mouse_state) + .color, + ) + .constrained() + .with_width(gutter_margin * style.icon_margin_scale) + .aligned() + .constrained() + .with_height(line_height) + .with_width(gutter_margin) + .aligned() + }, + ) + .with_cursor_style(CursorStyle::PointingHand) + .with_padding(Padding::uniform(3.)) + .on_click(MouseButton::Left, { + move |_, editor, cx| match fold_status { + FoldStatus::Folded => { + editor.unfold_at(&UnfoldAt { buffer_row }, cx); + } + FoldStatus::Foldable => { + editor.fold_at(&FoldAt { buffer_row }, cx); + } + } + }) + .into_any() + }) + }) + .flatten() + }) + .collect() + } + + pub fn context_menu_visible(&self) -> bool { + self.context_menu + .read() + .as_ref() + .map_or(false, |menu| menu.visible()) + } + + pub fn render_context_menu( + &self, + cursor_position: DisplayPoint, + style: EditorStyle, + cx: &mut ViewContext, + ) -> Option<(DisplayPoint, AnyElement)> { + self.context_menu.read().as_ref().map(|menu| { + menu.render( + cursor_position, + style, + self.workspace.as_ref().map(|(w, _)| w.clone()), + cx, + ) + }) + } + + fn hide_context_menu(&mut self, cx: &mut ViewContext) -> Option { + cx.notify(); + self.completion_tasks.clear(); + let context_menu = self.context_menu.write().take(); + if context_menu.is_some() { + self.update_visible_copilot_suggestion(cx); + } + context_menu + } + + pub fn insert_snippet( + &mut self, + insertion_ranges: &[Range], + snippet: Snippet, + cx: &mut ViewContext, + ) -> Result<()> { + let tabstops = self.buffer.update(cx, |buffer, cx| { + let snippet_text: Arc = snippet.text.clone().into(); + buffer.edit( + insertion_ranges + .iter() + .cloned() + .map(|range| (range, snippet_text.clone())), + Some(AutoindentMode::EachLine), + cx, + ); + + let snapshot = &*buffer.read(cx); + let snippet = &snippet; + snippet + .tabstops + .iter() + .map(|tabstop| { + let mut tabstop_ranges = tabstop + .iter() + .flat_map(|tabstop_range| { + let mut delta = 0_isize; + insertion_ranges.iter().map(move |insertion_range| { + let insertion_start = insertion_range.start as isize + delta; + delta += + snippet.text.len() as isize - insertion_range.len() as isize; + + let start = snapshot.anchor_before( + (insertion_start + tabstop_range.start) as usize, + ); + let end = snapshot + .anchor_after((insertion_start + tabstop_range.end) as usize); + start..end + }) + }) + .collect::>(); + tabstop_ranges.sort_unstable_by(|a, b| a.start.cmp(&b.start, snapshot)); + tabstop_ranges + }) + .collect::>() + }); + + if let Some(tabstop) = tabstops.first() { + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges(tabstop.iter().cloned()); + }); + self.snippet_stack.push(SnippetState { + active_index: 0, + ranges: tabstops, + }); + } + + Ok(()) + } + + pub fn move_to_next_snippet_tabstop(&mut self, cx: &mut ViewContext) -> bool { + self.move_to_snippet_tabstop(Bias::Right, cx) + } + + pub fn move_to_prev_snippet_tabstop(&mut self, cx: &mut ViewContext) -> bool { + self.move_to_snippet_tabstop(Bias::Left, cx) + } + + pub fn move_to_snippet_tabstop(&mut self, bias: Bias, cx: &mut ViewContext) -> bool { + if let Some(mut snippet) = self.snippet_stack.pop() { + match bias { + Bias::Left => { + if snippet.active_index > 0 { + snippet.active_index -= 1; + } else { + self.snippet_stack.push(snippet); + return false; + } + } + Bias::Right => { + if snippet.active_index + 1 < snippet.ranges.len() { + snippet.active_index += 1; + } else { + self.snippet_stack.push(snippet); + return false; + } + } + } + if let Some(current_ranges) = snippet.ranges.get(snippet.active_index) { + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_anchor_ranges(current_ranges.iter().cloned()) + }); + // If snippet state is not at the last tabstop, push it back on the stack + if snippet.active_index + 1 < snippet.ranges.len() { + self.snippet_stack.push(snippet); + } + return true; + } + } + + false + } + + pub fn clear(&mut self, cx: &mut ViewContext) { + self.transact(cx, |this, cx| { + this.select_all(&SelectAll, cx); + this.insert("", cx); + }); + } + + pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext) { + self.transact(cx, |this, cx| { + this.select_autoclose_pair(cx); + let mut selections = this.selections.all::(cx); + if !this.selections.line_mode { + let display_map = this.display_map.update(cx, |map, cx| map.snapshot(cx)); + for selection in &mut selections { + if selection.is_empty() { + let old_head = selection.head(); + let mut new_head = + movement::left(&display_map, old_head.to_display_point(&display_map)) + .to_point(&display_map); + if let Some((buffer, line_buffer_range)) = display_map + .buffer_snapshot + .buffer_line_for_row(old_head.row) + { + let indent_size = + buffer.indent_size_for_line(line_buffer_range.start.row); + let indent_len = match indent_size.kind { + IndentKind::Space => { + buffer.settings_at(line_buffer_range.start, cx).tab_size + } + IndentKind::Tab => NonZeroU32::new(1).unwrap(), + }; + if old_head.column <= indent_size.len && old_head.column > 0 { + let indent_len = indent_len.get(); + new_head = cmp::min( + new_head, + Point::new( + old_head.row, + ((old_head.column - 1) / indent_len) * indent_len, + ), + ); + } + } + + selection.set_head(new_head, SelectionGoal::None); + } + } + } + + this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections)); + this.insert("", cx); + this.refresh_copilot_suggestions(true, cx); + }); + } + + pub fn delete(&mut self, _: &Delete, cx: &mut ViewContext) { + self.transact(cx, |this, cx| { + this.change_selections(Some(Autoscroll::fit()), cx, |s| { + let line_mode = s.line_mode; + s.move_with(|map, selection| { + if selection.is_empty() && !line_mode { + let cursor = movement::right(map, selection.head()); + selection.end = cursor; + selection.reversed = true; + selection.goal = SelectionGoal::None; + } + }) + }); + this.insert("", cx); + this.refresh_copilot_suggestions(true, cx); + }); + } + + pub fn tab_prev(&mut self, _: &TabPrev, cx: &mut ViewContext) { + if self.move_to_prev_snippet_tabstop(cx) { + return; + } + + self.outdent(&Outdent, cx); + } + + pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext) { + if self.move_to_next_snippet_tabstop(cx) { + return; + } + + let mut selections = self.selections.all_adjusted(cx); + let buffer = self.buffer.read(cx); + let snapshot = buffer.snapshot(cx); + let rows_iter = selections.iter().map(|s| s.head().row); + let suggested_indents = snapshot.suggested_indents(rows_iter, cx); + + let mut edits = Vec::new(); + let mut prev_edited_row = 0; + let mut row_delta = 0; + for selection in &mut selections { + if selection.start.row != prev_edited_row { + row_delta = 0; + } + prev_edited_row = selection.end.row; + + // If the selection is non-empty, then increase the indentation of the selected lines. + if !selection.is_empty() { + row_delta = + Self::indent_selection(buffer, &snapshot, selection, &mut edits, row_delta, cx); + continue; + } + + // If the selection is empty and the cursor is in the leading whitespace before the + // suggested indentation, then auto-indent the line. + let cursor = selection.head(); + let current_indent = snapshot.indent_size_for_line(cursor.row); + if let Some(suggested_indent) = suggested_indents.get(&cursor.row).copied() { + if cursor.column < suggested_indent.len + && cursor.column <= current_indent.len + && current_indent.len <= suggested_indent.len + { + selection.start = Point::new(cursor.row, suggested_indent.len); + selection.end = selection.start; + if row_delta == 0 { + edits.extend(Buffer::edit_for_indent_size_adjustment( + cursor.row, + current_indent, + suggested_indent, + )); + row_delta = suggested_indent.len - current_indent.len; + } + continue; + } + } + + // Accept copilot suggestion if there is only one selection and the cursor is not + // in the leading whitespace. + if self.selections.count() == 1 + && cursor.column >= current_indent.len + && self.has_active_copilot_suggestion(cx) + { + self.accept_copilot_suggestion(cx); + return; + } + + // Otherwise, insert a hard or soft tab. + let settings = buffer.settings_at(cursor, cx); + let tab_size = if settings.hard_tabs { + IndentSize::tab() + } else { + let tab_size = settings.tab_size.get(); + let char_column = snapshot + .text_for_range(Point::new(cursor.row, 0)..cursor) + .flat_map(str::chars) + .count() + + row_delta as usize; + let chars_to_next_tab_stop = tab_size - (char_column as u32 % tab_size); + IndentSize::spaces(chars_to_next_tab_stop) + }; + selection.start = Point::new(cursor.row, cursor.column + row_delta + tab_size.len); + selection.end = selection.start; + edits.push((cursor..cursor, tab_size.chars().collect::())); + row_delta += tab_size.len; + } + + self.transact(cx, |this, cx| { + this.buffer.update(cx, |b, cx| b.edit(edits, None, cx)); + this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections)); + this.refresh_copilot_suggestions(true, cx); + }); + } + + pub fn indent(&mut self, _: &Indent, cx: &mut ViewContext) { + let mut selections = self.selections.all::(cx); + let mut prev_edited_row = 0; + let mut row_delta = 0; + let mut edits = Vec::new(); + let buffer = self.buffer.read(cx); + let snapshot = buffer.snapshot(cx); + for selection in &mut selections { + if selection.start.row != prev_edited_row { + row_delta = 0; + } + prev_edited_row = selection.end.row; + + row_delta = + Self::indent_selection(buffer, &snapshot, selection, &mut edits, row_delta, cx); + } + + self.transact(cx, |this, cx| { + this.buffer.update(cx, |b, cx| b.edit(edits, None, cx)); + this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections)); + }); + } + + fn indent_selection( + buffer: &MultiBuffer, + snapshot: &MultiBufferSnapshot, + selection: &mut Selection, + edits: &mut Vec<(Range, String)>, + delta_for_start_row: u32, + cx: &AppContext, + ) -> u32 { + let settings = buffer.settings_at(selection.start, cx); + let tab_size = settings.tab_size.get(); + let indent_kind = if settings.hard_tabs { + IndentKind::Tab + } else { + IndentKind::Space + }; + let mut start_row = selection.start.row; + let mut end_row = selection.end.row + 1; + + // If a selection ends at the beginning of a line, don't indent + // that last line. + if selection.end.column == 0 { + end_row -= 1; + } + + // Avoid re-indenting a row that has already been indented by a + // previous selection, but still update this selection's column + // to reflect that indentation. + if delta_for_start_row > 0 { + start_row += 1; + selection.start.column += delta_for_start_row; + if selection.end.row == selection.start.row { + selection.end.column += delta_for_start_row; + } + } + + let mut delta_for_end_row = 0; + for row in start_row..end_row { + let current_indent = snapshot.indent_size_for_line(row); + let indent_delta = match (current_indent.kind, indent_kind) { + (IndentKind::Space, IndentKind::Space) => { + let columns_to_next_tab_stop = tab_size - (current_indent.len % tab_size); + IndentSize::spaces(columns_to_next_tab_stop) + } + (IndentKind::Tab, IndentKind::Space) => IndentSize::spaces(tab_size), + (_, IndentKind::Tab) => IndentSize::tab(), + }; + + let row_start = Point::new(row, 0); + edits.push(( + row_start..row_start, + indent_delta.chars().collect::(), + )); + + // Update this selection's endpoints to reflect the indentation. + if row == selection.start.row { + selection.start.column += indent_delta.len; + } + if row == selection.end.row { + selection.end.column += indent_delta.len; + delta_for_end_row = indent_delta.len; + } + } + + if selection.start.row == selection.end.row { + delta_for_start_row + delta_for_end_row + } else { + delta_for_end_row + } + } + + pub fn outdent(&mut self, _: &Outdent, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let selections = self.selections.all::(cx); + let mut deletion_ranges = Vec::new(); + let mut last_outdent = None; + { + let buffer = self.buffer.read(cx); + let snapshot = buffer.snapshot(cx); + for selection in &selections { + let settings = buffer.settings_at(selection.start, cx); + let tab_size = settings.tab_size.get(); + let mut rows = selection.spanned_rows(false, &display_map); + + // Avoid re-outdenting a row that has already been outdented by a + // previous selection. + if let Some(last_row) = last_outdent { + if last_row == rows.start { + rows.start += 1; + } + } + + for row in rows { + let indent_size = snapshot.indent_size_for_line(row); + if indent_size.len > 0 { + let deletion_len = match indent_size.kind { + IndentKind::Space => { + let columns_to_prev_tab_stop = indent_size.len % tab_size; + if columns_to_prev_tab_stop == 0 { + tab_size + } else { + columns_to_prev_tab_stop + } + } + IndentKind::Tab => 1, + }; + deletion_ranges.push(Point::new(row, 0)..Point::new(row, deletion_len)); + last_outdent = Some(row); + } + } + } + } + + self.transact(cx, |this, cx| { + this.buffer.update(cx, |buffer, cx| { + let empty_str: Arc = "".into(); + buffer.edit( + deletion_ranges + .into_iter() + .map(|range| (range, empty_str.clone())), + None, + cx, + ); + }); + let selections = this.selections.all::(cx); + this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections)); + }); + } + + pub fn delete_line(&mut self, _: &DeleteLine, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let selections = self.selections.all::(cx); + + let mut new_cursors = Vec::new(); + let mut edit_ranges = Vec::new(); + let mut selections = selections.iter().peekable(); + while let Some(selection) = selections.next() { + let mut rows = selection.spanned_rows(false, &display_map); + let goal_display_column = selection.head().to_display_point(&display_map).column(); + + // Accumulate contiguous regions of rows that we want to delete. + while let Some(next_selection) = selections.peek() { + let next_rows = next_selection.spanned_rows(false, &display_map); + if next_rows.start <= rows.end { + rows.end = next_rows.end; + selections.next().unwrap(); + } else { + break; + } + } + + let buffer = &display_map.buffer_snapshot; + let mut edit_start = Point::new(rows.start, 0).to_offset(buffer); + let edit_end; + let cursor_buffer_row; + if buffer.max_point().row >= rows.end { + // If there's a line after the range, delete the \n from the end of the row range + // and position the cursor on the next line. + edit_end = Point::new(rows.end, 0).to_offset(buffer); + cursor_buffer_row = rows.end; + } else { + // If there isn't a line after the range, delete the \n from the line before the + // start of the row range and position the cursor there. + edit_start = edit_start.saturating_sub(1); + edit_end = buffer.len(); + cursor_buffer_row = rows.start.saturating_sub(1); + } + + let mut cursor = Point::new(cursor_buffer_row, 0).to_display_point(&display_map); + *cursor.column_mut() = + cmp::min(goal_display_column, display_map.line_len(cursor.row())); + + new_cursors.push(( + selection.id, + buffer.anchor_after(cursor.to_point(&display_map)), + )); + edit_ranges.push(edit_start..edit_end); + } + + self.transact(cx, |this, cx| { + let buffer = this.buffer.update(cx, |buffer, cx| { + let empty_str: Arc = "".into(); + buffer.edit( + edit_ranges + .into_iter() + .map(|range| (range, empty_str.clone())), + None, + cx, + ); + buffer.snapshot(cx) + }); + let new_selections = new_cursors + .into_iter() + .map(|(id, cursor)| { + let cursor = cursor.to_point(&buffer); + Selection { + id, + start: cursor, + end: cursor, + reversed: false, + goal: SelectionGoal::None, + } + }) + .collect(); + + this.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select(new_selections); + }); + }); + } + + pub fn join_lines(&mut self, _: &JoinLines, cx: &mut ViewContext) { + let mut row_ranges = Vec::>::new(); + for selection in self.selections.all::(cx) { + let start = selection.start.row; + let end = if selection.start.row == selection.end.row { + selection.start.row + 1 + } else { + selection.end.row + }; + + if let Some(last_row_range) = row_ranges.last_mut() { + if start <= last_row_range.end { + last_row_range.end = end; + continue; + } + } + row_ranges.push(start..end); + } + + let snapshot = self.buffer.read(cx).snapshot(cx); + let mut cursor_positions = Vec::new(); + for row_range in &row_ranges { + let anchor = snapshot.anchor_before(Point::new( + row_range.end - 1, + snapshot.line_len(row_range.end - 1), + )); + cursor_positions.push(anchor.clone()..anchor); + } + + self.transact(cx, |this, cx| { + for row_range in row_ranges.into_iter().rev() { + for row in row_range.rev() { + let end_of_line = Point::new(row, snapshot.line_len(row)); + let indent = snapshot.indent_size_for_line(row + 1); + let start_of_next_line = Point::new(row + 1, indent.len); + + let replace = if snapshot.line_len(row + 1) > indent.len { + " " + } else { + "" + }; + + this.buffer.update(cx, |buffer, cx| { + buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx) + }); + } + } + + this.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_anchor_ranges(cursor_positions) + }); + }); + } + + pub fn sort_lines_case_sensitive( + &mut self, + _: &SortLinesCaseSensitive, + cx: &mut ViewContext, + ) { + self.manipulate_lines(cx, |lines| lines.sort()) + } + + pub fn sort_lines_case_insensitive( + &mut self, + _: &SortLinesCaseInsensitive, + cx: &mut ViewContext, + ) { + self.manipulate_lines(cx, |lines| lines.sort_by_key(|line| line.to_lowercase())) + } + + pub fn reverse_lines(&mut self, _: &ReverseLines, cx: &mut ViewContext) { + self.manipulate_lines(cx, |lines| lines.reverse()) + } + + pub fn shuffle_lines(&mut self, _: &ShuffleLines, cx: &mut ViewContext) { + self.manipulate_lines(cx, |lines| lines.shuffle(&mut thread_rng())) + } + + fn manipulate_lines(&mut self, cx: &mut ViewContext, mut callback: Fn) + where + Fn: FnMut(&mut [&str]), + { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut edits = Vec::new(); + + let selections = self.selections.all::(cx); + let mut selections = selections.iter().peekable(); + let mut contiguous_row_selections = Vec::new(); + let mut new_selections = Vec::new(); + + while let Some(selection) = selections.next() { + let (start_row, end_row) = consume_contiguous_rows( + &mut contiguous_row_selections, + selection, + &display_map, + &mut selections, + ); + + let start_point = Point::new(start_row, 0); + let end_point = Point::new(end_row - 1, buffer.line_len(end_row - 1)); + let text = buffer + .text_for_range(start_point..end_point) + .collect::(); + let mut lines = text.split("\n").collect_vec(); + + let lines_len = lines.len(); + callback(&mut lines); + + // This is a current limitation with selections. + // If we wanted to support removing or adding lines, we'd need to fix the logic associated with selections. + debug_assert!( + lines.len() == lines_len, + "callback should not change the number of lines" + ); + + edits.push((start_point..end_point, lines.join("\n"))); + let start_anchor = buffer.anchor_after(start_point); + let end_anchor = buffer.anchor_before(end_point); + + // Make selection and push + new_selections.push(Selection { + id: selection.id, + start: start_anchor.to_offset(&buffer), + end: end_anchor.to_offset(&buffer), + goal: SelectionGoal::None, + reversed: selection.reversed, + }); + } + + self.transact(cx, |this, cx| { + this.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, None, cx); + }); + + this.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select(new_selections); + }); + + this.request_autoscroll(Autoscroll::fit(), cx); + }); + } + + pub fn convert_to_upper_case(&mut self, _: &ConvertToUpperCase, cx: &mut ViewContext) { + self.manipulate_text(cx, |text| text.to_uppercase()) + } + + pub fn convert_to_lower_case(&mut self, _: &ConvertToLowerCase, cx: &mut ViewContext) { + self.manipulate_text(cx, |text| text.to_lowercase()) + } + + pub fn convert_to_title_case(&mut self, _: &ConvertToTitleCase, cx: &mut ViewContext) { + self.manipulate_text(cx, |text| { + // Hack to get around the fact that to_case crate doesn't support '\n' as a word boundary + // https://github.com/rutrum/convert-case/issues/16 + text.split("\n") + .map(|line| line.to_case(Case::Title)) + .join("\n") + }) + } + + pub fn convert_to_snake_case(&mut self, _: &ConvertToSnakeCase, cx: &mut ViewContext) { + self.manipulate_text(cx, |text| text.to_case(Case::Snake)) + } + + pub fn convert_to_kebab_case(&mut self, _: &ConvertToKebabCase, cx: &mut ViewContext) { + self.manipulate_text(cx, |text| text.to_case(Case::Kebab)) + } + + pub fn convert_to_upper_camel_case( + &mut self, + _: &ConvertToUpperCamelCase, + cx: &mut ViewContext, + ) { + self.manipulate_text(cx, |text| { + // Hack to get around the fact that to_case crate doesn't support '\n' as a word boundary + // https://github.com/rutrum/convert-case/issues/16 + text.split("\n") + .map(|line| line.to_case(Case::UpperCamel)) + .join("\n") + }) + } + + pub fn convert_to_lower_camel_case( + &mut self, + _: &ConvertToLowerCamelCase, + cx: &mut ViewContext, + ) { + self.manipulate_text(cx, |text| text.to_case(Case::Camel)) + } + + fn manipulate_text(&mut self, cx: &mut ViewContext, mut callback: Fn) + where + Fn: FnMut(&str) -> String, + { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut new_selections = Vec::new(); + let mut edits = Vec::new(); + let mut selection_adjustment = 0i32; + + for selection in self.selections.all::(cx) { + let selection_is_empty = selection.is_empty(); + + let (start, end) = if selection_is_empty { + let word_range = movement::surrounding_word( + &display_map, + selection.start.to_display_point(&display_map), + ); + let start = word_range.start.to_offset(&display_map, Bias::Left); + let end = word_range.end.to_offset(&display_map, Bias::Left); + (start, end) + } else { + (selection.start, selection.end) + }; + + let text = buffer.text_for_range(start..end).collect::(); + let old_length = text.len() as i32; + let text = callback(&text); + + new_selections.push(Selection { + start: (start as i32 - selection_adjustment) as usize, + end: ((start + text.len()) as i32 - selection_adjustment) as usize, + goal: SelectionGoal::None, + ..selection + }); + + selection_adjustment += old_length - text.len() as i32; + + edits.push((start..end, text)); + } + + self.transact(cx, |this, cx| { + this.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, None, cx); + }); + + this.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select(new_selections); + }); + + this.request_autoscroll(Autoscroll::fit(), cx); + }); + } + + pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + let selections = self.selections.all::(cx); + + let mut edits = Vec::new(); + let mut selections_iter = selections.iter().peekable(); + while let Some(selection) = selections_iter.next() { + // Avoid duplicating the same lines twice. + let mut rows = selection.spanned_rows(false, &display_map); + + while let Some(next_selection) = selections_iter.peek() { + let next_rows = next_selection.spanned_rows(false, &display_map); + if next_rows.start < rows.end { + rows.end = next_rows.end; + selections_iter.next().unwrap(); + } else { + break; + } + } + + // Copy the text from the selected row region and splice it at the start of the region. + let start = Point::new(rows.start, 0); + let end = Point::new(rows.end - 1, buffer.line_len(rows.end - 1)); + let text = buffer + .text_for_range(start..end) + .chain(Some("\n")) + .collect::(); + edits.push((start..start, text)); + } + + self.transact(cx, |this, cx| { + this.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, None, cx); + }); + + this.request_autoscroll(Autoscroll::fit(), cx); + }); + } + + pub fn move_line_up(&mut self, _: &MoveLineUp, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut edits = Vec::new(); + let mut unfold_ranges = Vec::new(); + let mut refold_ranges = Vec::new(); + + let selections = self.selections.all::(cx); + let mut selections = selections.iter().peekable(); + let mut contiguous_row_selections = Vec::new(); + let mut new_selections = Vec::new(); + + while let Some(selection) = selections.next() { + // Find all the selections that span a contiguous row range + let (start_row, end_row) = consume_contiguous_rows( + &mut contiguous_row_selections, + selection, + &display_map, + &mut selections, + ); + + // Move the text spanned by the row range to be before the line preceding the row range + if start_row > 0 { + let range_to_move = Point::new(start_row - 1, buffer.line_len(start_row - 1)) + ..Point::new(end_row - 1, buffer.line_len(end_row - 1)); + let insertion_point = display_map + .prev_line_boundary(Point::new(start_row - 1, 0)) + .0; + + // Don't move lines across excerpts + if buffer + .excerpt_boundaries_in_range(( + Bound::Excluded(insertion_point), + Bound::Included(range_to_move.end), + )) + .next() + .is_none() + { + let text = buffer + .text_for_range(range_to_move.clone()) + .flat_map(|s| s.chars()) + .skip(1) + .chain(['\n']) + .collect::(); + + edits.push(( + buffer.anchor_after(range_to_move.start) + ..buffer.anchor_before(range_to_move.end), + String::new(), + )); + let insertion_anchor = buffer.anchor_after(insertion_point); + edits.push((insertion_anchor..insertion_anchor, text)); + + let row_delta = range_to_move.start.row - insertion_point.row + 1; + + // Move selections up + new_selections.extend(contiguous_row_selections.drain(..).map( + |mut selection| { + selection.start.row -= row_delta; + selection.end.row -= row_delta; + selection + }, + )); + + // Move folds up + unfold_ranges.push(range_to_move.clone()); + for fold in display_map.folds_in_range( + buffer.anchor_before(range_to_move.start) + ..buffer.anchor_after(range_to_move.end), + ) { + let mut start = fold.start.to_point(&buffer); + let mut end = fold.end.to_point(&buffer); + start.row -= row_delta; + end.row -= row_delta; + refold_ranges.push(start..end); + } + } + } + + // If we didn't move line(s), preserve the existing selections + new_selections.append(&mut contiguous_row_selections); + } + + self.transact(cx, |this, cx| { + this.unfold_ranges(unfold_ranges, true, true, cx); + this.buffer.update(cx, |buffer, cx| { + for (range, text) in edits { + buffer.edit([(range, text)], None, cx); + } + }); + this.fold_ranges(refold_ranges, true, cx); + this.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select(new_selections); + }) + }); + } + + pub fn move_line_down(&mut self, _: &MoveLineDown, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut edits = Vec::new(); + let mut unfold_ranges = Vec::new(); + let mut refold_ranges = Vec::new(); + + let selections = self.selections.all::(cx); + let mut selections = selections.iter().peekable(); + let mut contiguous_row_selections = Vec::new(); + let mut new_selections = Vec::new(); + + while let Some(selection) = selections.next() { + // Find all the selections that span a contiguous row range + let (start_row, end_row) = consume_contiguous_rows( + &mut contiguous_row_selections, + selection, + &display_map, + &mut selections, + ); + + // Move the text spanned by the row range to be after the last line of the row range + if end_row <= buffer.max_point().row { + let range_to_move = Point::new(start_row, 0)..Point::new(end_row, 0); + let insertion_point = display_map.next_line_boundary(Point::new(end_row, 0)).0; + + // Don't move lines across excerpt boundaries + if buffer + .excerpt_boundaries_in_range(( + Bound::Excluded(range_to_move.start), + Bound::Included(insertion_point), + )) + .next() + .is_none() + { + let mut text = String::from("\n"); + text.extend(buffer.text_for_range(range_to_move.clone())); + text.pop(); // Drop trailing newline + edits.push(( + buffer.anchor_after(range_to_move.start) + ..buffer.anchor_before(range_to_move.end), + String::new(), + )); + let insertion_anchor = buffer.anchor_after(insertion_point); + edits.push((insertion_anchor..insertion_anchor, text)); + + let row_delta = insertion_point.row - range_to_move.end.row + 1; + + // Move selections down + new_selections.extend(contiguous_row_selections.drain(..).map( + |mut selection| { + selection.start.row += row_delta; + selection.end.row += row_delta; + selection + }, + )); + + // Move folds down + unfold_ranges.push(range_to_move.clone()); + for fold in display_map.folds_in_range( + buffer.anchor_before(range_to_move.start) + ..buffer.anchor_after(range_to_move.end), + ) { + let mut start = fold.start.to_point(&buffer); + let mut end = fold.end.to_point(&buffer); + start.row += row_delta; + end.row += row_delta; + refold_ranges.push(start..end); + } + } + } + + // If we didn't move line(s), preserve the existing selections + new_selections.append(&mut contiguous_row_selections); + } + + self.transact(cx, |this, cx| { + this.unfold_ranges(unfold_ranges, true, true, cx); + this.buffer.update(cx, |buffer, cx| { + for (range, text) in edits { + buffer.edit([(range, text)], None, cx); + } + }); + this.fold_ranges(refold_ranges, true, cx); + this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections)); + }); + } + + pub fn transpose(&mut self, _: &Transpose, cx: &mut ViewContext) { + let text_layout_details = &self.text_layout_details(cx); + self.transact(cx, |this, cx| { + let edits = this.change_selections(Some(Autoscroll::fit()), cx, |s| { + let mut edits: Vec<(Range, String)> = Default::default(); + let line_mode = s.line_mode; + s.move_with(|display_map, selection| { + if !selection.is_empty() || line_mode { + return; + } + + let mut head = selection.head(); + let mut transpose_offset = head.to_offset(display_map, Bias::Right); + if head.column() == display_map.line_len(head.row()) { + transpose_offset = display_map + .buffer_snapshot + .clip_offset(transpose_offset.saturating_sub(1), Bias::Left); + } + + if transpose_offset == 0 { + return; + } + + *head.column_mut() += 1; + head = display_map.clip_point(head, Bias::Right); + let goal = SelectionGoal::HorizontalPosition( + display_map.x_for_point(head, &text_layout_details), + ); + selection.collapse_to(head, goal); + + let transpose_start = display_map + .buffer_snapshot + .clip_offset(transpose_offset.saturating_sub(1), Bias::Left); + if edits.last().map_or(true, |e| e.0.end <= transpose_start) { + let transpose_end = display_map + .buffer_snapshot + .clip_offset(transpose_offset + 1, Bias::Right); + if let Some(ch) = + display_map.buffer_snapshot.chars_at(transpose_start).next() + { + edits.push((transpose_start..transpose_offset, String::new())); + edits.push((transpose_end..transpose_end, ch.to_string())); + } + } + }); + edits + }); + this.buffer + .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); + let selections = this.selections.all::(cx); + this.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select(selections); + }); + }); + } + + pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext) { + let mut text = String::new(); + let buffer = self.buffer.read(cx).snapshot(cx); + let mut selections = self.selections.all::(cx); + let mut clipboard_selections = Vec::with_capacity(selections.len()); + { + let max_point = buffer.max_point(); + let mut is_first = true; + for selection in &mut selections { + let is_entire_line = selection.is_empty() || self.selections.line_mode; + if is_entire_line { + selection.start = Point::new(selection.start.row, 0); + selection.end = cmp::min(max_point, Point::new(selection.end.row + 1, 0)); + selection.goal = SelectionGoal::None; + } + if is_first { + is_first = false; + } else { + text += "\n"; + } + let mut len = 0; + for chunk in buffer.text_for_range(selection.start..selection.end) { + text.push_str(chunk); + len += chunk.len(); + } + clipboard_selections.push(ClipboardSelection { + len, + is_entire_line, + first_line_indent: buffer.indent_size_for_line(selection.start.row).len, + }); + } + } + + self.transact(cx, |this, cx| { + this.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select(selections); + }); + this.insert("", cx); + cx.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); + }); + } + + pub fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { + let selections = self.selections.all::(cx); + let buffer = self.buffer.read(cx).read(cx); + let mut text = String::new(); + + let mut clipboard_selections = Vec::with_capacity(selections.len()); + { + let max_point = buffer.max_point(); + let mut is_first = true; + for selection in selections.iter() { + let mut start = selection.start; + let mut end = selection.end; + let is_entire_line = selection.is_empty() || self.selections.line_mode; + if is_entire_line { + start = Point::new(start.row, 0); + end = cmp::min(max_point, Point::new(end.row + 1, 0)); + } + if is_first { + is_first = false; + } else { + text += "\n"; + } + let mut len = 0; + for chunk in buffer.text_for_range(start..end) { + text.push_str(chunk); + len += chunk.len(); + } + clipboard_selections.push(ClipboardSelection { + len, + is_entire_line, + first_line_indent: buffer.indent_size_for_line(start.row).len, + }); + } + } + + cx.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); + } + + pub fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { + self.transact(cx, |this, cx| { + if let Some(item) = cx.read_from_clipboard() { + let clipboard_text = Cow::Borrowed(item.text()); + if let Some(mut clipboard_selections) = item.metadata::>() { + let old_selections = this.selections.all::(cx); + let all_selections_were_entire_line = + clipboard_selections.iter().all(|s| s.is_entire_line); + let first_selection_indent_column = + clipboard_selections.first().map(|s| s.first_line_indent); + if clipboard_selections.len() != old_selections.len() { + clipboard_selections.drain(..); + } + + this.buffer.update(cx, |buffer, cx| { + let snapshot = buffer.read(cx); + let mut start_offset = 0; + let mut edits = Vec::new(); + let mut original_indent_columns = Vec::new(); + let line_mode = this.selections.line_mode; + for (ix, selection) in old_selections.iter().enumerate() { + let to_insert; + let entire_line; + let original_indent_column; + if let Some(clipboard_selection) = clipboard_selections.get(ix) { + let end_offset = start_offset + clipboard_selection.len; + to_insert = &clipboard_text[start_offset..end_offset]; + entire_line = clipboard_selection.is_entire_line; + start_offset = end_offset + 1; + original_indent_column = + Some(clipboard_selection.first_line_indent); + } else { + to_insert = clipboard_text.as_str(); + entire_line = all_selections_were_entire_line; + original_indent_column = first_selection_indent_column + } + + // If the corresponding selection was empty when this slice of the + // clipboard text was written, then the entire line containing the + // selection was copied. If this selection is also currently empty, + // then paste the line before the current line of the buffer. + let range = if selection.is_empty() && !line_mode && entire_line { + let column = selection.start.to_point(&snapshot).column as usize; + let line_start = selection.start - column; + line_start..line_start + } else { + selection.range() + }; + + edits.push((range, to_insert)); + original_indent_columns.extend(original_indent_column); + } + drop(snapshot); + + buffer.edit( + edits, + Some(AutoindentMode::Block { + original_indent_columns, + }), + cx, + ); + }); + + let selections = this.selections.all::(cx); + this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections)); + } else { + this.insert(&clipboard_text, cx); + } + } + }); + } + + pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext) { + if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) { + if let Some((selections, _)) = self.selection_history.transaction(tx_id).cloned() { + self.change_selections(None, cx, |s| { + s.select_anchors(selections.to_vec()); + }); + } + self.request_autoscroll(Autoscroll::fit(), cx); + self.unmark_text(cx); + self.refresh_copilot_suggestions(true, cx); + cx.emit(Event::Edited); + } + } + + pub fn redo(&mut self, _: &Redo, cx: &mut ViewContext) { + if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) { + if let Some((_, Some(selections))) = self.selection_history.transaction(tx_id).cloned() + { + self.change_selections(None, cx, |s| { + s.select_anchors(selections.to_vec()); + }); + } + self.request_autoscroll(Autoscroll::fit(), cx); + self.unmark_text(cx); + self.refresh_copilot_suggestions(true, cx); + cx.emit(Event::Edited); + } + } + + pub fn finalize_last_transaction(&mut self, cx: &mut ViewContext) { + self.buffer + .update(cx, |buffer, cx| buffer.finalize_last_transaction(cx)); + } + + pub fn move_left(&mut self, _: &MoveLeft, cx: &mut ViewContext) { + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + let line_mode = s.line_mode; + s.move_with(|map, selection| { + let cursor = if selection.is_empty() && !line_mode { + movement::left(map, selection.start) + } else { + selection.start + }; + selection.collapse_to(cursor, SelectionGoal::None); + }); + }) + } + + pub fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext) { + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_heads_with(|map, head, _| (movement::left(map, head), SelectionGoal::None)); + }) + } + + pub fn move_right(&mut self, _: &MoveRight, cx: &mut ViewContext) { + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + let line_mode = s.line_mode; + s.move_with(|map, selection| { + let cursor = if selection.is_empty() && !line_mode { + movement::right(map, selection.end) + } else { + selection.end + }; + selection.collapse_to(cursor, SelectionGoal::None) + }); + }) + } + + pub fn select_right(&mut self, _: &SelectRight, cx: &mut ViewContext) { + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_heads_with(|map, head, _| (movement::right(map, head), SelectionGoal::None)); + }) + } + + pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { + if self.take_rename(true, cx).is_some() { + return; + } + + if matches!(self.mode, EditorMode::SingleLine) { + cx.propagate_action(); + return; + } + + let text_layout_details = &self.text_layout_details(cx); + + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + let line_mode = s.line_mode; + s.move_with(|map, selection| { + if !selection.is_empty() && !line_mode { + selection.goal = SelectionGoal::None; + } + let (cursor, goal) = movement::up( + map, + selection.start, + selection.goal, + false, + &text_layout_details, + ); + selection.collapse_to(cursor, goal); + }); + }) + } + + pub fn move_page_up(&mut self, action: &MovePageUp, cx: &mut ViewContext) { + if self.take_rename(true, cx).is_some() { + return; + } + + if matches!(self.mode, EditorMode::SingleLine) { + cx.propagate_action(); + return; + } + + let row_count = if let Some(row_count) = self.visible_line_count() { + row_count as u32 - 1 + } else { + return; + }; + + let autoscroll = if action.center_cursor { + Autoscroll::center() + } else { + Autoscroll::fit() + }; + + let text_layout_details = &self.text_layout_details(cx); + + self.change_selections(Some(autoscroll), cx, |s| { + let line_mode = s.line_mode; + s.move_with(|map, selection| { + if !selection.is_empty() && !line_mode { + selection.goal = SelectionGoal::None; + } + let (cursor, goal) = movement::up_by_rows( + map, + selection.end, + row_count, + selection.goal, + false, + &text_layout_details, + ); + selection.collapse_to(cursor, goal); + }); + }); + } + + pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext) { + let text_layout_details = &self.text_layout_details(cx); + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_heads_with(|map, head, goal| { + movement::up(map, head, goal, false, &text_layout_details) + }) + }) + } + + pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { + self.take_rename(true, cx); + + if self.mode == EditorMode::SingleLine { + cx.propagate_action(); + return; + } + + let text_layout_details = &self.text_layout_details(cx); + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + let line_mode = s.line_mode; + s.move_with(|map, selection| { + if !selection.is_empty() && !line_mode { + selection.goal = SelectionGoal::None; + } + let (cursor, goal) = movement::down( + map, + selection.end, + selection.goal, + false, + &text_layout_details, + ); + selection.collapse_to(cursor, goal); + }); + }); + } + + pub fn move_page_down(&mut self, action: &MovePageDown, cx: &mut ViewContext) { + if self.take_rename(true, cx).is_some() { + return; + } + + if self + .context_menu + .write() + .as_mut() + .map(|menu| menu.select_last(self.project.as_ref(), cx)) + .unwrap_or(false) + { + return; + } + + if matches!(self.mode, EditorMode::SingleLine) { + cx.propagate_action(); + return; + } + + let row_count = if let Some(row_count) = self.visible_line_count() { + row_count as u32 - 1 + } else { + return; + }; + + let autoscroll = if action.center_cursor { + Autoscroll::center() + } else { + Autoscroll::fit() + }; + + let text_layout_details = &self.text_layout_details(cx); + self.change_selections(Some(autoscroll), cx, |s| { + let line_mode = s.line_mode; + s.move_with(|map, selection| { + if !selection.is_empty() && !line_mode { + selection.goal = SelectionGoal::None; + } + let (cursor, goal) = movement::down_by_rows( + map, + selection.end, + row_count, + selection.goal, + false, + &text_layout_details, + ); + selection.collapse_to(cursor, goal); + }); + }); + } + + pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext) { + let text_layout_details = &self.text_layout_details(cx); + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_heads_with(|map, head, goal| { + movement::down(map, head, goal, false, &text_layout_details) + }) + }); + } + + pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext) { + if let Some(context_menu) = self.context_menu.write().as_mut() { + context_menu.select_first(self.project.as_ref(), cx); + } + } + + pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext) { + if let Some(context_menu) = self.context_menu.write().as_mut() { + context_menu.select_prev(self.project.as_ref(), cx); + } + } + + pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext) { + if let Some(context_menu) = self.context_menu.write().as_mut() { + context_menu.select_next(self.project.as_ref(), cx); + } + } + + pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext) { + if let Some(context_menu) = self.context_menu.write().as_mut() { + context_menu.select_last(self.project.as_ref(), cx); + } + } + + pub fn move_to_previous_word_start( + &mut self, + _: &MoveToPreviousWordStart, + cx: &mut ViewContext, + ) { + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_cursors_with(|map, head, _| { + ( + movement::previous_word_start(map, head), + SelectionGoal::None, + ) + }); + }) + } + + pub fn move_to_previous_subword_start( + &mut self, + _: &MoveToPreviousSubwordStart, + cx: &mut ViewContext, + ) { + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_cursors_with(|map, head, _| { + ( + movement::previous_subword_start(map, head), + SelectionGoal::None, + ) + }); + }) + } + + pub fn select_to_previous_word_start( + &mut self, + _: &SelectToPreviousWordStart, + cx: &mut ViewContext, + ) { + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::previous_word_start(map, head), + SelectionGoal::None, + ) + }); + }) + } + + pub fn select_to_previous_subword_start( + &mut self, + _: &SelectToPreviousSubwordStart, + cx: &mut ViewContext, + ) { + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::previous_subword_start(map, head), + SelectionGoal::None, + ) + }); + }) + } + + pub fn delete_to_previous_word_start( + &mut self, + _: &DeleteToPreviousWordStart, + cx: &mut ViewContext, + ) { + self.transact(cx, |this, cx| { + this.select_autoclose_pair(cx); + this.change_selections(Some(Autoscroll::fit()), cx, |s| { + let line_mode = s.line_mode; + s.move_with(|map, selection| { + if selection.is_empty() && !line_mode { + let cursor = movement::previous_word_start(map, selection.head()); + selection.set_head(cursor, SelectionGoal::None); + } + }); + }); + this.insert("", cx); + }); + } + + pub fn delete_to_previous_subword_start( + &mut self, + _: &DeleteToPreviousSubwordStart, + cx: &mut ViewContext, + ) { + self.transact(cx, |this, cx| { + this.select_autoclose_pair(cx); + this.change_selections(Some(Autoscroll::fit()), cx, |s| { + let line_mode = s.line_mode; + s.move_with(|map, selection| { + if selection.is_empty() && !line_mode { + let cursor = movement::previous_subword_start(map, selection.head()); + selection.set_head(cursor, SelectionGoal::None); + } + }); + }); + this.insert("", cx); + }); + } + + pub fn move_to_next_word_end(&mut self, _: &MoveToNextWordEnd, cx: &mut ViewContext) { + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_cursors_with(|map, head, _| { + (movement::next_word_end(map, head), SelectionGoal::None) + }); + }) + } + + pub fn move_to_next_subword_end( + &mut self, + _: &MoveToNextSubwordEnd, + cx: &mut ViewContext, + ) { + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_cursors_with(|map, head, _| { + (movement::next_subword_end(map, head), SelectionGoal::None) + }); + }) + } + + pub fn select_to_next_word_end(&mut self, _: &SelectToNextWordEnd, cx: &mut ViewContext) { + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_heads_with(|map, head, _| { + (movement::next_word_end(map, head), SelectionGoal::None) + }); + }) + } + + pub fn select_to_next_subword_end( + &mut self, + _: &SelectToNextSubwordEnd, + cx: &mut ViewContext, + ) { + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_heads_with(|map, head, _| { + (movement::next_subword_end(map, head), SelectionGoal::None) + }); + }) + } + + pub fn delete_to_next_word_end(&mut self, _: &DeleteToNextWordEnd, cx: &mut ViewContext) { + self.transact(cx, |this, cx| { + this.change_selections(Some(Autoscroll::fit()), cx, |s| { + let line_mode = s.line_mode; + s.move_with(|map, selection| { + if selection.is_empty() && !line_mode { + let cursor = movement::next_word_end(map, selection.head()); + selection.set_head(cursor, SelectionGoal::None); + } + }); + }); + this.insert("", cx); + }); + } + + pub fn delete_to_next_subword_end( + &mut self, + _: &DeleteToNextSubwordEnd, + cx: &mut ViewContext, + ) { + self.transact(cx, |this, cx| { + this.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_with(|map, selection| { + if selection.is_empty() { + let cursor = movement::next_subword_end(map, selection.head()); + selection.set_head(cursor, SelectionGoal::None); + } + }); + }); + this.insert("", cx); + }); + } + + pub fn move_to_beginning_of_line( + &mut self, + _: &MoveToBeginningOfLine, + cx: &mut ViewContext, + ) { + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_cursors_with(|map, head, _| { + ( + movement::indented_line_beginning(map, head, true), + SelectionGoal::None, + ) + }); + }) + } + + pub fn select_to_beginning_of_line( + &mut self, + action: &SelectToBeginningOfLine, + cx: &mut ViewContext, + ) { + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::indented_line_beginning(map, head, action.stop_at_soft_wraps), + SelectionGoal::None, + ) + }); + }); + } + + pub fn delete_to_beginning_of_line( + &mut self, + _: &DeleteToBeginningOfLine, + cx: &mut ViewContext, + ) { + self.transact(cx, |this, cx| { + this.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_with(|_, selection| { + selection.reversed = true; + }); + }); + + this.select_to_beginning_of_line( + &SelectToBeginningOfLine { + stop_at_soft_wraps: false, + }, + cx, + ); + this.backspace(&Backspace, cx); + }); + } + + pub fn move_to_end_of_line(&mut self, _: &MoveToEndOfLine, cx: &mut ViewContext) { + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_cursors_with(|map, head, _| { + (movement::line_end(map, head, true), SelectionGoal::None) + }); + }) + } + + pub fn select_to_end_of_line( + &mut self, + action: &SelectToEndOfLine, + cx: &mut ViewContext, + ) { + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::line_end(map, head, action.stop_at_soft_wraps), + SelectionGoal::None, + ) + }); + }) + } + + pub fn delete_to_end_of_line(&mut self, _: &DeleteToEndOfLine, cx: &mut ViewContext) { + self.transact(cx, |this, cx| { + this.select_to_end_of_line( + &SelectToEndOfLine { + stop_at_soft_wraps: false, + }, + cx, + ); + this.delete(&Delete, cx); + }); + } + + pub fn cut_to_end_of_line(&mut self, _: &CutToEndOfLine, cx: &mut ViewContext) { + self.transact(cx, |this, cx| { + this.select_to_end_of_line( + &SelectToEndOfLine { + stop_at_soft_wraps: false, + }, + cx, + ); + this.cut(&Cut, cx); + }); + } + + pub fn move_to_start_of_paragraph( + &mut self, + _: &MoveToStartOfParagraph, + cx: &mut ViewContext, + ) { + if matches!(self.mode, EditorMode::SingleLine) { + cx.propagate_action(); + return; + } + + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_with(|map, selection| { + selection.collapse_to( + movement::start_of_paragraph(map, selection.head(), 1), + SelectionGoal::None, + ) + }); + }) + } + + pub fn move_to_end_of_paragraph( + &mut self, + _: &MoveToEndOfParagraph, + cx: &mut ViewContext, + ) { + if matches!(self.mode, EditorMode::SingleLine) { + cx.propagate_action(); + return; + } + + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_with(|map, selection| { + selection.collapse_to( + movement::end_of_paragraph(map, selection.head(), 1), + SelectionGoal::None, + ) + }); + }) + } + + pub fn select_to_start_of_paragraph( + &mut self, + _: &SelectToStartOfParagraph, + cx: &mut ViewContext, + ) { + if matches!(self.mode, EditorMode::SingleLine) { + cx.propagate_action(); + return; + } + + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::start_of_paragraph(map, head, 1), + SelectionGoal::None, + ) + }); + }) + } + + pub fn select_to_end_of_paragraph( + &mut self, + _: &SelectToEndOfParagraph, + cx: &mut ViewContext, + ) { + if matches!(self.mode, EditorMode::SingleLine) { + cx.propagate_action(); + return; + } + + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::end_of_paragraph(map, head, 1), + SelectionGoal::None, + ) + }); + }) + } + + pub fn move_to_beginning(&mut self, _: &MoveToBeginning, cx: &mut ViewContext) { + if matches!(self.mode, EditorMode::SingleLine) { + cx.propagate_action(); + return; + } + + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges(vec![0..0]); + }); + } + + pub fn select_to_beginning(&mut self, _: &SelectToBeginning, cx: &mut ViewContext) { + let mut selection = self.selections.last::(cx); + selection.set_head(Point::zero(), SelectionGoal::None); + + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select(vec![selection]); + }); + } + + pub fn move_to_end(&mut self, _: &MoveToEnd, cx: &mut ViewContext) { + if matches!(self.mode, EditorMode::SingleLine) { + cx.propagate_action(); + return; + } + + let cursor = self.buffer.read(cx).read(cx).len(); + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges(vec![cursor..cursor]) + }); + } + + pub fn set_nav_history(&mut self, nav_history: Option) { + self.nav_history = nav_history; + } + + pub fn nav_history(&self) -> Option<&ItemNavHistory> { + self.nav_history.as_ref() + } + + fn push_to_nav_history( + &mut self, + cursor_anchor: Anchor, + new_position: Option, + cx: &mut ViewContext, + ) { + if let Some(nav_history) = self.nav_history.as_mut() { + let buffer = self.buffer.read(cx).read(cx); + let cursor_position = cursor_anchor.to_point(&buffer); + let scroll_state = self.scroll_manager.anchor(); + let scroll_top_row = scroll_state.top_row(&buffer); + drop(buffer); + + if let Some(new_position) = new_position { + let row_delta = (new_position.row as i64 - cursor_position.row as i64).abs(); + if row_delta < MIN_NAVIGATION_HISTORY_ROW_DELTA { + return; + } + } + + nav_history.push( + Some(NavigationData { + cursor_anchor, + cursor_position, + scroll_anchor: scroll_state, + scroll_top_row, + }), + cx, + ); + } + } + + pub fn select_to_end(&mut self, _: &SelectToEnd, cx: &mut ViewContext) { + let buffer = self.buffer.read(cx).snapshot(cx); + let mut selection = self.selections.first::(cx); + selection.set_head(buffer.len(), SelectionGoal::None); + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select(vec![selection]); + }); + } + + pub fn select_all(&mut self, _: &SelectAll, cx: &mut ViewContext) { + let end = self.buffer.read(cx).read(cx).len(); + self.change_selections(None, cx, |s| { + s.select_ranges(vec![0..end]); + }); + } + + pub fn select_line(&mut self, _: &SelectLine, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.selections.all::(cx); + let max_point = display_map.buffer_snapshot.max_point(); + for selection in &mut selections { + let rows = selection.spanned_rows(true, &display_map); + selection.start = Point::new(rows.start, 0); + selection.end = cmp::min(max_point, Point::new(rows.end, 0)); + selection.reversed = false; + } + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select(selections); + }); + } + + pub fn split_selection_into_lines( + &mut self, + _: &SplitSelectionIntoLines, + cx: &mut ViewContext, + ) { + let mut to_unfold = Vec::new(); + let mut new_selection_ranges = Vec::new(); + { + let selections = self.selections.all::(cx); + let buffer = self.buffer.read(cx).read(cx); + for selection in selections { + for row in selection.start.row..selection.end.row { + let cursor = Point::new(row, buffer.line_len(row)); + new_selection_ranges.push(cursor..cursor); + } + new_selection_ranges.push(selection.end..selection.end); + to_unfold.push(selection.start..selection.end); + } + } + self.unfold_ranges(to_unfold, true, true, cx); + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges(new_selection_ranges); + }); + } + + pub fn add_selection_above(&mut self, _: &AddSelectionAbove, cx: &mut ViewContext) { + self.add_selection(true, cx); + } + + pub fn add_selection_below(&mut self, _: &AddSelectionBelow, cx: &mut ViewContext) { + self.add_selection(false, cx); + } + + fn add_selection(&mut self, above: bool, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.selections.all::(cx); + let text_layout_details = self.text_layout_details(cx); + let mut state = self.add_selections_state.take().unwrap_or_else(|| { + let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone(); + let range = oldest_selection.display_range(&display_map).sorted(); + + let start_x = display_map.x_for_point(range.start, &text_layout_details); + let end_x = display_map.x_for_point(range.end, &text_layout_details); + let positions = start_x.min(end_x)..start_x.max(end_x); + + selections.clear(); + let mut stack = Vec::new(); + for row in range.start.row()..=range.end.row() { + if let Some(selection) = self.selections.build_columnar_selection( + &display_map, + row, + &positions, + oldest_selection.reversed, + &text_layout_details, + ) { + stack.push(selection.id); + selections.push(selection); + } + } + + if above { + stack.reverse(); + } + + AddSelectionsState { above, stack } + }); + + let last_added_selection = *state.stack.last().unwrap(); + let mut new_selections = Vec::new(); + if above == state.above { + let end_row = if above { + 0 + } else { + display_map.max_point().row() + }; + + 'outer: for selection in selections { + if selection.id == last_added_selection { + let range = selection.display_range(&display_map).sorted(); + debug_assert_eq!(range.start.row(), range.end.row()); + let mut row = range.start.row(); + let positions = if let SelectionGoal::HorizontalRange { start, end } = + selection.goal + { + start..end + } else { + let start_x = display_map.x_for_point(range.start, &text_layout_details); + let end_x = display_map.x_for_point(range.end, &text_layout_details); + + start_x.min(end_x)..start_x.max(end_x) + }; + + while row != end_row { + if above { + row -= 1; + } else { + row += 1; + } + + if let Some(new_selection) = self.selections.build_columnar_selection( + &display_map, + row, + &positions, + selection.reversed, + &text_layout_details, + ) { + state.stack.push(new_selection.id); + if above { + new_selections.push(new_selection); + new_selections.push(selection); + } else { + new_selections.push(selection); + new_selections.push(new_selection); + } + + continue 'outer; + } + } + } + + new_selections.push(selection); + } + } else { + new_selections = selections; + new_selections.retain(|s| s.id != last_added_selection); + state.stack.pop(); + } + + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select(new_selections); + }); + if state.stack.len() > 1 { + self.add_selections_state = Some(state); + } + } + + pub fn select_next_match_internal( + &mut self, + display_map: &DisplaySnapshot, + replace_newest: bool, + autoscroll: Option, + cx: &mut ViewContext, + ) -> Result<()> { + fn select_next_match_ranges( + this: &mut Editor, + range: Range, + replace_newest: bool, + auto_scroll: Option, + cx: &mut ViewContext, + ) { + this.unfold_ranges([range.clone()], false, true, cx); + this.change_selections(auto_scroll, cx, |s| { + if replace_newest { + s.delete(s.newest_anchor().id); + } + s.insert_range(range.clone()); + }); + } + + let buffer = &display_map.buffer_snapshot; + let mut selections = self.selections.all::(cx); + if let Some(mut select_next_state) = self.select_next_state.take() { + let query = &select_next_state.query; + if !select_next_state.done { + let first_selection = selections.iter().min_by_key(|s| s.id).unwrap(); + let last_selection = selections.iter().max_by_key(|s| s.id).unwrap(); + let mut next_selected_range = None; + + let bytes_after_last_selection = + buffer.bytes_in_range(last_selection.end..buffer.len()); + let bytes_before_first_selection = buffer.bytes_in_range(0..first_selection.start); + let query_matches = query + .stream_find_iter(bytes_after_last_selection) + .map(|result| (last_selection.end, result)) + .chain( + query + .stream_find_iter(bytes_before_first_selection) + .map(|result| (0, result)), + ); + + for (start_offset, query_match) in query_matches { + let query_match = query_match.unwrap(); // can only fail due to I/O + let offset_range = + start_offset + query_match.start()..start_offset + query_match.end(); + let display_range = offset_range.start.to_display_point(&display_map) + ..offset_range.end.to_display_point(&display_map); + + if !select_next_state.wordwise + || (!movement::is_inside_word(&display_map, display_range.start) + && !movement::is_inside_word(&display_map, display_range.end)) + { + if selections + .iter() + .find(|selection| selection.range().overlaps(&offset_range)) + .is_none() + { + next_selected_range = Some(offset_range); + break; + } + } + } + + if let Some(next_selected_range) = next_selected_range { + select_next_match_ranges( + self, + next_selected_range, + replace_newest, + autoscroll, + cx, + ); + } else { + select_next_state.done = true; + } + } + + self.select_next_state = Some(select_next_state); + } else if selections.len() == 1 { + let selection = selections.last_mut().unwrap(); + if selection.start == selection.end { + let word_range = movement::surrounding_word( + &display_map, + selection.start.to_display_point(&display_map), + ); + selection.start = word_range.start.to_offset(&display_map, Bias::Left); + selection.end = word_range.end.to_offset(&display_map, Bias::Left); + selection.goal = SelectionGoal::None; + selection.reversed = false; + + let query = buffer + .text_for_range(selection.start..selection.end) + .collect::(); + + let is_empty = query.is_empty(); + let select_state = SelectNextState { + query: AhoCorasick::new(&[query])?, + wordwise: true, + done: is_empty, + }; + select_next_match_ranges( + self, + selection.start..selection.end, + replace_newest, + autoscroll, + cx, + ); + self.select_next_state = Some(select_state); + } else { + let query = buffer + .text_for_range(selection.start..selection.end) + .collect::(); + self.select_next_state = Some(SelectNextState { + query: AhoCorasick::new(&[query])?, + wordwise: false, + done: false, + }); + self.select_next_match_internal(display_map, replace_newest, autoscroll, cx)?; + } + } + Ok(()) + } + + pub fn select_all_matches( + &mut self, + action: &SelectAllMatches, + cx: &mut ViewContext, + ) -> Result<()> { + self.push_to_selection_history(); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + + loop { + self.select_next_match_internal(&display_map, action.replace_newest, None, cx)?; + + if self + .select_next_state + .as_ref() + .map(|selection_state| selection_state.done) + .unwrap_or(true) + { + break; + } + } + + Ok(()) + } + + pub fn select_next(&mut self, action: &SelectNext, cx: &mut ViewContext) -> Result<()> { + self.push_to_selection_history(); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + self.select_next_match_internal( + &display_map, + action.replace_newest, + Some(Autoscroll::newest()), + cx, + )?; + Ok(()) + } + + pub fn select_previous( + &mut self, + action: &SelectPrevious, + cx: &mut ViewContext, + ) -> Result<()> { + self.push_to_selection_history(); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + let mut selections = self.selections.all::(cx); + if let Some(mut select_prev_state) = self.select_prev_state.take() { + let query = &select_prev_state.query; + if !select_prev_state.done { + let first_selection = selections.iter().min_by_key(|s| s.id).unwrap(); + let last_selection = selections.iter().max_by_key(|s| s.id).unwrap(); + let mut next_selected_range = None; + // When we're iterating matches backwards, the oldest match will actually be the furthest one in the buffer. + let bytes_before_last_selection = + buffer.reversed_bytes_in_range(0..last_selection.start); + let bytes_after_first_selection = + buffer.reversed_bytes_in_range(first_selection.end..buffer.len()); + let query_matches = query + .stream_find_iter(bytes_before_last_selection) + .map(|result| (last_selection.start, result)) + .chain( + query + .stream_find_iter(bytes_after_first_selection) + .map(|result| (buffer.len(), result)), + ); + for (end_offset, query_match) in query_matches { + let query_match = query_match.unwrap(); // can only fail due to I/O + let offset_range = + end_offset - query_match.end()..end_offset - query_match.start(); + let display_range = offset_range.start.to_display_point(&display_map) + ..offset_range.end.to_display_point(&display_map); + + if !select_prev_state.wordwise + || (!movement::is_inside_word(&display_map, display_range.start) + && !movement::is_inside_word(&display_map, display_range.end)) + { + next_selected_range = Some(offset_range); + break; + } + } + + if let Some(next_selected_range) = next_selected_range { + self.unfold_ranges([next_selected_range.clone()], false, true, cx); + self.change_selections(Some(Autoscroll::newest()), cx, |s| { + if action.replace_newest { + s.delete(s.newest_anchor().id); + } + s.insert_range(next_selected_range); + }); + } else { + select_prev_state.done = true; + } + } + + self.select_prev_state = Some(select_prev_state); + } else if selections.len() == 1 { + let selection = selections.last_mut().unwrap(); + if selection.start == selection.end { + let word_range = movement::surrounding_word( + &display_map, + selection.start.to_display_point(&display_map), + ); + selection.start = word_range.start.to_offset(&display_map, Bias::Left); + selection.end = word_range.end.to_offset(&display_map, Bias::Left); + selection.goal = SelectionGoal::None; + selection.reversed = false; + + let query = buffer + .text_for_range(selection.start..selection.end) + .collect::(); + let query = query.chars().rev().collect::(); + let select_state = SelectNextState { + query: AhoCorasick::new(&[query])?, + wordwise: true, + done: false, + }; + self.unfold_ranges([selection.start..selection.end], false, true, cx); + self.change_selections(Some(Autoscroll::newest()), cx, |s| { + s.select(selections); + }); + self.select_prev_state = Some(select_state); + } else { + let query = buffer + .text_for_range(selection.start..selection.end) + .collect::(); + let query = query.chars().rev().collect::(); + self.select_prev_state = Some(SelectNextState { + query: AhoCorasick::new(&[query])?, + wordwise: false, + done: false, + }); + self.select_previous(action, cx)?; + } + } + Ok(()) + } + + pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext) { + let text_layout_details = &self.text_layout_details(cx); + self.transact(cx, |this, cx| { + let mut selections = this.selections.all::(cx); + let mut edits = Vec::new(); + let mut selection_edit_ranges = Vec::new(); + let mut last_toggled_row = None; + let snapshot = this.buffer.read(cx).read(cx); + let empty_str: Arc = "".into(); + let mut suffixes_inserted = Vec::new(); + + fn comment_prefix_range( + snapshot: &MultiBufferSnapshot, + row: u32, + comment_prefix: &str, + comment_prefix_whitespace: &str, + ) -> Range { + let start = Point::new(row, snapshot.indent_size_for_line(row).len); + + let mut line_bytes = snapshot + .bytes_in_range(start..snapshot.max_point()) + .flatten() + .copied(); + + // If this line currently begins with the line comment prefix, then record + // the range containing the prefix. + if line_bytes + .by_ref() + .take(comment_prefix.len()) + .eq(comment_prefix.bytes()) + { + // Include any whitespace that matches the comment prefix. + let matching_whitespace_len = line_bytes + .zip(comment_prefix_whitespace.bytes()) + .take_while(|(a, b)| a == b) + .count() as u32; + let end = Point::new( + start.row, + start.column + comment_prefix.len() as u32 + matching_whitespace_len, + ); + start..end + } else { + start..start + } + } + + fn comment_suffix_range( + snapshot: &MultiBufferSnapshot, + row: u32, + comment_suffix: &str, + comment_suffix_has_leading_space: bool, + ) -> Range { + let end = Point::new(row, snapshot.line_len(row)); + let suffix_start_column = end.column.saturating_sub(comment_suffix.len() as u32); + + let mut line_end_bytes = snapshot + .bytes_in_range(Point::new(end.row, suffix_start_column.saturating_sub(1))..end) + .flatten() + .copied(); + + let leading_space_len = if suffix_start_column > 0 + && line_end_bytes.next() == Some(b' ') + && comment_suffix_has_leading_space + { + 1 + } else { + 0 + }; + + // If this line currently begins with the line comment prefix, then record + // the range containing the prefix. + if line_end_bytes.by_ref().eq(comment_suffix.bytes()) { + let start = Point::new(end.row, suffix_start_column - leading_space_len); + start..end + } else { + end..end + } + } + + // TODO: Handle selections that cross excerpts + for selection in &mut selections { + let start_column = snapshot.indent_size_for_line(selection.start.row).len; + let language = if let Some(language) = + snapshot.language_scope_at(Point::new(selection.start.row, start_column)) + { + language + } else { + continue; + }; + + selection_edit_ranges.clear(); + + // If multiple selections contain a given row, avoid processing that + // row more than once. + let mut start_row = selection.start.row; + if last_toggled_row == Some(start_row) { + start_row += 1; + } + let end_row = + if selection.end.row > selection.start.row && selection.end.column == 0 { + selection.end.row - 1 + } else { + selection.end.row + }; + last_toggled_row = Some(end_row); + + if start_row > end_row { + continue; + } + + // If the language has line comments, toggle those. + if let Some(full_comment_prefix) = language.line_comment_prefix() { + // Split the comment prefix's trailing whitespace into a separate string, + // as that portion won't be used for detecting if a line is a comment. + let comment_prefix = full_comment_prefix.trim_end_matches(' '); + let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..]; + let mut all_selection_lines_are_comments = true; + + for row in start_row..=end_row { + if snapshot.is_line_blank(row) && start_row < end_row { + continue; + } + + let prefix_range = comment_prefix_range( + snapshot.deref(), + row, + comment_prefix, + comment_prefix_whitespace, + ); + if prefix_range.is_empty() { + all_selection_lines_are_comments = false; + } + selection_edit_ranges.push(prefix_range); + } + + if all_selection_lines_are_comments { + edits.extend( + selection_edit_ranges + .iter() + .cloned() + .map(|range| (range, empty_str.clone())), + ); + } else { + let min_column = selection_edit_ranges + .iter() + .map(|r| r.start.column) + .min() + .unwrap_or(0); + edits.extend(selection_edit_ranges.iter().map(|range| { + let position = Point::new(range.start.row, min_column); + (position..position, full_comment_prefix.clone()) + })); + } + } else if let Some((full_comment_prefix, comment_suffix)) = + language.block_comment_delimiters() + { + let comment_prefix = full_comment_prefix.trim_end_matches(' '); + let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..]; + let prefix_range = comment_prefix_range( + snapshot.deref(), + start_row, + comment_prefix, + comment_prefix_whitespace, + ); + let suffix_range = comment_suffix_range( + snapshot.deref(), + end_row, + comment_suffix.trim_start_matches(' '), + comment_suffix.starts_with(' '), + ); + + if prefix_range.is_empty() || suffix_range.is_empty() { + edits.push(( + prefix_range.start..prefix_range.start, + full_comment_prefix.clone(), + )); + edits.push((suffix_range.end..suffix_range.end, comment_suffix.clone())); + suffixes_inserted.push((end_row, comment_suffix.len())); + } else { + edits.push((prefix_range, empty_str.clone())); + edits.push((suffix_range, empty_str.clone())); + } + } else { + continue; + } + } + + drop(snapshot); + this.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, None, cx); + }); + + // Adjust selections so that they end before any comment suffixes that + // were inserted. + let mut suffixes_inserted = suffixes_inserted.into_iter().peekable(); + let mut selections = this.selections.all::(cx); + let snapshot = this.buffer.read(cx).read(cx); + for selection in &mut selections { + while let Some((row, suffix_len)) = suffixes_inserted.peek().copied() { + match row.cmp(&selection.end.row) { + Ordering::Less => { + suffixes_inserted.next(); + continue; + } + Ordering::Greater => break, + Ordering::Equal => { + if selection.end.column == snapshot.line_len(row) { + if selection.is_empty() { + selection.start.column -= suffix_len as u32; + } + selection.end.column -= suffix_len as u32; + } + break; + } + } + } + } + + drop(snapshot); + this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections)); + + let selections = this.selections.all::(cx); + let selections_on_single_row = selections.windows(2).all(|selections| { + selections[0].start.row == selections[1].start.row + && selections[0].end.row == selections[1].end.row + && selections[0].start.row == selections[0].end.row + }); + let selections_selecting = selections + .iter() + .any(|selection| selection.start != selection.end); + let advance_downwards = action.advance_downwards + && selections_on_single_row + && !selections_selecting + && this.mode != EditorMode::SingleLine; + + if advance_downwards { + let snapshot = this.buffer.read(cx).snapshot(cx); + + this.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_cursors_with(|display_snapshot, display_point, _| { + let mut point = display_point.to_point(display_snapshot); + point.row += 1; + point = snapshot.clip_point(point, Bias::Left); + let display_point = point.to_display_point(display_snapshot); + let goal = SelectionGoal::HorizontalPosition( + display_snapshot.x_for_point(display_point, &text_layout_details), + ); + (display_point, goal) + }) + }); + } + }); + } + + pub fn select_larger_syntax_node( + &mut self, + _: &SelectLargerSyntaxNode, + cx: &mut ViewContext, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + let old_selections = self.selections.all::(cx).into_boxed_slice(); + + let mut stack = mem::take(&mut self.select_larger_syntax_node_stack); + let mut selected_larger_node = false; + let new_selections = old_selections + .iter() + .map(|selection| { + let old_range = selection.start..selection.end; + let mut new_range = old_range.clone(); + while let Some(containing_range) = + buffer.range_for_syntax_ancestor(new_range.clone()) + { + new_range = containing_range; + if !display_map.intersects_fold(new_range.start) + && !display_map.intersects_fold(new_range.end) + { + break; + } + } + + selected_larger_node |= new_range != old_range; + Selection { + id: selection.id, + start: new_range.start, + end: new_range.end, + goal: SelectionGoal::None, + reversed: selection.reversed, + } + }) + .collect::>(); + + if selected_larger_node { + stack.push(old_selections); + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select(new_selections); + }); + } + self.select_larger_syntax_node_stack = stack; + } + + pub fn select_smaller_syntax_node( + &mut self, + _: &SelectSmallerSyntaxNode, + cx: &mut ViewContext, + ) { + let mut stack = mem::take(&mut self.select_larger_syntax_node_stack); + if let Some(selections) = stack.pop() { + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select(selections.to_vec()); + }); + } + self.select_larger_syntax_node_stack = stack; + } + + pub fn move_to_enclosing_bracket( + &mut self, + _: &MoveToEnclosingBracket, + cx: &mut ViewContext, + ) { + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_offsets_with(|snapshot, selection| { + let Some(enclosing_bracket_ranges) = + snapshot.enclosing_bracket_ranges(selection.start..selection.end) + else { + return; + }; + + let mut best_length = usize::MAX; + let mut best_inside = false; + let mut best_in_bracket_range = false; + let mut best_destination = None; + for (open, close) in enclosing_bracket_ranges { + let close = close.to_inclusive(); + let length = close.end() - open.start; + let inside = selection.start >= open.end && selection.end <= *close.start(); + let in_bracket_range = open.to_inclusive().contains(&selection.head()) + || close.contains(&selection.head()); + + // If best is next to a bracket and current isn't, skip + if !in_bracket_range && best_in_bracket_range { + continue; + } + + // Prefer smaller lengths unless best is inside and current isn't + if length > best_length && (best_inside || !inside) { + continue; + } + + best_length = length; + best_inside = inside; + best_in_bracket_range = in_bracket_range; + best_destination = Some( + if close.contains(&selection.start) && close.contains(&selection.end) { + if inside { + open.end + } else { + open.start + } + } else { + if inside { + *close.start() + } else { + *close.end() + } + }, + ); + } + + if let Some(destination) = best_destination { + selection.collapse_to(destination, SelectionGoal::None); + } + }) + }); + } + + pub fn undo_selection(&mut self, _: &UndoSelection, cx: &mut ViewContext) { + self.end_selection(cx); + self.selection_history.mode = SelectionHistoryMode::Undoing; + if let Some(entry) = self.selection_history.undo_stack.pop_back() { + self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec())); + self.select_next_state = entry.select_next_state; + self.select_prev_state = entry.select_prev_state; + self.add_selections_state = entry.add_selections_state; + self.request_autoscroll(Autoscroll::newest(), cx); + } + self.selection_history.mode = SelectionHistoryMode::Normal; + } + + pub fn redo_selection(&mut self, _: &RedoSelection, cx: &mut ViewContext) { + self.end_selection(cx); + self.selection_history.mode = SelectionHistoryMode::Redoing; + if let Some(entry) = self.selection_history.redo_stack.pop_back() { + self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec())); + self.select_next_state = entry.select_next_state; + self.select_prev_state = entry.select_prev_state; + self.add_selections_state = entry.add_selections_state; + self.request_autoscroll(Autoscroll::newest(), cx); + } + self.selection_history.mode = SelectionHistoryMode::Normal; + } + + fn go_to_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext) { + self.go_to_diagnostic_impl(Direction::Next, cx) + } + + fn go_to_prev_diagnostic(&mut self, _: &GoToPrevDiagnostic, cx: &mut ViewContext) { + self.go_to_diagnostic_impl(Direction::Prev, cx) + } + + pub fn go_to_diagnostic_impl(&mut self, direction: Direction, cx: &mut ViewContext) { + let buffer = self.buffer.read(cx).snapshot(cx); + let selection = self.selections.newest::(cx); + + // If there is an active Diagnostic Popover. Jump to it's diagnostic instead. + if direction == Direction::Next { + if let Some(popover) = self.hover_state.diagnostic_popover.as_ref() { + let (group_id, jump_to) = popover.activation_info(); + if self.activate_diagnostics(group_id, cx) { + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + let mut new_selection = s.newest_anchor().clone(); + new_selection.collapse_to(jump_to, SelectionGoal::None); + s.select_anchors(vec![new_selection.clone()]); + }); + } + return; + } + } + + let mut active_primary_range = self.active_diagnostics.as_ref().map(|active_diagnostics| { + active_diagnostics + .primary_range + .to_offset(&buffer) + .to_inclusive() + }); + let mut search_start = if let Some(active_primary_range) = active_primary_range.as_ref() { + if active_primary_range.contains(&selection.head()) { + *active_primary_range.end() + } else { + selection.head() + } + } else { + selection.head() + }; + + loop { + let mut diagnostics = if direction == Direction::Prev { + buffer.diagnostics_in_range::<_, usize>(0..search_start, true) + } else { + buffer.diagnostics_in_range::<_, usize>(search_start..buffer.len(), false) + }; + let group = diagnostics.find_map(|entry| { + if entry.diagnostic.is_primary + && entry.diagnostic.severity <= DiagnosticSeverity::WARNING + && !entry.range.is_empty() + && Some(entry.range.end) != active_primary_range.as_ref().map(|r| *r.end()) + && !entry.range.contains(&search_start) + { + Some((entry.range, entry.diagnostic.group_id)) + } else { + None + } + }); + + if let Some((primary_range, group_id)) = group { + if self.activate_diagnostics(group_id, cx) { + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select(vec![Selection { + id: selection.id, + start: primary_range.start, + end: primary_range.start, + reversed: false, + goal: SelectionGoal::None, + }]); + }); + } + break; + } else { + // Cycle around to the start of the buffer, potentially moving back to the start of + // the currently active diagnostic. + active_primary_range.take(); + if direction == Direction::Prev { + if search_start == buffer.len() { + break; + } else { + search_start = buffer.len(); + } + } else if search_start == 0 { + break; + } else { + search_start = 0; + } + } + } + } + + fn go_to_hunk(&mut self, _: &GoToHunk, cx: &mut ViewContext) { + let snapshot = self + .display_map + .update(cx, |display_map, cx| display_map.snapshot(cx)); + let selection = self.selections.newest::(cx); + + if !self.seek_in_direction( + &snapshot, + selection.head(), + false, + snapshot + .buffer_snapshot + .git_diff_hunks_in_range((selection.head().row + 1)..u32::MAX), + cx, + ) { + let wrapped_point = Point::zero(); + self.seek_in_direction( + &snapshot, + wrapped_point, + true, + snapshot + .buffer_snapshot + .git_diff_hunks_in_range((wrapped_point.row + 1)..u32::MAX), + cx, + ); + } + } + + fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, cx: &mut ViewContext) { + let snapshot = self + .display_map + .update(cx, |display_map, cx| display_map.snapshot(cx)); + let selection = self.selections.newest::(cx); + + if !self.seek_in_direction( + &snapshot, + selection.head(), + false, + snapshot + .buffer_snapshot + .git_diff_hunks_in_range_rev(0..selection.head().row), + cx, + ) { + let wrapped_point = snapshot.buffer_snapshot.max_point(); + self.seek_in_direction( + &snapshot, + wrapped_point, + true, + snapshot + .buffer_snapshot + .git_diff_hunks_in_range_rev(0..wrapped_point.row), + cx, + ); + } + } + + fn seek_in_direction( + &mut self, + snapshot: &DisplaySnapshot, + initial_point: Point, + is_wrapped: bool, + hunks: impl Iterator>, + cx: &mut ViewContext, + ) -> bool { + let display_point = initial_point.to_display_point(snapshot); + let mut hunks = hunks + .map(|hunk| diff_hunk_to_display(hunk, &snapshot)) + .filter(|hunk| { + if is_wrapped { + true + } else { + !hunk.contains_display_row(display_point.row()) + } + }) + .dedup(); + + if let Some(hunk) = hunks.next() { + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + let row = hunk.start_display_row(); + let point = DisplayPoint::new(row, 0); + s.select_display_ranges([point..point]); + }); + + true + } else { + false + } + } + + pub fn go_to_definition(&mut self, _: &GoToDefinition, cx: &mut ViewContext) { + self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, false, cx); + } + + pub fn go_to_type_definition(&mut self, _: &GoToTypeDefinition, cx: &mut ViewContext) { + self.go_to_definition_of_kind(GotoDefinitionKind::Type, false, cx); + } + + pub fn go_to_definition_split(&mut self, _: &GoToDefinitionSplit, cx: &mut ViewContext) { + self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, true, cx); + } + + pub fn go_to_type_definition_split( + &mut self, + _: &GoToTypeDefinitionSplit, + cx: &mut ViewContext, + ) { + self.go_to_definition_of_kind(GotoDefinitionKind::Type, true, cx); + } + + fn go_to_definition_of_kind( + &mut self, + kind: GotoDefinitionKind, + split: bool, + cx: &mut ViewContext, + ) { + let Some(workspace) = self.workspace(cx) else { + return; + }; + let buffer = self.buffer.read(cx); + let head = self.selections.newest::(cx).head(); + let (buffer, head) = if let Some(text_anchor) = buffer.text_anchor_for_position(head, cx) { + text_anchor + } else { + return; + }; + + let project = workspace.read(cx).project().clone(); + let definitions = project.update(cx, |project, cx| match kind { + GotoDefinitionKind::Symbol => project.definition(&buffer, head, cx), + GotoDefinitionKind::Type => project.type_definition(&buffer, head, cx), + }); + + cx.spawn_labeled("Fetching Definition...", |editor, mut cx| async move { + let definitions = definitions.await?; + editor.update(&mut cx, |editor, cx| { + editor.navigate_to_definitions( + definitions + .into_iter() + .map(GoToDefinitionLink::Text) + .collect(), + split, + cx, + ); + })?; + Ok::<(), anyhow::Error>(()) + }) + .detach_and_log_err(cx); + } + + pub fn navigate_to_definitions( + &mut self, + mut definitions: Vec, + split: bool, + cx: &mut ViewContext, + ) { + let Some(workspace) = self.workspace(cx) else { + return; + }; + let pane = workspace.read(cx).active_pane().clone(); + // If there is one definition, just open it directly + if definitions.len() == 1 { + let definition = definitions.pop().unwrap(); + let target_task = match definition { + GoToDefinitionLink::Text(link) => Task::Ready(Some(Ok(Some(link.target)))), + GoToDefinitionLink::InlayHint(lsp_location, server_id) => { + self.compute_target_location(lsp_location, server_id, cx) + } + }; + cx.spawn(|editor, mut cx| async move { + let target = target_task.await.context("target resolution task")?; + if let Some(target) = target { + editor.update(&mut cx, |editor, cx| { + let range = target.range.to_offset(target.buffer.read(cx)); + let range = editor.range_for_match(&range); + if Some(&target.buffer) == editor.buffer.read(cx).as_singleton().as_ref() { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([range]); + }); + } else { + cx.window_context().defer(move |cx| { + let target_editor: ViewHandle = + workspace.update(cx, |workspace, cx| { + if split { + workspace.split_project_item(target.buffer.clone(), cx) + } else { + workspace.open_project_item(target.buffer.clone(), cx) + } + }); + target_editor.update(cx, |target_editor, cx| { + // When selecting a definition in a different buffer, disable the nav history + // to avoid creating a history entry at the previous cursor location. + pane.update(cx, |pane, _| pane.disable_history()); + target_editor.change_selections( + Some(Autoscroll::fit()), + cx, + |s| { + s.select_ranges([range]); + }, + ); + pane.update(cx, |pane, _| pane.enable_history()); + }); + }); + } + }) + } else { + Ok(()) + } + }) + .detach_and_log_err(cx); + } else if !definitions.is_empty() { + let replica_id = self.replica_id(cx); + cx.spawn(|editor, mut cx| async move { + let (title, location_tasks) = editor + .update(&mut cx, |editor, cx| { + let title = definitions + .iter() + .find_map(|definition| match definition { + GoToDefinitionLink::Text(link) => { + link.origin.as_ref().map(|origin| { + let buffer = origin.buffer.read(cx); + format!( + "Definitions for {}", + buffer + .text_for_range(origin.range.clone()) + .collect::() + ) + }) + } + GoToDefinitionLink::InlayHint(_, _) => None, + }) + .unwrap_or("Definitions".to_string()); + let location_tasks = definitions + .into_iter() + .map(|definition| match definition { + GoToDefinitionLink::Text(link) => { + Task::Ready(Some(Ok(Some(link.target)))) + } + GoToDefinitionLink::InlayHint(lsp_location, server_id) => { + editor.compute_target_location(lsp_location, server_id, cx) + } + }) + .collect::>(); + (title, location_tasks) + }) + .context("location tasks preparation")?; + + let locations = futures::future::join_all(location_tasks) + .await + .into_iter() + .filter_map(|location| location.transpose()) + .collect::>() + .context("location tasks")?; + workspace.update(&mut cx, |workspace, cx| { + Self::open_locations_in_multibuffer( + workspace, locations, replica_id, title, split, cx, + ) + }); + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + } + + fn compute_target_location( + &self, + lsp_location: lsp::Location, + server_id: LanguageServerId, + cx: &mut ViewContext, + ) -> Task>> { + let Some(project) = self.project.clone() else { + return Task::Ready(Some(Ok(None))); + }; + + cx.spawn(move |editor, mut cx| async move { + let location_task = editor.update(&mut cx, |editor, cx| { + project.update(cx, |project, cx| { + let language_server_name = + editor.buffer.read(cx).as_singleton().and_then(|buffer| { + project + .language_server_for_buffer(buffer.read(cx), server_id, cx) + .map(|(_, lsp_adapter)| { + LanguageServerName(Arc::from(lsp_adapter.name())) + }) + }); + language_server_name.map(|language_server_name| { + project.open_local_buffer_via_lsp( + lsp_location.uri.clone(), + server_id, + language_server_name, + cx, + ) + }) + }) + })?; + let location = match location_task { + Some(task) => Some({ + let target_buffer_handle = task.await.context("open local buffer")?; + let range = { + target_buffer_handle.update(&mut cx, |target_buffer, _| { + let target_start = target_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.start), + Bias::Left, + ); + let target_end = target_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.end), + Bias::Left, + ); + target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end) + }) + }; + Location { + buffer: target_buffer_handle, + range, + } + }), + None => None, + }; + Ok(location) + }) + } + + pub fn find_all_references( + workspace: &mut Workspace, + _: &FindAllReferences, + cx: &mut ViewContext, + ) -> Option>> { + let active_item = workspace.active_item(cx)?; + let editor_handle = active_item.act_as::(cx)?; + + let editor = editor_handle.read(cx); + let buffer = editor.buffer.read(cx); + let head = editor.selections.newest::(cx).head(); + let (buffer, head) = buffer.text_anchor_for_position(head, cx)?; + let replica_id = editor.replica_id(cx); + + let project = workspace.project().clone(); + let references = project.update(cx, |project, cx| project.references(&buffer, head, cx)); + Some(cx.spawn_labeled( + "Finding All References...", + |workspace, mut cx| async move { + let locations = references.await?; + if locations.is_empty() { + return Ok(()); + } + + workspace.update(&mut cx, |workspace, cx| { + let title = locations + .first() + .as_ref() + .map(|location| { + let buffer = location.buffer.read(cx); + format!( + "References to `{}`", + buffer + .text_for_range(location.range.clone()) + .collect::() + ) + }) + .unwrap(); + Self::open_locations_in_multibuffer( + workspace, locations, replica_id, title, false, cx, + ); + })?; + + Ok(()) + }, + )) + } + + /// Opens a multibuffer with the given project locations in it + pub fn open_locations_in_multibuffer( + workspace: &mut Workspace, + mut locations: Vec, + replica_id: ReplicaId, + title: String, + split: bool, + cx: &mut ViewContext, + ) { + // If there are multiple definitions, open them in a multibuffer + locations.sort_by_key(|location| location.buffer.read(cx).remote_id()); + let mut locations = locations.into_iter().peekable(); + let mut ranges_to_highlight = Vec::new(); + + let excerpt_buffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(replica_id); + while let Some(location) = locations.next() { + let buffer = location.buffer.read(cx); + let mut ranges_for_buffer = Vec::new(); + let range = location.range.to_offset(buffer); + ranges_for_buffer.push(range.clone()); + + while let Some(next_location) = locations.peek() { + if next_location.buffer == location.buffer { + ranges_for_buffer.push(next_location.range.to_offset(buffer)); + locations.next(); + } else { + break; + } + } + + ranges_for_buffer.sort_by_key(|range| (range.start, Reverse(range.end))); + ranges_to_highlight.extend(multibuffer.push_excerpts_with_context_lines( + location.buffer.clone(), + ranges_for_buffer, + 1, + cx, + )) + } + + multibuffer.with_title(title) + }); + + let editor = cx.add_view(|cx| { + Editor::for_multibuffer(excerpt_buffer, Some(workspace.project().clone()), cx) + }); + editor.update(cx, |editor, cx| { + editor.highlight_background::( + ranges_to_highlight, + |theme| theme.editor.highlighted_line_background, + cx, + ); + }); + if split { + workspace.split_item(SplitDirection::Right, Box::new(editor), cx); + } else { + workspace.add_item(Box::new(editor), cx); + } + } + + pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext) -> Option>> { + use language::ToOffset as _; + + let project = self.project.clone()?; + let selection = self.selections.newest_anchor().clone(); + let (cursor_buffer, cursor_buffer_position) = self + .buffer + .read(cx) + .text_anchor_for_position(selection.head(), cx)?; + let (tail_buffer, _) = self + .buffer + .read(cx) + .text_anchor_for_position(selection.tail(), cx)?; + if tail_buffer != cursor_buffer { + return None; + } + + let snapshot = cursor_buffer.read(cx).snapshot(); + let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot); + let prepare_rename = project.update(cx, |project, cx| { + project.prepare_rename(cursor_buffer, cursor_buffer_offset, cx) + }); + + Some(cx.spawn(|this, mut cx| async move { + let rename_range = if let Some(range) = prepare_rename.await? { + Some(range) + } else { + this.update(&mut cx, |this, cx| { + let buffer = this.buffer.read(cx).snapshot(cx); + let mut buffer_highlights = this + .document_highlights_for_position(selection.head(), &buffer) + .filter(|highlight| { + highlight.start.excerpt_id == selection.head().excerpt_id + && highlight.end.excerpt_id == selection.head().excerpt_id + }); + buffer_highlights + .next() + .map(|highlight| highlight.start.text_anchor..highlight.end.text_anchor) + })? + }; + if let Some(rename_range) = rename_range { + let rename_buffer_range = rename_range.to_offset(&snapshot); + let cursor_offset_in_rename_range = + cursor_buffer_offset.saturating_sub(rename_buffer_range.start); + + this.update(&mut cx, |this, cx| { + this.take_rename(false, cx); + let style = this.style(cx); + let buffer = this.buffer.read(cx).read(cx); + let cursor_offset = selection.head().to_offset(&buffer); + let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range); + let rename_end = rename_start + rename_buffer_range.len(); + let range = buffer.anchor_before(rename_start)..buffer.anchor_after(rename_end); + let mut old_highlight_id = None; + let old_name: Arc = buffer + .chunks(rename_start..rename_end, true) + .map(|chunk| { + if old_highlight_id.is_none() { + old_highlight_id = chunk.syntax_highlight_id; + } + chunk.text + }) + .collect::() + .into(); + + drop(buffer); + + // Position the selection in the rename editor so that it matches the current selection. + this.show_local_selections = false; + let rename_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line(None, cx); + if let Some(old_highlight_id) = old_highlight_id { + editor.override_text_style = + Some(Box::new(move |style| old_highlight_id.style(&style.syntax))); + } + editor.buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, old_name.clone())], None, cx) + }); + editor.select_all(&SelectAll, cx); + editor + }); + + let ranges = this + .clear_background_highlights::(cx) + .into_iter() + .flat_map(|(_, ranges)| ranges.into_iter()) + .chain( + this.clear_background_highlights::(cx) + .into_iter() + .flat_map(|(_, ranges)| ranges.into_iter()), + ) + .collect(); + + this.highlight_text::( + ranges, + HighlightStyle { + fade_out: Some(style.rename_fade), + ..Default::default() + }, + cx, + ); + cx.focus(&rename_editor); + let block_id = this.insert_blocks( + [BlockProperties { + style: BlockStyle::Flex, + position: range.start.clone(), + height: 1, + render: Arc::new({ + let editor = rename_editor.clone(); + move |cx: &mut BlockContext| { + ChildView::new(&editor, cx) + .contained() + .with_padding_left(cx.anchor_x) + .into_any() + } + }), + disposition: BlockDisposition::Below, + }], + Some(Autoscroll::fit()), + cx, + )[0]; + this.pending_rename = Some(RenameState { + range, + old_name, + editor: rename_editor, + block_id, + }); + })?; + } + + Ok(()) + })) + } + + pub fn confirm_rename( + workspace: &mut Workspace, + _: &ConfirmRename, + cx: &mut ViewContext, + ) -> Option>> { + let editor = workspace.active_item(cx)?.act_as::(cx)?; + + let (buffer, range, old_name, new_name) = editor.update(cx, |editor, cx| { + let rename = editor.take_rename(false, cx)?; + let buffer = editor.buffer.read(cx); + let (start_buffer, start) = + buffer.text_anchor_for_position(rename.range.start.clone(), cx)?; + let (end_buffer, end) = + buffer.text_anchor_for_position(rename.range.end.clone(), cx)?; + if start_buffer == end_buffer { + let new_name = rename.editor.read(cx).text(cx); + Some((start_buffer, start..end, rename.old_name, new_name)) + } else { + None + } + })?; + + let rename = workspace.project().clone().update(cx, |project, cx| { + project.perform_rename(buffer.clone(), range.start, new_name.clone(), true, cx) + }); + + let editor = editor.downgrade(); + Some(cx.spawn(|workspace, mut cx| async move { + let project_transaction = rename.await?; + Self::open_project_transaction( + &editor, + workspace, + project_transaction, + format!("Rename: {} → {}", old_name, new_name), + cx.clone(), + ) + .await?; + + editor.update(&mut cx, |editor, cx| { + editor.refresh_document_highlights(cx); + })?; + Ok(()) + })) + } + + fn take_rename( + &mut self, + moving_cursor: bool, + cx: &mut ViewContext, + ) -> Option { + let rename = self.pending_rename.take()?; + self.remove_blocks( + [rename.block_id].into_iter().collect(), + Some(Autoscroll::fit()), + cx, + ); + self.clear_highlights::(cx); + self.show_local_selections = true; + + if moving_cursor { + let rename_editor = rename.editor.read(cx); + let cursor_in_rename_editor = rename_editor.selections.newest::(cx).head(); + + // Update the selection to match the position of the selection inside + // the rename editor. + let snapshot = self.buffer.read(cx).read(cx); + let rename_range = rename.range.to_offset(&snapshot); + let cursor_in_editor = snapshot + .clip_offset(rename_range.start + cursor_in_rename_editor, Bias::Left) + .min(rename_range.end); + drop(snapshot); + + self.change_selections(None, cx, |s| { + s.select_ranges(vec![cursor_in_editor..cursor_in_editor]) + }); + } else { + self.refresh_document_highlights(cx); + } + + Some(rename) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn pending_rename(&self) -> Option<&RenameState> { + self.pending_rename.as_ref() + } + + fn format(&mut self, _: &Format, cx: &mut ViewContext) -> Option>> { + let project = match &self.project { + Some(project) => project.clone(), + None => return None, + }; + + Some(self.perform_format(project, FormatTrigger::Manual, cx)) + } + + fn perform_format( + &mut self, + project: ModelHandle, + trigger: FormatTrigger, + cx: &mut ViewContext, + ) -> Task> { + let buffer = self.buffer().clone(); + let buffers = buffer.read(cx).all_buffers(); + + let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse(); + let format = project.update(cx, |project, cx| project.format(buffers, true, trigger, cx)); + + cx.spawn(|_, mut cx| async move { + let transaction = futures::select_biased! { + _ = timeout => { + log::warn!("timed out waiting for formatting"); + None + } + transaction = format.log_err().fuse() => transaction, + }; + + buffer.update(&mut cx, |buffer, cx| { + if let Some(transaction) = transaction { + if !buffer.is_singleton() { + buffer.push_transaction(&transaction.0, cx); + } + } + + cx.notify(); + }); + + Ok(()) + }) + } + + fn restart_language_server(&mut self, _: &RestartLanguageServer, cx: &mut ViewContext) { + if let Some(project) = self.project.clone() { + self.buffer.update(cx, |multi_buffer, cx| { + project.update(cx, |project, cx| { + project.restart_language_servers_for_buffers(multi_buffer.all_buffers(), cx); + }); + }) + } + } + + fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext) { + cx.show_character_palette(); + } + + fn refresh_active_diagnostics(&mut self, cx: &mut ViewContext) { + if let Some(active_diagnostics) = self.active_diagnostics.as_mut() { + let buffer = self.buffer.read(cx).snapshot(cx); + let primary_range_start = active_diagnostics.primary_range.start.to_offset(&buffer); + let is_valid = buffer + .diagnostics_in_range::<_, usize>(active_diagnostics.primary_range.clone(), false) + .any(|entry| { + entry.diagnostic.is_primary + && !entry.range.is_empty() + && entry.range.start == primary_range_start + && entry.diagnostic.message == active_diagnostics.primary_message + }); + + if is_valid != active_diagnostics.is_valid { + active_diagnostics.is_valid = is_valid; + let mut new_styles = HashMap::default(); + for (block_id, diagnostic) in &active_diagnostics.blocks { + new_styles.insert( + *block_id, + diagnostic_block_renderer(diagnostic.clone(), is_valid), + ); + } + self.display_map + .update(cx, |display_map, _| display_map.replace_blocks(new_styles)); + } + } + } + + fn activate_diagnostics(&mut self, group_id: usize, cx: &mut ViewContext) -> bool { + self.dismiss_diagnostics(cx); + self.active_diagnostics = self.display_map.update(cx, |display_map, cx| { + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut primary_range = None; + let mut primary_message = None; + let mut group_end = Point::zero(); + let diagnostic_group = buffer + .diagnostic_group::(group_id) + .map(|entry| { + if entry.range.end > group_end { + group_end = entry.range.end; + } + if entry.diagnostic.is_primary { + primary_range = Some(entry.range.clone()); + primary_message = Some(entry.diagnostic.message.clone()); + } + entry + }) + .collect::>(); + let primary_range = primary_range?; + let primary_message = primary_message?; + let primary_range = + buffer.anchor_after(primary_range.start)..buffer.anchor_before(primary_range.end); + + let blocks = display_map + .insert_blocks( + diagnostic_group.iter().map(|entry| { + let diagnostic = entry.diagnostic.clone(); + let message_height = diagnostic.message.lines().count() as u8; + BlockProperties { + style: BlockStyle::Fixed, + position: buffer.anchor_after(entry.range.start), + height: message_height, + render: diagnostic_block_renderer(diagnostic, true), + disposition: BlockDisposition::Below, + } + }), + cx, + ) + .into_iter() + .zip(diagnostic_group.into_iter().map(|entry| entry.diagnostic)) + .collect(); + + Some(ActiveDiagnosticGroup { + primary_range, + primary_message, + blocks, + is_valid: true, + }) + }); + self.active_diagnostics.is_some() + } + + fn dismiss_diagnostics(&mut self, cx: &mut ViewContext) { + if let Some(active_diagnostic_group) = self.active_diagnostics.take() { + self.display_map.update(cx, |display_map, cx| { + display_map.remove_blocks(active_diagnostic_group.blocks.into_keys().collect(), cx); + }); + cx.notify(); + } + } + + pub fn set_selections_from_remote( + &mut self, + selections: Vec>, + pending_selection: Option>, + cx: &mut ViewContext, + ) { + let old_cursor_position = self.selections.newest_anchor().head(); + self.selections.change_with(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, cx); + } + + fn push_to_selection_history(&mut self) { + self.selection_history.push(SelectionHistoryEntry { + selections: self.selections.disjoint_anchors(), + select_next_state: self.select_next_state.clone(), + select_prev_state: self.select_prev_state.clone(), + add_selections_state: self.add_selections_state.clone(), + }); + } + + pub fn transact( + &mut self, + cx: &mut ViewContext, + update: impl FnOnce(&mut Self, &mut ViewContext), + ) -> Option { + self.start_transaction_at(Instant::now(), cx); + update(self, cx); + self.end_transaction_at(Instant::now(), cx) + } + + fn start_transaction_at(&mut self, now: Instant, cx: &mut ViewContext) { + self.end_selection(cx); + if let Some(tx_id) = self + .buffer + .update(cx, |buffer, cx| buffer.start_transaction_at(now, cx)) + { + self.selection_history + .insert_transaction(tx_id, self.selections.disjoint_anchors()); + } + } + + fn end_transaction_at( + &mut self, + now: Instant, + cx: &mut ViewContext, + ) -> Option { + if let Some(tx_id) = self + .buffer + .update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) + { + if let Some((_, end_selections)) = self.selection_history.transaction_mut(tx_id) { + *end_selections = Some(self.selections.disjoint_anchors()); + } else { + error!("unexpectedly ended a transaction that wasn't started by this editor"); + } + + cx.emit(Event::Edited); + Some(tx_id) + } else { + None + } + } + + pub fn fold(&mut self, _: &Fold, cx: &mut ViewContext) { + let mut fold_ranges = Vec::new(); + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + + let selections = self.selections.all_adjusted(cx); + for selection in selections { + let range = selection.range().sorted(); + let buffer_start_row = range.start.row; + + for row in (0..=range.end.row).rev() { + let fold_range = display_map.foldable_range(row); + + if let Some(fold_range) = fold_range { + if fold_range.end.row >= buffer_start_row { + fold_ranges.push(fold_range); + if row <= range.start.row { + break; + } + } + } + } + } + + self.fold_ranges(fold_ranges, true, cx); + } + + pub fn fold_at(&mut self, fold_at: &FoldAt, cx: &mut ViewContext) { + let buffer_row = fold_at.buffer_row; + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + + if let Some(fold_range) = display_map.foldable_range(buffer_row) { + let autoscroll = self + .selections + .all::(cx) + .iter() + .any(|selection| fold_range.overlaps(&selection.range())); + + self.fold_ranges(std::iter::once(fold_range), autoscroll, cx); + } + } + + pub fn unfold_lines(&mut self, _: &UnfoldLines, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + let selections = self.selections.all::(cx); + let ranges = selections + .iter() + .map(|s| { + let range = s.display_range(&display_map).sorted(); + let mut start = range.start.to_point(&display_map); + let mut end = range.end.to_point(&display_map); + start.column = 0; + end.column = buffer.line_len(end.row); + start..end + }) + .collect::>(); + + self.unfold_ranges(ranges, true, true, cx); + } + + pub fn unfold_at(&mut self, unfold_at: &UnfoldAt, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + + let intersection_range = Point::new(unfold_at.buffer_row, 0) + ..Point::new( + unfold_at.buffer_row, + display_map.buffer_snapshot.line_len(unfold_at.buffer_row), + ); + + let autoscroll = self + .selections + .all::(cx) + .iter() + .any(|selection| selection.range().overlaps(&intersection_range)); + + self.unfold_ranges(std::iter::once(intersection_range), true, autoscroll, cx) + } + + pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext) { + let selections = self.selections.all::(cx); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let line_mode = self.selections.line_mode; + let ranges = selections.into_iter().map(|s| { + if line_mode { + let start = Point::new(s.start.row, 0); + let end = Point::new(s.end.row, display_map.buffer_snapshot.line_len(s.end.row)); + start..end + } else { + s.start..s.end + } + }); + self.fold_ranges(ranges, true, cx); + } + + pub fn fold_ranges( + &mut self, + ranges: impl IntoIterator>, + auto_scroll: bool, + cx: &mut ViewContext, + ) { + let mut ranges = ranges.into_iter().peekable(); + if ranges.peek().is_some() { + self.display_map.update(cx, |map, cx| map.fold(ranges, cx)); + + if auto_scroll { + self.request_autoscroll(Autoscroll::fit(), cx); + } + + cx.notify(); + } + } + + pub fn unfold_ranges( + &mut self, + ranges: impl IntoIterator>, + inclusive: bool, + auto_scroll: bool, + cx: &mut ViewContext, + ) { + let mut ranges = ranges.into_iter().peekable(); + if ranges.peek().is_some() { + self.display_map + .update(cx, |map, cx| map.unfold(ranges, inclusive, cx)); + if auto_scroll { + self.request_autoscroll(Autoscroll::fit(), cx); + } + + cx.notify(); + } + } + + pub fn gutter_hover( + &mut self, + GutterHover { hovered }: &GutterHover, + cx: &mut ViewContext, + ) { + self.gutter_hovered = *hovered; + cx.notify(); + } + + pub fn insert_blocks( + &mut self, + blocks: impl IntoIterator>, + autoscroll: Option, + cx: &mut ViewContext, + ) -> Vec { + let blocks = self + .display_map + .update(cx, |display_map, cx| display_map.insert_blocks(blocks, cx)); + if let Some(autoscroll) = autoscroll { + self.request_autoscroll(autoscroll, cx); + } + blocks + } + + pub fn replace_blocks( + &mut self, + blocks: HashMap, + autoscroll: Option, + cx: &mut ViewContext, + ) { + self.display_map + .update(cx, |display_map, _| display_map.replace_blocks(blocks)); + if let Some(autoscroll) = autoscroll { + self.request_autoscroll(autoscroll, cx); + } + } + + pub fn remove_blocks( + &mut self, + block_ids: HashSet, + autoscroll: Option, + cx: &mut ViewContext, + ) { + self.display_map.update(cx, |display_map, cx| { + display_map.remove_blocks(block_ids, cx) + }); + if let Some(autoscroll) = autoscroll { + self.request_autoscroll(autoscroll, cx); + } + } + + pub fn longest_row(&self, cx: &mut AppContext) -> u32 { + self.display_map + .update(cx, |map, cx| map.snapshot(cx)) + .longest_row() + } + + pub fn max_point(&self, cx: &mut AppContext) -> DisplayPoint { + self.display_map + .update(cx, |map, cx| map.snapshot(cx)) + .max_point() + } + + pub fn text(&self, cx: &AppContext) -> String { + self.buffer.read(cx).read(cx).text() + } + + pub fn set_text(&mut self, text: impl Into>, cx: &mut ViewContext) { + self.transact(cx, |this, cx| { + this.buffer + .read(cx) + .as_singleton() + .expect("you can only call set_text on editors for singleton buffers") + .update(cx, |buffer, cx| buffer.set_text(text, cx)); + }); + } + + pub fn display_text(&self, cx: &mut AppContext) -> String { + self.display_map + .update(cx, |map, cx| map.snapshot(cx)) + .text() + } + + pub fn wrap_guides(&self, cx: &AppContext) -> SmallVec<[(usize, bool); 2]> { + let mut wrap_guides = smallvec::smallvec![]; + + if self.show_wrap_guides == Some(false) { + return wrap_guides; + } + + let settings = self.buffer.read(cx).settings_at(0, cx); + if settings.show_wrap_guides { + if let SoftWrap::Column(soft_wrap) = self.soft_wrap_mode(cx) { + wrap_guides.push((soft_wrap as usize, true)); + } + wrap_guides.extend(settings.wrap_guides.iter().map(|guide| (*guide, false))) + } + + wrap_guides + } + + pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap { + let settings = self.buffer.read(cx).settings_at(0, cx); + let mode = self + .soft_wrap_mode_override + .unwrap_or_else(|| settings.soft_wrap); + match mode { + language_settings::SoftWrap::None => SoftWrap::None, + language_settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth, + language_settings::SoftWrap::PreferredLineLength => { + SoftWrap::Column(settings.preferred_line_length) + } + } + } + + pub fn set_soft_wrap_mode( + &mut self, + mode: language_settings::SoftWrap, + cx: &mut ViewContext, + ) { + self.soft_wrap_mode_override = Some(mode); + cx.notify(); + } + + pub fn set_wrap_width(&self, width: Option, cx: &mut AppContext) -> bool { + self.display_map + .update(cx, |map, cx| map.set_wrap_width(width, cx)) + } + + pub fn toggle_soft_wrap(&mut self, _: &ToggleSoftWrap, cx: &mut ViewContext) { + if self.soft_wrap_mode_override.is_some() { + self.soft_wrap_mode_override.take(); + } else { + let soft_wrap = match self.soft_wrap_mode(cx) { + SoftWrap::None => language_settings::SoftWrap::EditorWidth, + SoftWrap::EditorWidth | SoftWrap::Column(_) => language_settings::SoftWrap::None, + }; + self.soft_wrap_mode_override = Some(soft_wrap); + } + cx.notify(); + } + + pub fn set_show_gutter(&mut self, show_gutter: bool, cx: &mut ViewContext) { + self.show_gutter = show_gutter; + cx.notify(); + } + + pub fn set_show_wrap_guides(&mut self, show_gutter: bool, cx: &mut ViewContext) { + self.show_wrap_guides = Some(show_gutter); + cx.notify(); + } + + pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext) { + if let Some(buffer) = self.buffer().read(cx).as_singleton() { + if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) { + cx.reveal_path(&file.abs_path(cx)); + } + } + } + + pub fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext) { + if let Some(buffer) = self.buffer().read(cx).as_singleton() { + if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) { + if let Some(path) = file.abs_path(cx).to_str() { + cx.write_to_clipboard(ClipboardItem::new(path.to_string())); + } + } + } + } + + pub fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext) { + if let Some(buffer) = self.buffer().read(cx).as_singleton() { + if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) { + if let Some(path) = file.path().to_str() { + cx.write_to_clipboard(ClipboardItem::new(path.to_string())); + } + } + } + } + + pub fn highlight_rows(&mut self, rows: Option>) { + self.highlighted_rows = rows; + } + + pub fn highlighted_rows(&self) -> Option> { + self.highlighted_rows.clone() + } + + pub fn highlight_background( + &mut self, + ranges: Vec>, + color_fetcher: fn(&Theme) -> Color, + cx: &mut ViewContext, + ) { + self.background_highlights + .insert(TypeId::of::(), (color_fetcher, ranges)); + cx.notify(); + } + + pub fn highlight_inlay_background( + &mut self, + ranges: Vec, + color_fetcher: fn(&Theme) -> Color, + cx: &mut ViewContext, + ) { + // TODO: no actual highlights happen for inlays currently, find a way to do that + self.inlay_background_highlights + .insert(Some(TypeId::of::()), (color_fetcher, ranges)); + cx.notify(); + } + + pub fn clear_background_highlights( + &mut self, + cx: &mut ViewContext, + ) -> Option { + let text_highlights = self.background_highlights.remove(&TypeId::of::()); + let inlay_highlights = self + .inlay_background_highlights + .remove(&Some(TypeId::of::())); + if text_highlights.is_some() || inlay_highlights.is_some() { + cx.notify(); + } + text_highlights + } + + #[cfg(feature = "test-support")] + pub fn all_text_background_highlights( + &mut self, + cx: &mut ViewContext, + ) -> Vec<(Range, Color)> { + let snapshot = self.snapshot(cx); + let buffer = &snapshot.buffer_snapshot; + let start = buffer.anchor_before(0); + let end = buffer.anchor_after(buffer.len()); + let theme = theme::current(cx); + self.background_highlights_in_range(start..end, &snapshot, theme.as_ref()) + } + + fn document_highlights_for_position<'a>( + &'a self, + position: Anchor, + buffer: &'a MultiBufferSnapshot, + ) -> impl 'a + Iterator> { + let read_highlights = self + .background_highlights + .get(&TypeId::of::()) + .map(|h| &h.1); + let write_highlights = self + .background_highlights + .get(&TypeId::of::()) + .map(|h| &h.1); + let left_position = position.bias_left(buffer); + let right_position = position.bias_right(buffer); + read_highlights + .into_iter() + .chain(write_highlights) + .flat_map(move |ranges| { + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe.end.cmp(&left_position, buffer); + if cmp.is_ge() { + Ordering::Greater + } else { + Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + + let right_position = right_position.clone(); + ranges[start_ix..] + .iter() + .take_while(move |range| range.start.cmp(&right_position, buffer).is_le()) + }) + } + + pub fn background_highlights_in_range( + &self, + search_range: Range, + display_snapshot: &DisplaySnapshot, + theme: &Theme, + ) -> Vec<(Range, Color)> { + let mut results = Vec::new(); + for (color_fetcher, ranges) in self.background_highlights.values() { + let color = color_fetcher(theme); + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe + .end + .cmp(&search_range.start, &display_snapshot.buffer_snapshot); + if cmp.is_gt() { + Ordering::Greater + } else { + Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + if range + .start + .cmp(&search_range.end, &display_snapshot.buffer_snapshot) + .is_ge() + { + break; + } + + let start = range.start.to_display_point(&display_snapshot); + let end = range.end.to_display_point(&display_snapshot); + results.push((start..end, color)) + } + } + results + } + + pub fn background_highlight_row_ranges( + &self, + search_range: Range, + display_snapshot: &DisplaySnapshot, + count: usize, + ) -> Vec> { + let mut results = Vec::new(); + let Some((_, ranges)) = self.background_highlights.get(&TypeId::of::()) else { + return vec![]; + }; + + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe + .end + .cmp(&search_range.start, &display_snapshot.buffer_snapshot); + if cmp.is_gt() { + Ordering::Greater + } else { + Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + let mut push_region = |start: Option, end: Option| { + if let (Some(start_display), Some(end_display)) = (start, end) { + results.push( + start_display.to_display_point(display_snapshot) + ..=end_display.to_display_point(display_snapshot), + ); + } + }; + let mut start_row: Option = None; + let mut end_row: Option = None; + if ranges.len() > count { + return Vec::new(); + } + for range in &ranges[start_ix..] { + if range + .start + .cmp(&search_range.end, &display_snapshot.buffer_snapshot) + .is_ge() + { + break; + } + let end = range.end.to_point(&display_snapshot.buffer_snapshot); + if let Some(current_row) = &end_row { + if end.row == current_row.row { + continue; + } + } + let start = range.start.to_point(&display_snapshot.buffer_snapshot); + if start_row.is_none() { + assert_eq!(end_row, None); + start_row = Some(start); + end_row = Some(end); + continue; + } + if let Some(current_end) = end_row.as_mut() { + if start.row > current_end.row + 1 { + push_region(start_row, end_row); + start_row = Some(start); + end_row = Some(end); + } else { + // Merge two hunks. + *current_end = end; + } + } else { + unreachable!(); + } + } + // We might still have a hunk that was not rendered (if there was a search hit on the last line) + push_region(start_row, end_row); + results + } + + pub fn highlight_text( + &mut self, + ranges: Vec>, + style: HighlightStyle, + cx: &mut ViewContext, + ) { + self.display_map.update(cx, |map, _| { + map.highlight_text(TypeId::of::(), ranges, style) + }); + cx.notify(); + } + + pub fn highlight_inlays( + &mut self, + highlights: Vec, + style: HighlightStyle, + cx: &mut ViewContext, + ) { + self.display_map.update(cx, |map, _| { + map.highlight_inlays(TypeId::of::(), highlights, style) + }); + cx.notify(); + } + + pub fn text_highlights<'a, T: 'static>( + &'a self, + cx: &'a AppContext, + ) -> Option<(HighlightStyle, &'a [Range])> { + self.display_map.read(cx).text_highlights(TypeId::of::()) + } + + pub fn clear_highlights(&mut self, cx: &mut ViewContext) { + let cleared = self + .display_map + .update(cx, |map, _| map.clear_highlights(TypeId::of::())); + if cleared { + cx.notify(); + } + } + + pub fn show_local_cursors(&self, cx: &AppContext) -> bool { + self.blink_manager.read(cx).visible() && self.focused + } + + fn on_buffer_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { + cx.notify(); + } + + fn on_buffer_event( + &mut self, + multibuffer: ModelHandle, + event: &multi_buffer::Event, + cx: &mut ViewContext, + ) { + match event { + multi_buffer::Event::Edited { + sigleton_buffer_edited, + } => { + self.refresh_active_diagnostics(cx); + self.refresh_code_actions(cx); + if self.has_active_copilot_suggestion(cx) { + self.update_visible_copilot_suggestion(cx); + } + cx.emit(Event::BufferEdited); + + if *sigleton_buffer_edited { + if let Some(project) = &self.project { + let project = project.read(cx); + let languages_affected = multibuffer + .read(cx) + .all_buffers() + .into_iter() + .filter_map(|buffer| { + let buffer = buffer.read(cx); + let language = buffer.language()?; + if project.is_local() + && project.language_servers_for_buffer(buffer, cx).count() == 0 + { + None + } else { + Some(language) + } + }) + .cloned() + .collect::>(); + if !languages_affected.is_empty() { + self.refresh_inlay_hints( + InlayHintRefreshReason::BufferEdited(languages_affected), + cx, + ); + } + } + } + } + multi_buffer::Event::ExcerptsAdded { + buffer, + predecessor, + excerpts, + } => { + cx.emit(Event::ExcerptsAdded { + buffer: buffer.clone(), + predecessor: *predecessor, + excerpts: excerpts.clone(), + }); + self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); + } + multi_buffer::Event::ExcerptsRemoved { ids } => { + self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx); + cx.emit(Event::ExcerptsRemoved { ids: ids.clone() }) + } + multi_buffer::Event::Reparsed => cx.emit(Event::Reparsed), + multi_buffer::Event::DirtyChanged => cx.emit(Event::DirtyChanged), + multi_buffer::Event::Saved => cx.emit(Event::Saved), + multi_buffer::Event::FileHandleChanged => cx.emit(Event::TitleChanged), + multi_buffer::Event::Reloaded => cx.emit(Event::TitleChanged), + multi_buffer::Event::DiffBaseChanged => cx.emit(Event::DiffBaseChanged), + multi_buffer::Event::Closed => cx.emit(Event::Closed), + multi_buffer::Event::DiagnosticsUpdated => { + self.refresh_active_diagnostics(cx); + } + _ => {} + }; + } + + fn on_display_map_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { + cx.notify(); + } + + fn settings_changed(&mut self, cx: &mut ViewContext) { + self.refresh_copilot_suggestions(true, cx); + self.refresh_inlay_hints( + InlayHintRefreshReason::SettingsChange(inlay_hint_settings( + self.selections.newest_anchor().head(), + &self.buffer.read(cx).snapshot(cx), + cx, + )), + cx, + ); + } + + pub fn set_searchable(&mut self, searchable: bool) { + self.searchable = searchable; + } + + pub fn searchable(&self) -> bool { + self.searchable + } + + fn open_excerpts(workspace: &mut Workspace, _: &OpenExcerpts, cx: &mut ViewContext) { + let active_item = workspace.active_item(cx); + let editor_handle = if let Some(editor) = active_item + .as_ref() + .and_then(|item| item.act_as::(cx)) + { + editor + } else { + cx.propagate_action(); + return; + }; + + let editor = editor_handle.read(cx); + let buffer = editor.buffer.read(cx); + if buffer.is_singleton() { + cx.propagate_action(); + return; + } + + let mut new_selections_by_buffer = HashMap::default(); + for selection in editor.selections.all::(cx) { + for (buffer, mut range, _) in + buffer.range_to_buffer_ranges(selection.start..selection.end, cx) + { + if selection.reversed { + mem::swap(&mut range.start, &mut range.end); + } + new_selections_by_buffer + .entry(buffer) + .or_insert(Vec::new()) + .push(range) + } + } + + editor_handle.update(cx, |editor, cx| { + editor.push_to_nav_history(editor.selections.newest_anchor().head(), None, cx); + }); + let pane = workspace.active_pane().clone(); + pane.update(cx, |pane, _| pane.disable_history()); + + // We defer the pane interaction because we ourselves are a workspace item + // and activating a new item causes the pane to call a method on us reentrantly, + // which panics if we're on the stack. + cx.defer(move |workspace, cx| { + for (buffer, ranges) in new_selections_by_buffer.into_iter() { + let editor = workspace.open_project_item::(buffer, cx); + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::newest()), cx, |s| { + s.select_ranges(ranges); + }); + }); + } + + pane.update(cx, |pane, _| pane.enable_history()); + }); + } + + fn jump( + workspace: &mut Workspace, + path: ProjectPath, + position: Point, + anchor: language::Anchor, + cx: &mut ViewContext, + ) { + let editor = workspace.open_path(path, None, true, cx); + cx.spawn(|_, mut cx| async move { + let editor = editor + .await? + .downcast::() + .ok_or_else(|| anyhow!("opened item was not an editor"))? + .downgrade(); + editor.update(&mut cx, |editor, cx| { + let buffer = editor + .buffer() + .read(cx) + .as_singleton() + .ok_or_else(|| anyhow!("cannot jump in a multi-buffer"))?; + let buffer = buffer.read(cx); + let cursor = if buffer.can_resolve(&anchor) { + language::ToPoint::to_point(&anchor, buffer) + } else { + buffer.clip_point(position, Bias::Left) + }; + + let nav_history = editor.nav_history.take(); + editor.change_selections(Some(Autoscroll::newest()), cx, |s| { + s.select_ranges([cursor..cursor]); + }); + editor.nav_history = nav_history; + + anyhow::Ok(()) + })??; + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + + fn marked_text_ranges(&self, cx: &AppContext) -> Option>> { + let snapshot = self.buffer.read(cx).read(cx); + let (_, ranges) = self.text_highlights::(cx)?; + Some( + ranges + .iter() + .map(move |range| { + range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot) + }) + .collect(), + ) + } + + fn selection_replacement_ranges( + &self, + range: Range, + cx: &AppContext, + ) -> Vec> { + let selections = self.selections.all::(cx); + let newest_selection = selections + .iter() + .max_by_key(|selection| selection.id) + .unwrap(); + let start_delta = range.start.0 as isize - newest_selection.start.0 as isize; + let end_delta = range.end.0 as isize - newest_selection.end.0 as isize; + let snapshot = self.buffer.read(cx).read(cx); + selections + .into_iter() + .map(|mut selection| { + selection.start.0 = + (selection.start.0 as isize).saturating_add(start_delta) as usize; + selection.end.0 = (selection.end.0 as isize).saturating_add(end_delta) as usize; + snapshot.clip_offset_utf16(selection.start, Bias::Left) + ..snapshot.clip_offset_utf16(selection.end, Bias::Right) + }) + .collect() + } + + fn report_copilot_event( + &self, + suggestion_id: Option, + suggestion_accepted: bool, + cx: &AppContext, + ) { + let Some(project) = &self.project else { return }; + + // If None, we are either getting suggestions in a new, unsaved file, or in a file without an extension + let file_extension = self + .buffer + .read(cx) + .as_singleton() + .and_then(|b| b.read(cx).file()) + .and_then(|file| Path::new(file.file_name(cx)).extension()) + .and_then(|e| e.to_str()) + .map(|a| a.to_string()); + + let telemetry = project.read(cx).client().telemetry().clone(); + let telemetry_settings = *settings::get::(cx); + + let event = ClickhouseEvent::Copilot { + suggestion_id, + suggestion_accepted, + file_extension, + }; + telemetry.report_clickhouse_event(event, telemetry_settings); + } + + #[cfg(any(test, feature = "test-support"))] + fn report_editor_event( + &self, + _operation: &'static str, + _file_extension: Option, + _cx: &AppContext, + ) { + } + + #[cfg(not(any(test, feature = "test-support")))] + fn report_editor_event( + &self, + operation: &'static str, + file_extension: Option, + cx: &AppContext, + ) { + let Some(project) = &self.project else { return }; + + // If None, we are in a file without an extension + let file = self + .buffer + .read(cx) + .as_singleton() + .and_then(|b| b.read(cx).file()); + let file_extension = file_extension.or(file + .as_ref() + .and_then(|file| Path::new(file.file_name(cx)).extension()) + .and_then(|e| e.to_str()) + .map(|a| a.to_string())); + + let vim_mode = cx + .global::() + .raw_user_settings() + .get("vim_mode") + == Some(&serde_json::Value::Bool(true)); + let telemetry_settings = *settings::get::(cx); + let copilot_enabled = all_language_settings(file, cx).copilot_enabled(None, None); + let copilot_enabled_for_language = self + .buffer + .read(cx) + .settings_at(0, cx) + .show_copilot_suggestions; + + let telemetry = project.read(cx).client().telemetry().clone(); + let event = ClickhouseEvent::Editor { + file_extension, + vim_mode, + operation, + copilot_enabled, + copilot_enabled_for_language, + }; + telemetry.report_clickhouse_event(event, telemetry_settings) + } + + /// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines, + /// with each line being an array of {text, highlight} objects. + fn copy_highlight_json(&mut self, _: &CopyHighlightJson, cx: &mut ViewContext) { + let Some(buffer) = self.buffer.read(cx).as_singleton() else { + return; + }; + + #[derive(Serialize)] + struct Chunk<'a> { + text: String, + highlight: Option<&'a str>, + } + + let snapshot = buffer.read(cx).snapshot(); + let range = self + .selected_text_range(cx) + .and_then(|selected_range| { + if selected_range.is_empty() { + None + } else { + Some(selected_range) + } + }) + .unwrap_or_else(|| 0..snapshot.len()); + + let chunks = snapshot.chunks(range, true); + let mut lines = Vec::new(); + let mut line: VecDeque = VecDeque::new(); + + let theme = &theme::current(cx).editor.syntax; + + for chunk in chunks { + let highlight = chunk.syntax_highlight_id.and_then(|id| id.name(theme)); + let mut chunk_lines = chunk.text.split("\n").peekable(); + while let Some(text) = chunk_lines.next() { + let mut merged_with_last_token = false; + if let Some(last_token) = line.back_mut() { + if last_token.highlight == highlight { + last_token.text.push_str(text); + merged_with_last_token = true; + } + } + + if !merged_with_last_token { + line.push_back(Chunk { + text: text.into(), + highlight, + }); + } + + if chunk_lines.peek().is_some() { + if line.len() > 1 && line.front().unwrap().text.is_empty() { + line.pop_front(); + } + if line.len() > 1 && line.back().unwrap().text.is_empty() { + line.pop_back(); + } + + lines.push(mem::take(&mut line)); + } + } + } + + let Some(lines) = serde_json::to_string_pretty(&lines).log_err() else { + return; + }; + cx.write_to_clipboard(ClipboardItem::new(lines)); + } + + pub fn inlay_hint_cache(&self) -> &InlayHintCache { + &self.inlay_hint_cache + } + + pub fn replay_insert_event( + &mut self, + text: &str, + relative_utf16_range: Option>, + cx: &mut ViewContext, + ) { + if !self.input_enabled { + cx.emit(Event::InputIgnored { text: text.into() }); + return; + } + if let Some(relative_utf16_range) = relative_utf16_range { + let selections = self.selections.all::(cx); + self.change_selections(None, cx, |s| { + let new_ranges = selections.into_iter().map(|range| { + let start = OffsetUtf16( + range + .head() + .0 + .saturating_add_signed(relative_utf16_range.start), + ); + let end = OffsetUtf16( + range + .head() + .0 + .saturating_add_signed(relative_utf16_range.end), + ); + start..end + }); + s.select_ranges(new_ranges); + }); + } + + self.handle_input(text, cx); + } + + pub fn supports_inlay_hints(&self, cx: &AppContext) -> bool { + let Some(project) = self.project.as_ref() else { + return false; + }; + let project = project.read(cx); + + let mut supports = false; + self.buffer().read(cx).for_each_buffer(|buffer| { + if !supports { + supports = project + .language_servers_for_buffer(buffer.read(cx), cx) + .any( + |(_, server)| match server.capabilities().inlay_hint_provider { + Some(lsp::OneOf::Left(enabled)) => enabled, + Some(lsp::OneOf::Right(_)) => true, + None => false, + }, + ) + } + }); + supports + } +} + +pub trait CollaborationHub { + fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap; + fn user_participant_indices<'a>( + &self, + cx: &'a AppContext, + ) -> &'a HashMap; +} + +impl CollaborationHub for ModelHandle { + fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap { + self.read(cx).collaborators() + } + + fn user_participant_indices<'a>( + &self, + cx: &'a AppContext, + ) -> &'a HashMap { + self.read(cx).user_store().read(cx).participant_indices() + } +} + +fn inlay_hint_settings( + location: Anchor, + snapshot: &MultiBufferSnapshot, + cx: &mut ViewContext<'_, '_, Editor>, +) -> InlayHintSettings { + let file = snapshot.file_at(location); + let language = snapshot.language_at(location); + let settings = all_language_settings(file, cx); + settings + .language(language.map(|l| l.name()).as_deref()) + .inlay_hints +} + +fn consume_contiguous_rows( + contiguous_row_selections: &mut Vec>, + selection: &Selection, + display_map: &DisplaySnapshot, + selections: &mut std::iter::Peekable>>, +) -> (u32, u32) { + contiguous_row_selections.push(selection.clone()); + let start_row = selection.start.row; + let mut end_row = ending_row(selection, display_map); + + while let Some(next_selection) = selections.peek() { + if next_selection.start.row <= end_row { + end_row = ending_row(next_selection, display_map); + contiguous_row_selections.push(selections.next().unwrap().clone()); + } else { + break; + } + } + (start_row, end_row) +} + +fn ending_row(next_selection: &Selection, display_map: &DisplaySnapshot) -> u32 { + if next_selection.end.column > 0 || next_selection.is_empty() { + display_map.next_line_boundary(next_selection.end).0.row + 1 + } else { + next_selection.end.row + } +} + +impl EditorSnapshot { + pub fn remote_selections_in_range<'a>( + &'a self, + range: &'a Range, + collaboration_hub: &dyn CollaborationHub, + cx: &'a AppContext, + ) -> impl 'a + Iterator { + let participant_indices = collaboration_hub.user_participant_indices(cx); + let collaborators_by_peer_id = collaboration_hub.collaborators(cx); + let collaborators_by_replica_id = collaborators_by_peer_id + .iter() + .map(|(_, collaborator)| (collaborator.replica_id, collaborator)) + .collect::>(); + self.buffer_snapshot + .remote_selections_in_range(range) + .filter_map(move |(replica_id, line_mode, cursor_shape, selection)| { + let collaborator = collaborators_by_replica_id.get(&replica_id)?; + let participant_index = participant_indices.get(&collaborator.user_id).copied(); + Some(RemoteSelection { + replica_id, + selection, + cursor_shape, + line_mode, + participant_index, + peer_id: collaborator.peer_id, + }) + }) + } + + pub fn language_at(&self, position: T) -> Option<&Arc> { + self.display_snapshot.buffer_snapshot.language_at(position) + } + + pub fn is_focused(&self) -> bool { + self.is_focused + } + + pub fn placeholder_text(&self) -> Option<&Arc> { + self.placeholder_text.as_ref() + } + + pub fn scroll_position(&self) -> Vector2F { + self.scroll_anchor.scroll_position(&self.display_snapshot) + } +} + +impl Deref for EditorSnapshot { + type Target = DisplaySnapshot; + + fn deref(&self) -> &Self::Target { + &self.display_snapshot + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Event { + InputIgnored { + text: Arc, + }, + InputHandled { + utf16_range_to_replace: Option>, + text: Arc, + }, + ExcerptsAdded { + buffer: ModelHandle, + predecessor: ExcerptId, + excerpts: Vec<(ExcerptId, ExcerptRange)>, + }, + ExcerptsRemoved { + ids: Vec, + }, + BufferEdited, + Edited, + Reparsed, + Focused, + Blurred, + DirtyChanged, + Saved, + TitleChanged, + DiffBaseChanged, + SelectionsChanged { + local: bool, + }, + ScrollPositionChanged { + local: bool, + autoscroll: bool, + }, + Closed, +} + +pub struct EditorFocused(pub ViewHandle); +pub struct EditorBlurred(pub ViewHandle); +pub struct EditorReleased(pub WeakViewHandle); + +impl Entity for Editor { + type Event = Event; + + fn release(&mut self, cx: &mut AppContext) { + cx.emit_global(EditorReleased(self.handle.clone())); + } +} + +impl View for Editor { + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + let style = self.style(cx); + let font_changed = self.display_map.update(cx, |map, cx| { + map.set_fold_ellipses_color(style.folds.ellipses.text_color); + map.set_font(style.text.font_id, style.text.font_size, cx) + }); + + if font_changed { + cx.defer(move |editor, cx: &mut ViewContext| { + hide_hover(editor, cx); + hide_link_definition(editor, cx); + }); + } + + Stack::new() + .with_child(EditorElement::new(style.clone())) + .with_child(ChildView::new(&self.mouse_context_menu, cx)) + .into_any() + } + + fn ui_name() -> &'static str { + "Editor" + } + + fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext) { + if cx.is_self_focused() { + let focused_event = EditorFocused(cx.handle()); + cx.emit(Event::Focused); + cx.emit_global(focused_event); + } + if let Some(rename) = self.pending_rename.as_ref() { + cx.focus(&rename.editor); + } else if cx.is_self_focused() || !focused.is::() { + if !self.focused { + self.blink_manager.update(cx, BlinkManager::enable); + } + self.focused = true; + self.buffer.update(cx, |buffer, cx| { + buffer.finalize_last_transaction(cx); + if self.leader_peer_id.is_none() { + buffer.set_active_selections( + &self.selections.disjoint_anchors(), + self.selections.line_mode, + self.cursor_shape, + cx, + ); + } + }); + } + } + + fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + let blurred_event = EditorBlurred(cx.handle()); + cx.emit_global(blurred_event); + self.focused = false; + self.blink_manager.update(cx, BlinkManager::disable); + self.buffer + .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); + self.hide_context_menu(cx); + hide_hover(self, cx); + cx.emit(Event::Blurred); + cx.notify(); + } + + fn modifiers_changed( + &mut self, + event: &gpui::platform::ModifiersChangedEvent, + cx: &mut ViewContext, + ) -> bool { + let pending_selection = self.has_pending_selection(); + + if let Some(point) = &self.link_go_to_definition_state.last_trigger_point { + if event.cmd && !pending_selection { + let point = point.clone(); + let snapshot = self.snapshot(cx); + let kind = point.definition_kind(event.shift); + + show_link_definition(kind, self, point, snapshot, cx); + return false; + } + } + + { + if self.link_go_to_definition_state.symbol_range.is_some() + || !self.link_go_to_definition_state.definitions.is_empty() + { + self.link_go_to_definition_state.symbol_range.take(); + self.link_go_to_definition_state.definitions.clear(); + cx.notify(); + } + + self.link_go_to_definition_state.task = None; + + self.clear_highlights::(cx); + } + + false + } + + fn update_keymap_context(&self, keymap: &mut KeymapContext, cx: &AppContext) { + Self::reset_to_default_keymap_context(keymap); + let mode = match self.mode { + EditorMode::SingleLine => "single_line", + EditorMode::AutoHeight { .. } => "auto_height", + EditorMode::Full => "full", + }; + keymap.add_key("mode", mode); + if self.pending_rename.is_some() { + keymap.add_identifier("renaming"); + } + if self.context_menu_visible() { + match self.context_menu.read().as_ref() { + Some(ContextMenu::Completions(_)) => { + keymap.add_identifier("menu"); + keymap.add_identifier("showing_completions") + } + Some(ContextMenu::CodeActions(_)) => { + keymap.add_identifier("menu"); + keymap.add_identifier("showing_code_actions") + } + None => {} + } + } + + for layer in self.keymap_context_layers.values() { + keymap.extend(layer); + } + + if let Some(extension) = self + .buffer + .read(cx) + .as_singleton() + .and_then(|buffer| buffer.read(cx).file()?.path().extension()?.to_str()) + { + keymap.add_key("extension", extension.to_string()); + } + } + + fn text_for_range(&self, range_utf16: Range, cx: &AppContext) -> Option { + Some( + self.buffer + .read(cx) + .read(cx) + .text_for_range(OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end)) + .collect(), + ) + } + + fn selected_text_range(&self, cx: &AppContext) -> Option> { + // Prevent the IME menu from appearing when holding down an alphabetic key + // while input is disabled. + if !self.input_enabled { + return None; + } + + let range = self.selections.newest::(cx).range(); + Some(range.start.0..range.end.0) + } + + fn marked_text_range(&self, cx: &AppContext) -> Option> { + let snapshot = self.buffer.read(cx).read(cx); + let range = self.text_highlights::(cx)?.1.get(0)?; + Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0) + } + + fn unmark_text(&mut self, cx: &mut ViewContext) { + self.clear_highlights::(cx); + self.ime_transaction.take(); + } + + fn replace_text_in_range( + &mut self, + range_utf16: Option>, + text: &str, + cx: &mut ViewContext, + ) { + if !self.input_enabled { + cx.emit(Event::InputIgnored { text: text.into() }); + return; + } + + self.transact(cx, |this, cx| { + let new_selected_ranges = if let Some(range_utf16) = range_utf16 { + let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end); + Some(this.selection_replacement_ranges(range_utf16, cx)) + } else { + this.marked_text_ranges(cx) + }; + + let range_to_replace = new_selected_ranges.as_ref().and_then(|ranges_to_replace| { + let newest_selection_id = this.selections.newest_anchor().id; + this.selections + .all::(cx) + .iter() + .zip(ranges_to_replace.iter()) + .find_map(|(selection, range)| { + if selection.id == newest_selection_id { + Some( + (range.start.0 as isize - selection.head().0 as isize) + ..(range.end.0 as isize - selection.head().0 as isize), + ) + } else { + None + } + }) + }); + + cx.emit(Event::InputHandled { + utf16_range_to_replace: range_to_replace, + text: text.into(), + }); + + if let Some(new_selected_ranges) = new_selected_ranges { + this.change_selections(None, cx, |selections| { + selections.select_ranges(new_selected_ranges) + }); + } + + this.handle_input(text, cx); + }); + + if let Some(transaction) = self.ime_transaction { + self.buffer.update(cx, |buffer, cx| { + buffer.group_until_transaction(transaction, cx); + }); + } + + self.unmark_text(cx); + } + + fn replace_and_mark_text_in_range( + &mut self, + range_utf16: Option>, + text: &str, + new_selected_range_utf16: Option>, + cx: &mut ViewContext, + ) { + if !self.input_enabled { + cx.emit(Event::InputIgnored { text: text.into() }); + return; + } + + let transaction = self.transact(cx, |this, cx| { + let ranges_to_replace = if let Some(mut marked_ranges) = this.marked_text_ranges(cx) { + let snapshot = this.buffer.read(cx).read(cx); + if let Some(relative_range_utf16) = range_utf16.as_ref() { + for marked_range in &mut marked_ranges { + marked_range.end.0 = marked_range.start.0 + relative_range_utf16.end; + marked_range.start.0 += relative_range_utf16.start; + marked_range.start = + snapshot.clip_offset_utf16(marked_range.start, Bias::Left); + marked_range.end = + snapshot.clip_offset_utf16(marked_range.end, Bias::Right); + } + } + Some(marked_ranges) + } else if let Some(range_utf16) = range_utf16 { + let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end); + Some(this.selection_replacement_ranges(range_utf16, cx)) + } else { + None + }; + + let range_to_replace = ranges_to_replace.as_ref().and_then(|ranges_to_replace| { + let newest_selection_id = this.selections.newest_anchor().id; + this.selections + .all::(cx) + .iter() + .zip(ranges_to_replace.iter()) + .find_map(|(selection, range)| { + if selection.id == newest_selection_id { + Some( + (range.start.0 as isize - selection.head().0 as isize) + ..(range.end.0 as isize - selection.head().0 as isize), + ) + } else { + None + } + }) + }); + + cx.emit(Event::InputHandled { + utf16_range_to_replace: range_to_replace, + text: text.into(), + }); + + if let Some(ranges) = ranges_to_replace { + this.change_selections(None, cx, |s| s.select_ranges(ranges)); + } + + let marked_ranges = { + let snapshot = this.buffer.read(cx).read(cx); + this.selections + .disjoint_anchors() + .iter() + .map(|selection| { + selection.start.bias_left(&*snapshot)..selection.end.bias_right(&*snapshot) + }) + .collect::>() + }; + + if text.is_empty() { + this.unmark_text(cx); + } else { + this.highlight_text::( + marked_ranges.clone(), + this.style(cx).composition_mark, + cx, + ); + } + + this.handle_input(text, cx); + + if let Some(new_selected_range) = new_selected_range_utf16 { + let snapshot = this.buffer.read(cx).read(cx); + let new_selected_ranges = marked_ranges + .into_iter() + .map(|marked_range| { + let insertion_start = marked_range.start.to_offset_utf16(&snapshot).0; + let new_start = OffsetUtf16(new_selected_range.start + insertion_start); + let new_end = OffsetUtf16(new_selected_range.end + insertion_start); + snapshot.clip_offset_utf16(new_start, Bias::Left) + ..snapshot.clip_offset_utf16(new_end, Bias::Right) + }) + .collect::>(); + + drop(snapshot); + this.change_selections(None, cx, |selections| { + selections.select_ranges(new_selected_ranges) + }); + } + }); + + self.ime_transaction = self.ime_transaction.or(transaction); + if let Some(transaction) = self.ime_transaction { + self.buffer.update(cx, |buffer, cx| { + buffer.group_until_transaction(transaction, cx); + }); + } + + if self.text_highlights::(cx).is_none() { + self.ime_transaction.take(); + } + } +} + +fn build_style( + settings: &ThemeSettings, + get_field_editor_theme: Option<&GetFieldEditorTheme>, + override_text_style: Option<&OverrideTextStyle>, + cx: &AppContext, +) -> EditorStyle { + let font_cache = cx.font_cache(); + let line_height_scalar = settings.line_height(); + let theme_id = settings.theme.meta.id; + let mut theme = settings.theme.editor.clone(); + let mut style = if let Some(get_field_editor_theme) = get_field_editor_theme { + let field_editor_theme = get_field_editor_theme(&settings.theme); + theme.text_color = field_editor_theme.text.color; + theme.selection = field_editor_theme.selection; + theme.background = field_editor_theme + .container + .background_color + .unwrap_or_default(); + EditorStyle { + text: field_editor_theme.text, + placeholder_text: field_editor_theme.placeholder_text, + line_height_scalar, + theme, + theme_id, + } + } else { + let font_family_id = settings.buffer_font_family; + let font_family_name = cx.font_cache().family_name(font_family_id).unwrap(); + let font_properties = Default::default(); + let font_id = font_cache + .select_font(font_family_id, &font_properties) + .unwrap(); + let font_size = settings.buffer_font_size(cx); + EditorStyle { + text: TextStyle { + color: settings.theme.editor.text_color, + font_family_name, + font_family_id, + font_id, + font_size, + font_properties, + underline: Default::default(), + soft_wrap: false, + }, + placeholder_text: None, + line_height_scalar, + theme, + theme_id, + } + }; + + if let Some(highlight_style) = override_text_style.and_then(|build_style| build_style(&style)) { + if let Some(highlighted) = style + .text + .clone() + .highlight(highlight_style, font_cache) + .log_err() + { + style.text = highlighted; + } + } + + style +} + +trait SelectionExt { + fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range; + fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range; + fn display_range(&self, map: &DisplaySnapshot) -> Range; + fn spanned_rows(&self, include_end_if_at_line_start: bool, map: &DisplaySnapshot) + -> Range; +} + +impl SelectionExt for Selection { + fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range { + let start = self.start.to_point(buffer); + let end = self.end.to_point(buffer); + if self.reversed { + end..start + } else { + start..end + } + } + + fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range { + let start = self.start.to_offset(buffer); + let end = self.end.to_offset(buffer); + if self.reversed { + end..start + } else { + start..end + } + } + + fn display_range(&self, map: &DisplaySnapshot) -> Range { + let start = self + .start + .to_point(&map.buffer_snapshot) + .to_display_point(map); + let end = self + .end + .to_point(&map.buffer_snapshot) + .to_display_point(map); + if self.reversed { + end..start + } else { + start..end + } + } + + fn spanned_rows( + &self, + include_end_if_at_line_start: bool, + map: &DisplaySnapshot, + ) -> Range { + let start = self.start.to_point(&map.buffer_snapshot); + let mut end = self.end.to_point(&map.buffer_snapshot); + if !include_end_if_at_line_start && start.row != end.row && end.column == 0 { + end.row -= 1; + } + + let buffer_start = map.prev_line_boundary(start).0; + let buffer_end = map.next_line_boundary(end).0; + buffer_start.row..buffer_end.row + 1 + } +} + +impl InvalidationStack { + fn invalidate(&mut self, selections: &[Selection], buffer: &MultiBufferSnapshot) + where + S: Clone + ToOffset, + { + while let Some(region) = self.last() { + let all_selections_inside_invalidation_ranges = + if selections.len() == region.ranges().len() { + selections + .iter() + .zip(region.ranges().iter().map(|r| r.to_offset(buffer))) + .all(|(selection, invalidation_range)| { + let head = selection.head().to_offset(buffer); + invalidation_range.start <= head && invalidation_range.end >= head + }) + } else { + false + }; + + if all_selections_inside_invalidation_ranges { + break; + } else { + self.pop(); + } + } + } +} + +impl Default for InvalidationStack { + fn default() -> Self { + Self(Default::default()) + } +} + +impl Deref for InvalidationStack { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for InvalidationStack { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl InvalidationRegion for SnippetState { + fn ranges(&self) -> &[Range] { + &self.ranges[self.active_index] + } +} + +impl Deref for EditorStyle { + type Target = theme::Editor; + + fn deref(&self) -> &Self::Target { + &self.theme + } +} + +pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> RenderBlock { + let mut highlighted_lines = Vec::new(); + + for (index, line) in diagnostic.message.lines().enumerate() { + let line = match &diagnostic.source { + Some(source) if index == 0 => { + let source_highlight = Vec::from_iter(0..source.len()); + highlight_diagnostic_message(source_highlight, &format!("{source}: {line}")) + } + + _ => highlight_diagnostic_message(Vec::new(), line), + }; + highlighted_lines.push(line); + } + let message = diagnostic.message; + Arc::new(move |cx: &mut BlockContext| { + let message = message.clone(); + let settings = settings::get::(cx); + let tooltip_style = settings.theme.tooltip.clone(); + let theme = &settings.theme.editor; + let style = diagnostic_style(diagnostic.severity, is_valid, theme); + let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round(); + let anchor_x = cx.anchor_x; + enum BlockContextToolip {} + MouseEventHandler::new::(cx.block_id, cx, |_, _| { + Flex::column() + .with_children(highlighted_lines.iter().map(|(line, highlights)| { + Label::new( + line.clone(), + style.message.clone().with_font_size(font_size), + ) + .with_highlights(highlights.clone()) + .contained() + .with_margin_left(anchor_x) + })) + .aligned() + .left() + .into_any() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, _, cx| { + cx.write_to_clipboard(ClipboardItem::new(message.clone())); + }) + // We really need to rethink this ID system... + .with_tooltip::( + cx.block_id, + "Copy diagnostic message", + None, + tooltip_style, + cx, + ) + .into_any() + }) +} + +pub fn highlight_diagnostic_message( + initial_highlights: Vec, + message: &str, +) -> (String, Vec) { + let mut message_without_backticks = String::new(); + let mut prev_offset = 0; + let mut inside_block = false; + let mut highlights = initial_highlights; + for (match_ix, (offset, _)) in message + .match_indices('`') + .chain([(message.len(), "")]) + .enumerate() + { + message_without_backticks.push_str(&message[prev_offset..offset]); + if inside_block { + highlights.extend(prev_offset - match_ix..offset - match_ix); + } + + inside_block = !inside_block; + prev_offset = offset + 1; + } + + (message_without_backticks, highlights) +} + +pub fn diagnostic_style( + severity: DiagnosticSeverity, + valid: bool, + theme: &theme::Editor, +) -> DiagnosticStyle { + match (severity, valid) { + (DiagnosticSeverity::ERROR, true) => theme.error_diagnostic.clone(), + (DiagnosticSeverity::ERROR, false) => theme.invalid_error_diagnostic.clone(), + (DiagnosticSeverity::WARNING, true) => theme.warning_diagnostic.clone(), + (DiagnosticSeverity::WARNING, false) => theme.invalid_warning_diagnostic.clone(), + (DiagnosticSeverity::INFORMATION, true) => theme.information_diagnostic.clone(), + (DiagnosticSeverity::INFORMATION, false) => theme.invalid_information_diagnostic.clone(), + (DiagnosticSeverity::HINT, true) => theme.hint_diagnostic.clone(), + (DiagnosticSeverity::HINT, false) => theme.invalid_hint_diagnostic.clone(), + _ => theme.invalid_hint_diagnostic.clone(), + } +} + +pub fn combine_syntax_and_fuzzy_match_highlights( + text: &str, + default_style: HighlightStyle, + syntax_ranges: impl Iterator, HighlightStyle)>, + match_indices: &[usize], +) -> Vec<(Range, HighlightStyle)> { + let mut result = Vec::new(); + let mut match_indices = match_indices.iter().copied().peekable(); + + for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())]) + { + syntax_highlight.weight = None; + + // Add highlights for any fuzzy match characters before the next + // syntax highlight range. + while let Some(&match_index) = match_indices.peek() { + if match_index >= range.start { + break; + } + match_indices.next(); + let end_index = char_ix_after(match_index, text); + let mut match_style = default_style; + match_style.weight = Some(fonts::Weight::BOLD); + result.push((match_index..end_index, match_style)); + } + + if range.start == usize::MAX { + break; + } + + // Add highlights for any fuzzy match characters within the + // syntax highlight range. + let mut offset = range.start; + while let Some(&match_index) = match_indices.peek() { + if match_index >= range.end { + break; + } + + match_indices.next(); + if match_index > offset { + result.push((offset..match_index, syntax_highlight)); + } + + let mut end_index = char_ix_after(match_index, text); + while let Some(&next_match_index) = match_indices.peek() { + if next_match_index == end_index && next_match_index < range.end { + end_index = char_ix_after(next_match_index, text); + match_indices.next(); + } else { + break; + } + } + + let mut match_style = syntax_highlight; + match_style.weight = Some(fonts::Weight::BOLD); + result.push((match_index..end_index, match_style)); + offset = end_index; + } + + if offset < range.end { + result.push((offset..range.end, syntax_highlight)); + } + } + + fn char_ix_after(ix: usize, text: &str) -> usize { + ix + text[ix..].chars().next().unwrap().len_utf8() + } + + result +} + +pub fn styled_runs_for_code_label<'a>( + label: &'a CodeLabel, + syntax_theme: &'a theme::SyntaxTheme, +) -> impl 'a + Iterator, HighlightStyle)> { + let fade_out = HighlightStyle { + fade_out: Some(0.35), + ..Default::default() + }; + + let mut prev_end = label.filter_range.end; + label + .runs + .iter() + .enumerate() + .flat_map(move |(ix, (range, highlight_id))| { + let style = if let Some(style) = highlight_id.style(syntax_theme) { + style + } else { + return Default::default(); + }; + let mut muted_style = style; + muted_style.highlight(fade_out); + + let mut runs = SmallVec::<[(Range, HighlightStyle); 3]>::new(); + if range.start >= label.filter_range.end { + if range.start > prev_end { + runs.push((prev_end..range.start, fade_out)); + } + runs.push((range.clone(), muted_style)); + } else if range.end <= label.filter_range.end { + runs.push((range.clone(), style)); + } else { + runs.push((range.start..label.filter_range.end, style)); + runs.push((label.filter_range.end..range.end, muted_style)); + } + prev_end = cmp::max(prev_end, range.end); + + if ix + 1 == label.runs.len() && label.text.len() > prev_end { + runs.push((prev_end..label.text.len(), fade_out)); + } + + runs + }) +} + +pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator + 'a { + let mut index = 0; + let mut codepoints = text.char_indices().peekable(); + + std::iter::from_fn(move || { + let start_index = index; + while let Some((new_index, codepoint)) = codepoints.next() { + index = new_index + codepoint.len_utf8(); + let current_upper = codepoint.is_uppercase(); + let next_upper = codepoints + .peek() + .map(|(_, c)| c.is_uppercase()) + .unwrap_or(false); + + if !current_upper && next_upper { + return Some(&text[start_index..index]); + } + } + + index = text.len(); + if start_index < text.len() { + return Some(&text[start_index..]); + } + None + }) + .flat_map(|word| word.split_inclusive('_')) + .flat_map(|word| word.split_inclusive('-')) +} + +trait RangeToAnchorExt { + fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range; +} + +impl RangeToAnchorExt for Range { + fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range { + snapshot.anchor_after(self.start)..snapshot.anchor_before(self.end) + } +} diff --git a/crates/editor2/src/editor_settings.rs b/crates/editor2/src/editor_settings.rs new file mode 100644 index 0000000000..75f8b800f9 --- /dev/null +++ b/crates/editor2/src/editor_settings.rs @@ -0,0 +1,62 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::Setting; + +#[derive(Deserialize)] +pub struct EditorSettings { + pub cursor_blink: bool, + pub hover_popover_enabled: bool, + pub show_completions_on_input: bool, + pub show_completion_documentation: bool, + pub use_on_type_format: bool, + pub scrollbar: Scrollbar, + pub relative_line_numbers: bool, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct Scrollbar { + pub show: ShowScrollbar, + pub git_diff: bool, + pub selections: bool, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ShowScrollbar { + Auto, + System, + Always, + Never, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +pub struct EditorSettingsContent { + pub cursor_blink: Option, + pub hover_popover_enabled: Option, + pub show_completions_on_input: Option, + pub show_completion_documentation: Option, + pub use_on_type_format: Option, + pub scrollbar: Option, + pub relative_line_numbers: Option, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct ScrollbarContent { + pub show: Option, + pub git_diff: Option, + pub selections: Option, +} + +impl Setting for EditorSettings { + const KEY: Option<&'static str> = None; + + type FileContent = EditorSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &gpui::AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs new file mode 100644 index 0000000000..feca741737 --- /dev/null +++ b/crates/editor2/src/editor_tests.rs @@ -0,0 +1,8195 @@ +use super::*; +use crate::{ + scroll::scroll_amount::ScrollAmount, + test::{ + assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext, + editor_test_context::EditorTestContext, select_ranges, + }, + JoinLines, +}; +use drag_and_drop::DragAndDrop; +use futures::StreamExt; +use gpui::{ + executor::Deterministic, + geometry::{rect::RectF, vector::vec2f}, + platform::{WindowBounds, WindowOptions}, + serde_json::{self, json}, + TestAppContext, +}; +use indoc::indoc; +use language::{ + language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent}, + BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry, + Override, Point, +}; +use parking_lot::Mutex; +use project::project_settings::{LspSettings, ProjectSettings}; +use project::FakeFs; +use std::sync::atomic; +use std::sync::atomic::AtomicUsize; +use std::{cell::RefCell, future::Future, rc::Rc, time::Instant}; +use unindent::Unindent; +use util::{ + assert_set_eq, + test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker}, +}; +use workspace::{ + item::{FollowableItem, Item, ItemHandle}, + NavigationEntry, ViewId, +}; + +#[gpui::test] +fn test_edit_events(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let buffer = cx.add_model(|cx| { + let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "123456"); + buffer.set_group_interval(Duration::from_secs(1)); + buffer + }); + + let events = Rc::new(RefCell::new(Vec::new())); + let editor1 = cx + .add_window({ + let events = events.clone(); + |cx| { + cx.subscribe(&cx.handle(), move |_, _, event, _| { + if matches!( + event, + Event::Edited | Event::BufferEdited | Event::DirtyChanged + ) { + events.borrow_mut().push(("editor1", event.clone())); + } + }) + .detach(); + Editor::for_buffer(buffer.clone(), None, cx) + } + }) + .root(cx); + let editor2 = cx + .add_window({ + let events = events.clone(); + |cx| { + cx.subscribe(&cx.handle(), move |_, _, event, _| { + if matches!( + event, + Event::Edited | Event::BufferEdited | Event::DirtyChanged + ) { + events.borrow_mut().push(("editor2", event.clone())); + } + }) + .detach(); + Editor::for_buffer(buffer.clone(), None, cx) + } + }) + .root(cx); + assert_eq!(mem::take(&mut *events.borrow_mut()), []); + + // Mutating editor 1 will emit an `Edited` event only for that editor. + editor1.update(cx, |editor, cx| editor.insert("X", cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor1", Event::Edited), + ("editor1", Event::BufferEdited), + ("editor2", Event::BufferEdited), + ("editor1", Event::DirtyChanged), + ("editor2", Event::DirtyChanged) + ] + ); + + // Mutating editor 2 will emit an `Edited` event only for that editor. + editor2.update(cx, |editor, cx| editor.delete(&Delete, cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor2", Event::Edited), + ("editor1", Event::BufferEdited), + ("editor2", Event::BufferEdited), + ] + ); + + // Undoing on editor 1 will emit an `Edited` event only for that editor. + editor1.update(cx, |editor, cx| editor.undo(&Undo, cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor1", Event::Edited), + ("editor1", Event::BufferEdited), + ("editor2", Event::BufferEdited), + ("editor1", Event::DirtyChanged), + ("editor2", Event::DirtyChanged), + ] + ); + + // Redoing on editor 1 will emit an `Edited` event only for that editor. + editor1.update(cx, |editor, cx| editor.redo(&Redo, cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor1", Event::Edited), + ("editor1", Event::BufferEdited), + ("editor2", Event::BufferEdited), + ("editor1", Event::DirtyChanged), + ("editor2", Event::DirtyChanged), + ] + ); + + // Undoing on editor 2 will emit an `Edited` event only for that editor. + editor2.update(cx, |editor, cx| editor.undo(&Undo, cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor2", Event::Edited), + ("editor1", Event::BufferEdited), + ("editor2", Event::BufferEdited), + ("editor1", Event::DirtyChanged), + ("editor2", Event::DirtyChanged), + ] + ); + + // Redoing on editor 2 will emit an `Edited` event only for that editor. + editor2.update(cx, |editor, cx| editor.redo(&Redo, cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor2", Event::Edited), + ("editor1", Event::BufferEdited), + ("editor2", Event::BufferEdited), + ("editor1", Event::DirtyChanged), + ("editor2", Event::DirtyChanged), + ] + ); + + // No event is emitted when the mutation is a no-op. + editor2.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([0..0])); + + editor.backspace(&Backspace, cx); + }); + assert_eq!(mem::take(&mut *events.borrow_mut()), []); +} + +#[gpui::test] +fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut now = Instant::now(); + let buffer = cx.add_model(|cx| language::Buffer::new(0, cx.model_id() as u64, "123456")); + let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval()); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx + .add_window(|cx| build_editor(buffer.clone(), cx)) + .root(cx); + + editor.update(cx, |editor, cx| { + editor.start_transaction_at(now, cx); + editor.change_selections(None, cx, |s| s.select_ranges([2..4])); + + editor.insert("cd", cx); + editor.end_transaction_at(now, cx); + assert_eq!(editor.text(cx), "12cd56"); + assert_eq!(editor.selections.ranges(cx), vec![4..4]); + + editor.start_transaction_at(now, cx); + editor.change_selections(None, cx, |s| s.select_ranges([4..5])); + editor.insert("e", cx); + editor.end_transaction_at(now, cx); + assert_eq!(editor.text(cx), "12cde6"); + assert_eq!(editor.selections.ranges(cx), vec![5..5]); + + now += group_interval + Duration::from_millis(1); + editor.change_selections(None, cx, |s| s.select_ranges([2..2])); + + // Simulate an edit in another editor + buffer.update(cx, |buffer, cx| { + buffer.start_transaction_at(now, cx); + buffer.edit([(0..1, "a")], None, cx); + buffer.edit([(1..1, "b")], None, cx); + buffer.end_transaction_at(now, cx); + }); + + assert_eq!(editor.text(cx), "ab2cde6"); + assert_eq!(editor.selections.ranges(cx), vec![3..3]); + + // Last transaction happened past the group interval in a different editor. + // Undo it individually and don't restore selections. + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "12cde6"); + assert_eq!(editor.selections.ranges(cx), vec![2..2]); + + // First two transactions happened within the group interval in this editor. + // Undo them together and restore selections. + editor.undo(&Undo, cx); + editor.undo(&Undo, cx); // Undo stack is empty here, so this is a no-op. + assert_eq!(editor.text(cx), "123456"); + assert_eq!(editor.selections.ranges(cx), vec![0..0]); + + // Redo the first two transactions together. + editor.redo(&Redo, cx); + assert_eq!(editor.text(cx), "12cde6"); + assert_eq!(editor.selections.ranges(cx), vec![5..5]); + + // Redo the last transaction on its own. + editor.redo(&Redo, cx); + assert_eq!(editor.text(cx), "ab2cde6"); + assert_eq!(editor.selections.ranges(cx), vec![6..6]); + + // Test empty transactions. + editor.start_transaction_at(now, cx); + editor.end_transaction_at(now, cx); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "12cde6"); + }); +} + +#[gpui::test] +fn test_ime_composition(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let buffer = cx.add_model(|cx| { + let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "abcde"); + // Ensure automatic grouping doesn't occur. + buffer.set_group_interval(Duration::ZERO); + buffer + }); + + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + cx.add_window(|cx| { + let mut editor = build_editor(buffer.clone(), cx); + + // Start a new IME composition. + editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx); + editor.replace_and_mark_text_in_range(Some(0..1), "á", None, cx); + editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, cx); + assert_eq!(editor.text(cx), "äbcde"); + assert_eq!( + editor.marked_text_ranges(cx), + Some(vec![OffsetUtf16(0)..OffsetUtf16(1)]) + ); + + // Finalize IME composition. + editor.replace_text_in_range(None, "ā", cx); + assert_eq!(editor.text(cx), "ābcde"); + assert_eq!(editor.marked_text_ranges(cx), None); + + // IME composition edits are grouped and are undone/redone at once. + editor.undo(&Default::default(), cx); + assert_eq!(editor.text(cx), "abcde"); + assert_eq!(editor.marked_text_ranges(cx), None); + editor.redo(&Default::default(), cx); + assert_eq!(editor.text(cx), "ābcde"); + assert_eq!(editor.marked_text_ranges(cx), None); + + // Start a new IME composition. + editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx); + assert_eq!( + editor.marked_text_ranges(cx), + Some(vec![OffsetUtf16(0)..OffsetUtf16(1)]) + ); + + // Undoing during an IME composition cancels it. + editor.undo(&Default::default(), cx); + assert_eq!(editor.text(cx), "ābcde"); + assert_eq!(editor.marked_text_ranges(cx), None); + + // Start a new IME composition with an invalid marked range, ensuring it gets clipped. + editor.replace_and_mark_text_in_range(Some(4..999), "è", None, cx); + assert_eq!(editor.text(cx), "ābcdè"); + assert_eq!( + editor.marked_text_ranges(cx), + Some(vec![OffsetUtf16(4)..OffsetUtf16(5)]) + ); + + // Finalize IME composition with an invalid replacement range, ensuring it gets clipped. + editor.replace_text_in_range(Some(4..999), "ę", cx); + assert_eq!(editor.text(cx), "ābcdę"); + assert_eq!(editor.marked_text_ranges(cx), None); + + // Start a new IME composition with multiple cursors. + editor.change_selections(None, cx, |s| { + s.select_ranges([ + OffsetUtf16(1)..OffsetUtf16(1), + OffsetUtf16(3)..OffsetUtf16(3), + OffsetUtf16(5)..OffsetUtf16(5), + ]) + }); + editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, cx); + assert_eq!(editor.text(cx), "XYZbXYZdXYZ"); + assert_eq!( + editor.marked_text_ranges(cx), + Some(vec![ + OffsetUtf16(0)..OffsetUtf16(3), + OffsetUtf16(4)..OffsetUtf16(7), + OffsetUtf16(8)..OffsetUtf16(11) + ]) + ); + + // Ensure the newly-marked range gets treated as relative to the previously-marked ranges. + editor.replace_and_mark_text_in_range(Some(1..2), "1", None, cx); + assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z"); + assert_eq!( + editor.marked_text_ranges(cx), + Some(vec![ + OffsetUtf16(1)..OffsetUtf16(2), + OffsetUtf16(5)..OffsetUtf16(6), + OffsetUtf16(9)..OffsetUtf16(10) + ]) + ); + + // Finalize IME composition with multiple cursors. + editor.replace_text_in_range(Some(9..10), "2", cx); + assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z"); + assert_eq!(editor.marked_text_ranges(cx), None); + + editor + }); +} + +#[gpui::test] +fn test_selection_with_mouse(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); + build_editor(buffer, cx) + }) + .root(cx); + editor.update(cx, |view, cx| { + view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); + }); + assert_eq!( + editor.update(cx, |view, cx| view.selections.display_ranges(cx)), + [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] + ); + + editor.update(cx, |view, cx| { + view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx); + }); + + assert_eq!( + editor.update(cx, |view, cx| view.selections.display_ranges(cx)), + [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] + ); + + editor.update(cx, |view, cx| { + view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx); + }); + + assert_eq!( + editor.update(cx, |view, cx| view.selections.display_ranges(cx)), + [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] + ); + + editor.update(cx, |view, cx| { + view.end_selection(cx); + view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx); + }); + + assert_eq!( + editor.update(cx, |view, cx| view.selections.display_ranges(cx)), + [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] + ); + + editor.update(cx, |view, cx| { + view.begin_selection(DisplayPoint::new(3, 3), true, 1, cx); + view.update_selection(DisplayPoint::new(0, 0), 0, Vector2F::zero(), cx); + }); + + assert_eq!( + editor.update(cx, |view, cx| view.selections.display_ranges(cx)), + [ + DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0) + ] + ); + + editor.update(cx, |view, cx| { + view.end_selection(cx); + }); + + assert_eq!( + editor.update(cx, |view, cx| view.selections.display_ranges(cx)), + [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)] + ); +} + +#[gpui::test] +fn test_canceling_pending_selection(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); + build_editor(buffer, cx) + }) + .root(cx); + + view.update(cx, |view, cx| { + view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); + assert_eq!( + view.selections.display_ranges(cx), + [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] + ); + }); + + view.update(cx, |view, cx| { + view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx); + assert_eq!( + view.selections.display_ranges(cx), + [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] + ); + }); + + view.update(cx, |view, cx| { + view.cancel(&Cancel, cx); + view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx); + assert_eq!( + view.selections.display_ranges(cx), + [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] + ); + }); +} + +#[gpui::test] +fn test_clone(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let (text, selection_ranges) = marked_text_ranges( + indoc! {" + one + two + threeˇ + four + fiveˇ + "}, + true, + ); + + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple(&text, cx); + build_editor(buffer, cx) + }) + .root(cx); + + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone())); + editor.fold_ranges( + [ + Point::new(1, 0)..Point::new(2, 0), + Point::new(3, 0)..Point::new(4, 0), + ], + true, + cx, + ); + }); + + let cloned_editor = editor + .update(cx, |editor, cx| { + cx.add_window(Default::default(), |cx| editor.clone(cx)) + }) + .root(cx); + + let snapshot = editor.update(cx, |e, cx| e.snapshot(cx)); + let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx)); + + assert_eq!( + cloned_editor.update(cx, |e, cx| e.display_text(cx)), + editor.update(cx, |e, cx| e.display_text(cx)) + ); + assert_eq!( + cloned_snapshot + .folds_in_range(0..text.len()) + .collect::>(), + snapshot.folds_in_range(0..text.len()).collect::>(), + ); + assert_set_eq!( + cloned_editor.read_with(cx, |editor, cx| editor.selections.ranges::(cx)), + editor.read_with(cx, |editor, cx| editor.selections.ranges(cx)) + ); + assert_set_eq!( + cloned_editor.update(cx, |e, cx| e.selections.display_ranges(cx)), + editor.update(cx, |e, cx| e.selections.display_ranges(cx)) + ); +} + +#[gpui::test] +async fn test_navigation_history(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + cx.set_global(DragAndDrop::::default()); + use workspace::item::Item; + + let fs = FakeFs::new(cx.background()); + let project = Project::test(fs, [], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + window.add_view(cx, |cx| { + let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); + let mut editor = build_editor(buffer.clone(), cx); + let handle = cx.handle(); + editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle))); + + fn pop_history(editor: &mut Editor, cx: &mut WindowContext) -> Option { + editor.nav_history.as_mut().unwrap().pop_backward(cx) + } + + // Move the cursor a small distance. + // Nothing is added to the navigation history. + editor.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]) + }); + editor.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]) + }); + assert!(pop_history(&mut editor, cx).is_none()); + + // Move the cursor a large distance. + // The history can jump back to the previous position. + editor.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)]) + }); + let nav_entry = pop_history(&mut editor, cx).unwrap(); + editor.navigate(nav_entry.data.unwrap(), cx); + assert_eq!(nav_entry.item.id(), cx.view_id()); + assert_eq!( + editor.selections.display_ranges(cx), + &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)] + ); + assert!(pop_history(&mut editor, cx).is_none()); + + // Move the cursor a small distance via the mouse. + // Nothing is added to the navigation history. + editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx); + editor.end_selection(cx); + assert_eq!( + editor.selections.display_ranges(cx), + &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] + ); + assert!(pop_history(&mut editor, cx).is_none()); + + // Move the cursor a large distance via the mouse. + // The history can jump back to the previous position. + editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx); + editor.end_selection(cx); + assert_eq!( + editor.selections.display_ranges(cx), + &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)] + ); + let nav_entry = pop_history(&mut editor, cx).unwrap(); + editor.navigate(nav_entry.data.unwrap(), cx); + assert_eq!(nav_entry.item.id(), cx.view_id()); + assert_eq!( + editor.selections.display_ranges(cx), + &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] + ); + assert!(pop_history(&mut editor, cx).is_none()); + + // Set scroll position to check later + editor.set_scroll_position(Vector2F::new(5.5, 5.5), cx); + let original_scroll_position = editor.scroll_manager.anchor(); + + // Jump to the end of the document and adjust scroll + editor.move_to_end(&MoveToEnd, cx); + editor.set_scroll_position(Vector2F::new(-2.5, -0.5), cx); + assert_ne!(editor.scroll_manager.anchor(), original_scroll_position); + + let nav_entry = pop_history(&mut editor, cx).unwrap(); + editor.navigate(nav_entry.data.unwrap(), cx); + assert_eq!(editor.scroll_manager.anchor(), original_scroll_position); + + // Ensure we don't panic when navigation data contains invalid anchors *and* points. + let mut invalid_anchor = editor.scroll_manager.anchor().anchor; + invalid_anchor.text_anchor.buffer_id = Some(999); + let invalid_point = Point::new(9999, 0); + editor.navigate( + Box::new(NavigationData { + cursor_anchor: invalid_anchor, + cursor_position: invalid_point, + scroll_anchor: ScrollAnchor { + anchor: invalid_anchor, + offset: Default::default(), + }, + scroll_top_row: invalid_point.row, + }), + cx, + ); + assert_eq!( + editor.selections.display_ranges(cx), + &[editor.max_point(cx)..editor.max_point(cx)] + ); + assert_eq!( + editor.scroll_position(cx), + vec2f(0., editor.max_point(cx).row() as f32) + ); + + editor + }); +} + +#[gpui::test] +fn test_cancel(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); + build_editor(buffer, cx) + }) + .root(cx); + + view.update(cx, |view, cx| { + view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx); + view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx); + view.end_selection(cx); + + view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx); + view.update_selection(DisplayPoint::new(0, 3), 0, Vector2F::zero(), cx); + view.end_selection(cx); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), + DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1), + ] + ); + }); + + view.update(cx, |view, cx| { + view.cancel(&Cancel, cx); + assert_eq!( + view.selections.display_ranges(cx), + [DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1)] + ); + }); + + view.update(cx, |view, cx| { + view.cancel(&Cancel, cx); + assert_eq!( + view.selections.display_ranges(cx), + [DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)] + ); + }); +} + +#[gpui::test] +fn test_fold_action(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple( + &" + impl Foo { + // Hello! + + fn a() { + 1 + } + + fn b() { + 2 + } + + fn c() { + 3 + } + } + " + .unindent(), + cx, + ); + build_editor(buffer.clone(), cx) + }) + .root(cx); + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)]); + }); + view.fold(&Fold, cx); + assert_eq!( + view.display_text(cx), + " + impl Foo { + // Hello! + + fn a() { + 1 + } + + fn b() {⋯ + } + + fn c() {⋯ + } + } + " + .unindent(), + ); + + view.fold(&Fold, cx); + assert_eq!( + view.display_text(cx), + " + impl Foo {⋯ + } + " + .unindent(), + ); + + view.unfold_lines(&UnfoldLines, cx); + assert_eq!( + view.display_text(cx), + " + impl Foo { + // Hello! + + fn a() { + 1 + } + + fn b() {⋯ + } + + fn c() {⋯ + } + } + " + .unindent(), + ); + + view.unfold_lines(&UnfoldLines, cx); + assert_eq!(view.display_text(cx), view.buffer.read(cx).read(cx).text()); + }); +} + +#[gpui::test] +fn test_move_cursor(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx)); + let view = cx + .add_window(|cx| build_editor(buffer.clone(), cx)) + .root(cx); + + buffer.update(cx, |buffer, cx| { + buffer.edit( + vec![ + (Point::new(1, 0)..Point::new(1, 0), "\t"), + (Point::new(1, 1)..Point::new(1, 1), "\t"), + ], + None, + cx, + ); + }); + view.update(cx, |view, cx| { + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] + ); + + view.move_right(&MoveRight, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)] + ); + + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] + ); + + view.move_up(&MoveUp, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] + ); + + view.move_to_end(&MoveToEnd, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)] + ); + + view.move_to_beginning(&MoveToBeginning, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] + ); + + view.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)]); + }); + view.select_to_beginning(&SelectToBeginning, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)] + ); + + view.select_to_end(&SelectToEnd, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)] + ); + }); +} + +#[gpui::test] +fn test_move_cursor_multibyte(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε", cx); + build_editor(buffer.clone(), cx) + }) + .root(cx); + + assert_eq!('ⓐ'.len_utf8(), 3); + assert_eq!('α'.len_utf8(), 2); + + view.update(cx, |view, cx| { + view.fold_ranges( + vec![ + Point::new(0, 6)..Point::new(0, 12), + Point::new(1, 2)..Point::new(1, 4), + Point::new(2, 4)..Point::new(2, 8), + ], + true, + cx, + ); + assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nab⋯e\nαβ⋯ε"); + + view.move_right(&MoveRight, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(0, "ⓐ".len())] + ); + view.move_right(&MoveRight, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(0, "ⓐⓑ".len())] + ); + view.move_right(&MoveRight, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(0, "ⓐⓑ⋯".len())] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(1, "ab⋯e".len())] + ); + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(1, "ab⋯".len())] + ); + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(1, "ab".len())] + ); + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(1, "a".len())] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(2, "α".len())] + ); + view.move_right(&MoveRight, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(2, "αβ".len())] + ); + view.move_right(&MoveRight, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(2, "αβ⋯".len())] + ); + view.move_right(&MoveRight, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(2, "αβ⋯ε".len())] + ); + + view.move_up(&MoveUp, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(1, "ab⋯e".len())] + ); + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(2, "αβ⋯ε".len())] + ); + view.move_up(&MoveUp, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(1, "ab⋯e".len())] + ); + + view.move_up(&MoveUp, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(0, "ⓐⓑ".len())] + ); + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(0, "ⓐ".len())] + ); + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(0, "".len())] + ); + }); +} + +#[gpui::test] +fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); + build_editor(buffer.clone(), cx) + }) + .root(cx); + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]); + }); + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(1, "abcd".len())] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(2, "αβγ".len())] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(3, "abcd".len())] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())] + ); + + view.move_up(&MoveUp, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(3, "abcd".len())] + ); + + view.move_up(&MoveUp, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(2, "αβγ".len())] + ); + }); +} + +#[gpui::test] +fn test_beginning_end_of_line(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\n def", cx); + build_editor(buffer, cx) + }) + .root(cx); + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), + ]); + }); + }); + + view.update(cx, |view, cx| { + view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_end_of_line(&MoveToEndOfLine, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), + ] + ); + }); + + // Moving to the end of line again is a no-op. + view.update(cx, |view, cx| { + view.move_to_end_of_line(&MoveToEndOfLine, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_left(&MoveLeft, cx); + view.select_to_beginning_of_line( + &SelectToBeginningOfLine { + stop_at_soft_wraps: true, + }, + cx, + ); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_to_beginning_of_line( + &SelectToBeginningOfLine { + stop_at_soft_wraps: true, + }, + cx, + ); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 0), + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_to_beginning_of_line( + &SelectToBeginningOfLine { + stop_at_soft_wraps: true, + }, + cx, + ); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_to_end_of_line( + &SelectToEndOfLine { + stop_at_soft_wraps: true, + }, + cx, + ); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 5), + ] + ); + }); + + view.update(cx, |view, cx| { + view.delete_to_end_of_line(&DeleteToEndOfLine, cx); + assert_eq!(view.display_text(cx), "ab\n de"); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), + ] + ); + }); + + view.update(cx, |view, cx| { + view.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx); + assert_eq!(view.display_text(cx), "\n"); + assert_eq!( + view.selections.display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ] + ); + }); +} + +#[gpui::test] +fn test_prev_next_word_boundary(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx); + build_editor(buffer, cx) + }) + .root(cx); + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11), + DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4), + ]) + }); + + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", view, cx); + + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", view, cx); + + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", view, cx); + + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", view, cx); + + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", view, cx); + + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", view, cx); + + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", view, cx); + + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", view, cx); + + view.move_right(&MoveRight, cx); + view.select_to_previous_word_start(&SelectToPreviousWordStart, cx); + assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", view, cx); + + view.select_to_previous_word_start(&SelectToPreviousWordStart, cx); + assert_selection_ranges("use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}", view, cx); + + view.select_to_next_word_end(&SelectToNextWordEnd, cx); + assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", view, cx); + }); +} + +#[gpui::test] +fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = + MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); + build_editor(buffer, cx) + }) + .root(cx); + + view.update(cx, |view, cx| { + view.set_wrap_width(Some(140.), cx); + assert_eq!( + view.display_text(cx), + "use one::{\n two::three::\n four::five\n};" + ); + + view.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)]); + }); + + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)] + ); + + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] + ); + + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] + ); + + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8)] + ); + + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] + ); + + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] + ); + }); +} + +#[gpui::test] +async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + + let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); + let window = cx.window; + window.simulate_resize(vec2f(100., 4. * line_height), &mut cx); + + cx.set_state( + &r#"ˇone + two + + three + fourˇ + five + + six"# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); + cx.assert_editor_state( + &r#"one + two + ˇ + three + four + five + ˇ + six"# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); + cx.assert_editor_state( + &r#"one + two + + three + four + five + ˇ + sixˇ"# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); + cx.assert_editor_state( + &r#"one + two + + three + four + five + + sixˇ"# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); + cx.assert_editor_state( + &r#"one + two + + three + four + five + ˇ + six"# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); + cx.assert_editor_state( + &r#"one + two + ˇ + three + four + five + + six"# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); + cx.assert_editor_state( + &r#"ˇone + two + + three + four + five + + six"# + .unindent(), + ); +} + +#[gpui::test] +async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); + let window = cx.window; + window.simulate_resize(vec2f(1000., 4. * line_height + 0.5), &mut cx); + + cx.set_state( + &r#"ˇone + two + three + four + five + six + seven + eight + nine + ten + "#, + ); + + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.)); + editor.scroll_screen(&ScrollAmount::Page(1.), cx); + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); + editor.scroll_screen(&ScrollAmount::Page(1.), cx); + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 6.)); + editor.scroll_screen(&ScrollAmount::Page(-1.), cx); + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); + + editor.scroll_screen(&ScrollAmount::Page(-0.5), cx); + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.)); + editor.scroll_screen(&ScrollAmount::Page(0.5), cx); + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); + }); +} + +#[gpui::test] +async fn test_autoscroll(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + + let line_height = cx.update_editor(|editor, cx| { + editor.set_vertical_scroll_margin(2, cx); + editor.style(cx).text.line_height(cx.font_cache()) + }); + + let window = cx.window; + window.simulate_resize(vec2f(1000., 6.0 * line_height), &mut cx); + + cx.set_state( + &r#"ˇone + two + three + four + five + six + seven + eight + nine + ten + "#, + ); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.0)); + }); + + // Add a cursor below the visible area. Since both cursors cannot fit + // on screen, the editor autoscrolls to reveal the newest cursor, and + // allows the vertical scroll margin below that cursor. + cx.update_editor(|editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { + selections.select_ranges([ + Point::new(0, 0)..Point::new(0, 0), + Point::new(6, 0)..Point::new(6, 0), + ]); + }) + }); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.0)); + }); + + // Move down. The editor cursor scrolls down to track the newest cursor. + cx.update_editor(|editor, cx| { + editor.move_down(&Default::default(), cx); + }); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 4.0)); + }); + + // Add a cursor above the visible area. Since both cursors fit on screen, + // the editor scrolls to show both. + cx.update_editor(|editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { + selections.select_ranges([ + Point::new(1, 0)..Point::new(1, 0), + Point::new(6, 0)..Point::new(6, 0), + ]); + }) + }); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.0)); + }); +} + +#[gpui::test] +async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + + let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); + let window = cx.window; + window.simulate_resize(vec2f(100., 4. * line_height), &mut cx); + + cx.set_state( + &r#" + ˇone + two + threeˇ + four + five + six + seven + eight + nine + ten + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx)); + cx.assert_editor_state( + &r#" + one + two + three + ˇfour + five + sixˇ + seven + eight + nine + ten + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx)); + cx.assert_editor_state( + &r#" + one + two + three + four + five + six + ˇseven + eight + nineˇ + ten + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx)); + cx.assert_editor_state( + &r#" + one + two + three + ˇfour + five + sixˇ + seven + eight + nine + ten + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx)); + cx.assert_editor_state( + &r#" + ˇone + two + threeˇ + four + five + six + seven + eight + nine + ten + "# + .unindent(), + ); + + // Test select collapsing + cx.update_editor(|editor, cx| { + editor.move_page_down(&MovePageDown::default(), cx); + editor.move_page_down(&MovePageDown::default(), cx); + editor.move_page_down(&MovePageDown::default(), cx); + }); + cx.assert_editor_state( + &r#" + one + two + three + four + five + six + seven + eight + nine + ˇten + ˇ"# + .unindent(), + ); +} + +#[gpui::test] +async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("one «two threeˇ» four"); + cx.update_editor(|editor, cx| { + editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx); + assert_eq!(editor.text(cx), " four"); + }); +} + +#[gpui::test] +fn test_delete_to_word_boundary(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("one two three four", cx); + build_editor(buffer.clone(), cx) + }) + .root(cx); + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + // an empty selection - the preceding word fragment is deleted + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + // characters selected - they are deleted + DisplayPoint::new(0, 9)..DisplayPoint::new(0, 12), + ]) + }); + view.delete_to_previous_word_start(&DeleteToPreviousWordStart, cx); + assert_eq!(view.buffer.read(cx).read(cx).text(), "e two te four"); + }); + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + // an empty selection - the following word fragment is deleted + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + // characters selected - they are deleted + DisplayPoint::new(0, 9)..DisplayPoint::new(0, 10), + ]) + }); + view.delete_to_next_word_end(&DeleteToNextWordEnd, cx); + assert_eq!(view.buffer.read(cx).read(cx).text(), "e t te our"); + }); +} + +#[gpui::test] +fn test_newline(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); + build_editor(buffer.clone(), cx) + }) + .root(cx); + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), + DisplayPoint::new(1, 6)..DisplayPoint::new(1, 6), + ]) + }); + + view.newline(&Newline, cx); + assert_eq!(view.text(cx), "aa\naa\n \n bb\n bb\n"); + }); +} + +#[gpui::test] +fn test_newline_with_old_selections(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple( + " + a + b( + X + ) + c( + X + ) + " + .unindent() + .as_str(), + cx, + ); + let mut editor = build_editor(buffer.clone(), cx); + editor.change_selections(None, cx, |s| { + s.select_ranges([ + Point::new(2, 4)..Point::new(2, 5), + Point::new(5, 4)..Point::new(5, 5), + ]) + }); + editor + }) + .root(cx); + + editor.update(cx, |editor, cx| { + // Edit the buffer directly, deleting ranges surrounding the editor's selections + editor.buffer.update(cx, |buffer, cx| { + buffer.edit( + [ + (Point::new(1, 2)..Point::new(3, 0), ""), + (Point::new(4, 2)..Point::new(6, 0), ""), + ], + None, + cx, + ); + assert_eq!( + buffer.read(cx).text(), + " + a + b() + c() + " + .unindent() + ); + }); + assert_eq!( + editor.selections.ranges(cx), + &[ + Point::new(1, 2)..Point::new(1, 2), + Point::new(2, 2)..Point::new(2, 2), + ], + ); + + editor.newline(&Newline, cx); + assert_eq!( + editor.text(cx), + " + a + b( + ) + c( + ) + " + .unindent() + ); + + // The selections are moved after the inserted newlines + assert_eq!( + editor.selections.ranges(cx), + &[ + Point::new(2, 0)..Point::new(2, 0), + Point::new(4, 0)..Point::new(4, 0), + ], + ); + }); +} + +#[gpui::test] +async fn test_newline_above(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4) + }); + + let language = Arc::new( + Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::language()), + ) + .with_indents_query(r#"(_ "(" ")" @end) @indent"#) + .unwrap(), + ); + + let mut cx = EditorTestContext::new(cx).await; + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + cx.set_state(indoc! {" + const a: ˇA = ( + (ˇ + «const_functionˇ»(ˇ), + so«mˇ»et«hˇ»ing_ˇelse,ˇ + )ˇ + ˇ);ˇ + "}); + + cx.update_editor(|e, cx| e.newline_above(&NewlineAbove, cx)); + cx.assert_editor_state(indoc! {" + ˇ + const a: A = ( + ˇ + ( + ˇ + ˇ + const_function(), + ˇ + ˇ + ˇ + ˇ + something_else, + ˇ + ) + ˇ + ˇ + ); + "}); +} + +#[gpui::test] +async fn test_newline_below(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4) + }); + + let language = Arc::new( + Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::language()), + ) + .with_indents_query(r#"(_ "(" ")" @end) @indent"#) + .unwrap(), + ); + + let mut cx = EditorTestContext::new(cx).await; + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + cx.set_state(indoc! {" + const a: ˇA = ( + (ˇ + «const_functionˇ»(ˇ), + so«mˇ»et«hˇ»ing_ˇelse,ˇ + )ˇ + ˇ);ˇ + "}); + + cx.update_editor(|e, cx| e.newline_below(&NewlineBelow, cx)); + cx.assert_editor_state(indoc! {" + const a: A = ( + ˇ + ( + ˇ + const_function(), + ˇ + ˇ + something_else, + ˇ + ˇ + ˇ + ˇ + ) + ˇ + ); + ˇ + ˇ + "}); +} + +#[gpui::test] +async fn test_newline_comments(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4) + }); + + let language = Arc::new(Language::new( + LanguageConfig { + line_comment: Some("//".into()), + ..LanguageConfig::default() + }, + None, + )); + { + let mut cx = EditorTestContext::new(cx).await; + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + cx.set_state(indoc! {" + // Fooˇ + "}); + + cx.update_editor(|e, cx| e.newline(&Newline, cx)); + cx.assert_editor_state(indoc! {" + // Foo + //ˇ + "}); + // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix. + cx.set_state(indoc! {" + ˇ// Foo + "}); + cx.update_editor(|e, cx| e.newline(&Newline, cx)); + cx.assert_editor_state(indoc! {" + + ˇ// Foo + "}); + } + // Ensure that comment continuations can be disabled. + update_test_language_settings(cx, |settings| { + settings.defaults.extend_comment_on_newline = Some(false); + }); + let mut cx = EditorTestContext::new(cx).await; + cx.set_state(indoc! {" + // Fooˇ + "}); + cx.update_editor(|e, cx| e.newline(&Newline, cx)); + cx.assert_editor_state(indoc! {" + // Foo + ˇ + "}); +} + +#[gpui::test] +fn test_insert_with_old_selections(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); + let mut editor = build_editor(buffer.clone(), cx); + editor.change_selections(None, cx, |s| s.select_ranges([3..4, 11..12, 19..20])); + editor + }) + .root(cx); + + editor.update(cx, |editor, cx| { + // Edit the buffer directly, deleting ranges surrounding the editor's selections + editor.buffer.update(cx, |buffer, cx| { + buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx); + assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent()); + }); + assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],); + + editor.insert("Z", cx); + assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)"); + + // The selections are moved after the inserted characters + assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],); + }); +} + +#[gpui::test] +async fn test_tab(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(3) + }); + + let mut cx = EditorTestContext::new(cx).await; + cx.set_state(indoc! {" + ˇabˇc + ˇ🏀ˇ🏀ˇefg + dˇ + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + ˇab ˇc + ˇ🏀 ˇ🏀 ˇefg + d ˇ + "}); + + cx.set_state(indoc! {" + a + «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ» + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + a + «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ» + "}); +} + +#[gpui::test] +async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + let language = Arc::new( + Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::language()), + ) + .with_indents_query(r#"(_ "(" ")" @end) @indent"#) + .unwrap(), + ); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + + // cursors that are already at the suggested indent level insert + // a soft tab. cursors that are to the left of the suggested indent + // auto-indent their line. + cx.set_state(indoc! {" + ˇ + const a: B = ( + c( + d( + ˇ + ) + ˇ + ˇ ) + ); + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + ˇ + const a: B = ( + c( + d( + ˇ + ) + ˇ + ˇ) + ); + "}); + + // handle auto-indent when there are multiple cursors on the same line + cx.set_state(indoc! {" + const a: B = ( + c( + ˇ ˇ + ˇ ) + ); + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + const a: B = ( + c( + ˇ + ˇ) + ); + "}); +} + +#[gpui::test] +async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4) + }); + + let language = Arc::new( + Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::language()), + ) + .with_indents_query(r#"(_ "{" "}" @end) @indent"#) + .unwrap(), + ); + + let mut cx = EditorTestContext::new(cx).await; + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + cx.set_state(indoc! {" + fn a() { + if b { + \t ˇc + } + } + "}); + + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + fn a() { + if b { + ˇc + } + } + "}); +} + +#[gpui::test] +async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4); + }); + + let mut cx = EditorTestContext::new(cx).await; + + cx.set_state(indoc! {" + «oneˇ» «twoˇ» + three + four + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + «oneˇ» «twoˇ» + three + four + "}); + + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + «oneˇ» «twoˇ» + three + four + "}); + + // select across line ending + cx.set_state(indoc! {" + one two + t«hree + ˇ» four + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + one two + t«hree + ˇ» four + "}); + + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + t«hree + ˇ» four + "}); + + // Ensure that indenting/outdenting works when the cursor is at column 0. + cx.set_state(indoc! {" + one two + ˇthree + four + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + one two + ˇthree + four + "}); + + cx.set_state(indoc! {" + one two + ˇ three + four + "}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + ˇthree + four + "}); +} + +#[gpui::test] +async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.hard_tabs = Some(true); + }); + + let mut cx = EditorTestContext::new(cx).await; + + // select two ranges on one line + cx.set_state(indoc! {" + «oneˇ» «twoˇ» + three + four + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + \t«oneˇ» «twoˇ» + three + four + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + \t\t«oneˇ» «twoˇ» + three + four + "}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + \t«oneˇ» «twoˇ» + three + four + "}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + «oneˇ» «twoˇ» + three + four + "}); + + // select across a line ending + cx.set_state(indoc! {" + one two + t«hree + ˇ»four + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + one two + \tt«hree + ˇ»four + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + one two + \t\tt«hree + ˇ»four + "}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + \tt«hree + ˇ»four + "}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + t«hree + ˇ»four + "}); + + // Ensure that indenting/outdenting works when the cursor is at column 0. + cx.set_state(indoc! {" + one two + ˇthree + four + "}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + ˇthree + four + "}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + one two + \tˇthree + four + "}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + ˇthree + four + "}); +} + +#[gpui::test] +fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { + init_test(cx, |settings| { + settings.languages.extend([ + ( + "TOML".into(), + LanguageSettingsContent { + tab_size: NonZeroU32::new(2), + ..Default::default() + }, + ), + ( + "Rust".into(), + LanguageSettingsContent { + tab_size: NonZeroU32::new(4), + ..Default::default() + }, + ), + ]); + }); + + let toml_language = Arc::new(Language::new( + LanguageConfig { + name: "TOML".into(), + ..Default::default() + }, + None, + )); + let rust_language = Arc::new(Language::new( + LanguageConfig { + name: "Rust".into(), + ..Default::default() + }, + None, + )); + + let toml_buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n").with_language(toml_language, cx) + }); + let rust_buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, "const c: usize = 3;\n") + .with_language(rust_language, cx) + }); + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + toml_buffer.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(2, 0), + primary: None, + }], + cx, + ); + multibuffer.push_excerpts( + rust_buffer.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 0), + primary: None, + }], + cx, + ); + multibuffer + }); + + cx.add_window(|cx| { + let mut editor = build_editor(multibuffer, cx); + + assert_eq!( + editor.text(cx), + indoc! {" + a = 1 + b = 2 + + const c: usize = 3; + "} + ); + + select_ranges( + &mut editor, + indoc! {" + «aˇ» = 1 + b = 2 + + «const c:ˇ» usize = 3; + "}, + cx, + ); + + editor.tab(&Tab, cx); + assert_text_with_selections( + &mut editor, + indoc! {" + «aˇ» = 1 + b = 2 + + «const c:ˇ» usize = 3; + "}, + cx, + ); + editor.tab_prev(&TabPrev, cx); + assert_text_with_selections( + &mut editor, + indoc! {" + «aˇ» = 1 + b = 2 + + «const c:ˇ» usize = 3; + "}, + cx, + ); + + editor + }); +} + +#[gpui::test] +async fn test_backspace(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + // Basic backspace + cx.set_state(indoc! {" + onˇe two three + fou«rˇ» five six + seven «ˇeight nine + »ten + "}); + cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); + cx.assert_editor_state(indoc! {" + oˇe two three + fouˇ five six + seven ˇten + "}); + + // Test backspace inside and around indents + cx.set_state(indoc! {" + zero + ˇone + ˇtwo + ˇ ˇ ˇ three + ˇ ˇ four + "}); + cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); + cx.assert_editor_state(indoc! {" + zero + ˇone + ˇtwo + ˇ threeˇ four + "}); + + // Test backspace with line_mode set to true + cx.update_editor(|e, _| e.selections.line_mode = true); + cx.set_state(indoc! {" + The ˇquick ˇbrown + fox jumps over + the lazy dog + ˇThe qu«ick bˇ»rown"}); + cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); + cx.assert_editor_state(indoc! {" + ˇfox jumps over + the lazy dogˇ"}); +} + +#[gpui::test] +async fn test_delete(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + cx.set_state(indoc! {" + onˇe two three + fou«rˇ» five six + seven «ˇeight nine + »ten + "}); + cx.update_editor(|e, cx| e.delete(&Delete, cx)); + cx.assert_editor_state(indoc! {" + onˇ two three + fouˇ five six + seven ˇten + "}); + + // Test backspace with line_mode set to true + cx.update_editor(|e, _| e.selections.line_mode = true); + cx.set_state(indoc! {" + The ˇquick ˇbrown + fox «ˇjum»ps over + the lazy dog + ˇThe qu«ick bˇ»rown"}); + cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); + cx.assert_editor_state("ˇthe lazy dogˇ"); +} + +#[gpui::test] +fn test_delete_line(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + build_editor(buffer, cx) + }) + .root(cx); + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + ]) + }); + view.delete_line(&DeleteLine, cx); + assert_eq!(view.display_text(cx), "ghi"); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1) + ] + ); + }); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + build_editor(buffer, cx) + }) + .root(cx); + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)]) + }); + view.delete_line(&DeleteLine, cx); + assert_eq!(view.display_text(cx), "ghi\n"); + assert_eq!( + view.selections.display_ranges(cx), + vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)] + ); + }); +} + +#[gpui::test] +fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx); + let mut editor = build_editor(buffer.clone(), cx); + let buffer = buffer.read(cx).as_singleton().unwrap(); + + assert_eq!( + editor.selections.ranges::(cx), + &[Point::new(0, 0)..Point::new(0, 0)] + ); + + // When on single line, replace newline at end by space + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n"); + assert_eq!( + editor.selections.ranges::(cx), + &[Point::new(0, 3)..Point::new(0, 3)] + ); + + // When multiple lines are selected, remove newlines that are spanned by the selection + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(0, 5)..Point::new(2, 2)]) + }); + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n"); + assert_eq!( + editor.selections.ranges::(cx), + &[Point::new(0, 11)..Point::new(0, 11)] + ); + + // Undo should be transactional + editor.undo(&Undo, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n"); + assert_eq!( + editor.selections.ranges::(cx), + &[Point::new(0, 5)..Point::new(2, 2)] + ); + + // When joining an empty line don't insert a space + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(2, 1)..Point::new(2, 2)]) + }); + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n"); + assert_eq!( + editor.selections.ranges::(cx), + [Point::new(2, 3)..Point::new(2, 3)] + ); + + // We can remove trailing newlines + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd"); + assert_eq!( + editor.selections.ranges::(cx), + [Point::new(2, 3)..Point::new(2, 3)] + ); + + // We don't blow up on the last line + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd"); + assert_eq!( + editor.selections.ranges::(cx), + [Point::new(2, 3)..Point::new(2, 3)] + ); + + // reset to test indentation + editor.buffer.update(cx, |buffer, cx| { + buffer.edit( + [ + (Point::new(1, 0)..Point::new(1, 2), " "), + (Point::new(2, 0)..Point::new(2, 3), " \n\td"), + ], + None, + cx, + ) + }); + + // We remove any leading spaces + assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td"); + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(0, 1)..Point::new(0, 1)]) + }); + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td"); + + // We don't insert a space for a line containing only spaces + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td"); + + // We ignore any leading tabs + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb c d"); + + editor + }); +} + +#[gpui::test] +fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx); + let mut editor = build_editor(buffer.clone(), cx); + let buffer = buffer.read(cx).as_singleton().unwrap(); + + editor.change_selections(None, cx, |s| { + s.select_ranges([ + Point::new(0, 2)..Point::new(1, 1), + Point::new(1, 2)..Point::new(1, 2), + Point::new(3, 1)..Point::new(3, 2), + ]) + }); + + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n"); + + assert_eq!( + editor.selections.ranges::(cx), + [ + Point::new(0, 7)..Point::new(0, 7), + Point::new(1, 3)..Point::new(1, 3) + ] + ); + editor + }); +} + +#[gpui::test] +async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + // Test sort_lines_case_insensitive() + cx.set_state(indoc! {" + «z + y + x + Z + Y + Xˇ» + "}); + cx.update_editor(|e, cx| e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, cx)); + cx.assert_editor_state(indoc! {" + «x + X + y + Y + z + Zˇ» + "}); + + // Test reverse_lines() + cx.set_state(indoc! {" + «5 + 4 + 3 + 2 + 1ˇ» + "}); + cx.update_editor(|e, cx| e.reverse_lines(&ReverseLines, cx)); + cx.assert_editor_state(indoc! {" + «1 + 2 + 3 + 4 + 5ˇ» + "}); + + // Skip testing shuffle_line() + + // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive() + // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines) + + // Don't manipulate when cursor is on single line, but expand the selection + cx.set_state(indoc! {" + ddˇdd + ccc + bb + a + "}); + cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); + cx.assert_editor_state(indoc! {" + «ddddˇ» + ccc + bb + a + "}); + + // Basic manipulate case + // Start selection moves to column 0 + // End of selection shrinks to fit shorter line + cx.set_state(indoc! {" + dd«d + ccc + bb + aaaaaˇ» + "}); + cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); + cx.assert_editor_state(indoc! {" + «aaaaa + bb + ccc + dddˇ» + "}); + + // Manipulate case with newlines + cx.set_state(indoc! {" + dd«d + ccc + + bb + aaaaa + + ˇ» + "}); + cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); + cx.assert_editor_state(indoc! {" + « + + aaaaa + bb + ccc + dddˇ» + + "}); +} + +#[gpui::test] +async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + // Manipulate with multiple selections on a single line + cx.set_state(indoc! {" + dd«dd + cˇ»c«c + bb + aaaˇ»aa + "}); + cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); + cx.assert_editor_state(indoc! {" + «aaaaa + bb + ccc + ddddˇ» + "}); + + // Manipulate with multiple disjoin selections + cx.set_state(indoc! {" + 5« + 4 + 3 + 2 + 1ˇ» + + dd«dd + ccc + bb + aaaˇ»aa + "}); + cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); + cx.assert_editor_state(indoc! {" + «1 + 2 + 3 + 4 + 5ˇ» + + «aaaaa + bb + ccc + ddddˇ» + "}); +} + +#[gpui::test] +async fn test_manipulate_text(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + // Test convert_to_upper_case() + cx.set_state(indoc! {" + «hello worldˇ» + "}); + cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); + cx.assert_editor_state(indoc! {" + «HELLO WORLDˇ» + "}); + + // Test convert_to_lower_case() + cx.set_state(indoc! {" + «HELLO WORLDˇ» + "}); + cx.update_editor(|e, cx| e.convert_to_lower_case(&ConvertToLowerCase, cx)); + cx.assert_editor_state(indoc! {" + «hello worldˇ» + "}); + + // Test multiple line, single selection case + // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary + cx.set_state(indoc! {" + «The quick brown + fox jumps over + the lazy dogˇ» + "}); + cx.update_editor(|e, cx| e.convert_to_title_case(&ConvertToTitleCase, cx)); + cx.assert_editor_state(indoc! {" + «The Quick Brown + Fox Jumps Over + The Lazy Dogˇ» + "}); + + // Test multiple line, single selection case + // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary + cx.set_state(indoc! {" + «The quick brown + fox jumps over + the lazy dogˇ» + "}); + cx.update_editor(|e, cx| e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, cx)); + cx.assert_editor_state(indoc! {" + «TheQuickBrown + FoxJumpsOver + TheLazyDogˇ» + "}); + + // From here on out, test more complex cases of manipulate_text() + + // Test no selection case - should affect words cursors are in + // Cursor at beginning, middle, and end of word + cx.set_state(indoc! {" + ˇhello big beauˇtiful worldˇ + "}); + cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); + cx.assert_editor_state(indoc! {" + «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ» + "}); + + // Test multiple selections on a single line and across multiple lines + cx.set_state(indoc! {" + «Theˇ» quick «brown + foxˇ» jumps «overˇ» + the «lazyˇ» dog + "}); + cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); + cx.assert_editor_state(indoc! {" + «THEˇ» quick «BROWN + FOXˇ» jumps «OVERˇ» + the «LAZYˇ» dog + "}); + + // Test case where text length grows + cx.set_state(indoc! {" + «tschüߡ» + "}); + cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); + cx.assert_editor_state(indoc! {" + «TSCHÜSSˇ» + "}); + + // Test to make sure we don't crash when text shrinks + cx.set_state(indoc! {" + aaa_bbbˇ + "}); + cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx)); + cx.assert_editor_state(indoc! {" + «aaaBbbˇ» + "}); + + // Test to make sure we all aware of the fact that each word can grow and shrink + // Final selections should be aware of this fact + cx.set_state(indoc! {" + aaa_bˇbb bbˇb_ccc ˇccc_ddd + "}); + cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx)); + cx.assert_editor_state(indoc! {" + «aaaBbbˇ» «bbbCccˇ» «cccDddˇ» + "}); +} + +#[gpui::test] +fn test_duplicate_line(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + build_editor(buffer, cx) + }) + .root(cx); + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + ]) + }); + view.duplicate_line(&DuplicateLine, cx); + assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n"); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), + DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0), + ] + ); + }); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + build_editor(buffer, cx) + }) + .root(cx); + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1), + ]) + }); + view.duplicate_line(&DuplicateLine, cx); + assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n"); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(3, 1)..DisplayPoint::new(4, 1), + DisplayPoint::new(4, 2)..DisplayPoint::new(5, 1), + ] + ); + }); +} + +#[gpui::test] +fn test_move_line_up_down(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); + build_editor(buffer, cx) + }) + .root(cx); + view.update(cx, |view, cx| { + view.fold_ranges( + vec![ + Point::new(0, 2)..Point::new(1, 2), + Point::new(2, 3)..Point::new(4, 1), + Point::new(7, 0)..Point::new(8, 4), + ], + true, + cx, + ); + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), + DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2), + ]) + }); + assert_eq!( + view.display_text(cx), + "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj" + ); + + view.move_line_up(&MoveLineUp, cx); + assert_eq!( + view.display_text(cx), + "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff" + ); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3), + DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_line_down(&MoveLineDown, cx); + assert_eq!( + view.display_text(cx), + "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj" + ); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), + DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_line_down(&MoveLineDown, cx); + assert_eq!( + view.display_text(cx), + "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj" + ); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), + DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_line_up(&MoveLineUp, cx); + assert_eq!( + view.display_text(cx), + "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff" + ); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3), + DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) + ] + ); + }); +} + +#[gpui::test] +fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); + build_editor(buffer, cx) + }) + .root(cx); + editor.update(cx, |editor, cx| { + let snapshot = editor.buffer.read(cx).snapshot(cx); + editor.insert_blocks( + [BlockProperties { + style: BlockStyle::Fixed, + position: snapshot.anchor_after(Point::new(2, 0)), + disposition: BlockDisposition::Below, + height: 1, + render: Arc::new(|_| Empty::new().into_any()), + }], + Some(Autoscroll::fit()), + cx, + ); + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(2, 0)..Point::new(2, 0)]) + }); + editor.move_line_down(&MoveLineDown, cx); + }); +} + +#[gpui::test] +fn test_transpose(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + _ = cx.add_window(|cx| { + let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), cx); + + editor.change_selections(None, cx, |s| s.select_ranges([1..1])); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bac"); + assert_eq!(editor.selections.ranges(cx), [2..2]); + + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bca"); + assert_eq!(editor.selections.ranges(cx), [3..3]); + + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bac"); + assert_eq!(editor.selections.ranges(cx), [3..3]); + + editor + }); + + _ = cx.add_window(|cx| { + let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); + + editor.change_selections(None, cx, |s| s.select_ranges([3..3])); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "acb\nde"); + assert_eq!(editor.selections.ranges(cx), [3..3]); + + editor.change_selections(None, cx, |s| s.select_ranges([4..4])); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "acbd\ne"); + assert_eq!(editor.selections.ranges(cx), [5..5]); + + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "acbde\n"); + assert_eq!(editor.selections.ranges(cx), [6..6]); + + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "acbd\ne"); + assert_eq!(editor.selections.ranges(cx), [6..6]); + + editor + }); + + _ = cx.add_window(|cx| { + let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); + + editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2, 4..4])); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bacd\ne"); + assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]); + + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bcade\n"); + assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]); + + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bcda\ne"); + assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]); + + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bcade\n"); + assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]); + + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bcaed\n"); + assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]); + + editor + }); + + _ = cx.add_window(|cx| { + let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), cx); + + editor.change_selections(None, cx, |s| s.select_ranges([4..4])); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "🏀🍐✋"); + assert_eq!(editor.selections.ranges(cx), [8..8]); + + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "🏀✋🍐"); + assert_eq!(editor.selections.ranges(cx), [11..11]); + + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "🏀🍐✋"); + assert_eq!(editor.selections.ranges(cx), [11..11]); + + editor + }); +} + +#[gpui::test] +async fn test_clipboard(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six "); + cx.update_editor(|e, cx| e.cut(&Cut, cx)); + cx.assert_editor_state("ˇtwo ˇfour ˇsix "); + + // Paste with three cursors. Each cursor pastes one slice of the clipboard text. + cx.set_state("two ˇfour ˇsix ˇ"); + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ"); + + // Paste again but with only two cursors. Since the number of cursors doesn't + // match the number of slices in the clipboard, the entire clipboard text + // is pasted at each cursor. + cx.set_state("ˇtwo one✅ four three six five ˇ"); + cx.update_editor(|e, cx| { + e.handle_input("( ", cx); + e.paste(&Paste, cx); + e.handle_input(") ", cx); + }); + cx.assert_editor_state( + &([ + "( one✅ ", + "three ", + "five ) ˇtwo one✅ four three six five ( one✅ ", + "three ", + "five ) ˇ", + ] + .join("\n")), + ); + + // Cut with three selections, one of which is full-line. + cx.set_state(indoc! {" + 1«2ˇ»3 + 4ˇ567 + «8ˇ»9"}); + cx.update_editor(|e, cx| e.cut(&Cut, cx)); + cx.assert_editor_state(indoc! {" + 1ˇ3 + ˇ9"}); + + // Paste with three selections, noticing how the copied selection that was full-line + // gets inserted before the second cursor. + cx.set_state(indoc! {" + 1ˇ3 + 9ˇ + «oˇ»ne"}); + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state(indoc! {" + 12ˇ3 + 4567 + 9ˇ + 8ˇne"}); + + // Copy with a single cursor only, which writes the whole line into the clipboard. + cx.set_state(indoc! {" + The quick brown + fox juˇmps over + the lazy dog"}); + cx.update_editor(|e, cx| e.copy(&Copy, cx)); + cx.cx.assert_clipboard_content(Some("fox jumps over\n")); + + // Paste with three selections, noticing how the copied full-line selection is inserted + // before the empty selections but replaces the selection that is non-empty. + cx.set_state(indoc! {" + Tˇhe quick brown + «foˇ»x jumps over + tˇhe lazy dog"}); + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state(indoc! {" + fox jumps over + Tˇhe quick brown + fox jumps over + ˇx jumps over + fox jumps over + tˇhe lazy dog"}); +} + +#[gpui::test] +async fn test_paste_multiline(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + let language = Arc::new(Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::language()), + )); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + + // Cut an indented block, without the leading whitespace. + cx.set_state(indoc! {" + const a: B = ( + c(), + «d( + e, + f + )ˇ» + ); + "}); + cx.update_editor(|e, cx| e.cut(&Cut, cx)); + cx.assert_editor_state(indoc! {" + const a: B = ( + c(), + ˇ + ); + "}); + + // Paste it at the same position. + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state(indoc! {" + const a: B = ( + c(), + d( + e, + f + )ˇ + ); + "}); + + // Paste it at a line with a lower indent level. + cx.set_state(indoc! {" + ˇ + const a: B = ( + c(), + ); + "}); + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state(indoc! {" + d( + e, + f + )ˇ + const a: B = ( + c(), + ); + "}); + + // Cut an indented block, with the leading whitespace. + cx.set_state(indoc! {" + const a: B = ( + c(), + « d( + e, + f + ) + ˇ»); + "}); + cx.update_editor(|e, cx| e.cut(&Cut, cx)); + cx.assert_editor_state(indoc! {" + const a: B = ( + c(), + ˇ); + "}); + + // Paste it at the same position. + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state(indoc! {" + const a: B = ( + c(), + d( + e, + f + ) + ˇ); + "}); + + // Paste it at a line with a higher indent level. + cx.set_state(indoc! {" + const a: B = ( + c(), + d( + e, + fˇ + ) + ); + "}); + cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.assert_editor_state(indoc! {" + const a: B = ( + c(), + d( + e, + f d( + e, + f + ) + ˇ + ) + ); + "}); +} + +#[gpui::test] +fn test_select_all(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx); + build_editor(buffer, cx) + }) + .root(cx); + view.update(cx, |view, cx| { + view.select_all(&SelectAll, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)] + ); + }); +} + +#[gpui::test] +fn test_select_line(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx); + build_editor(buffer, cx) + }) + .root(cx); + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2), + ]) + }); + view.select_line(&SelectLine, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(0, 0)..DisplayPoint::new(2, 0), + DisplayPoint::new(4, 0)..DisplayPoint::new(5, 0), + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_line(&SelectLine, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(0, 0)..DisplayPoint::new(3, 0), + DisplayPoint::new(4, 0)..DisplayPoint::new(5, 5), + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_line(&SelectLine, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)] + ); + }); +} + +#[gpui::test] +fn test_split_selection_into_lines(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); + build_editor(buffer, cx) + }) + .root(cx); + view.update(cx, |view, cx| { + view.fold_ranges( + vec![ + Point::new(0, 2)..Point::new(1, 2), + Point::new(2, 3)..Point::new(4, 1), + Point::new(7, 0)..Point::new(8, 4), + ], + true, + cx, + ); + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4), + ]) + }); + assert_eq!(view.display_text(cx), "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"); + }); + + view.update(cx, |view, cx| { + view.split_selection_into_lines(&SplitSelectionIntoLines, cx); + assert_eq!( + view.display_text(cx), + "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i" + ); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), + DisplayPoint::new(5, 4)..DisplayPoint::new(5, 4) + ] + ); + }); + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(5, 0)..DisplayPoint::new(0, 1)]) + }); + view.split_selection_into_lines(&SplitSelectionIntoLines, cx); + assert_eq!( + view.display_text(cx), + "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii" + ); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 5)..DisplayPoint::new(0, 5), + DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), + DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), + DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5), + DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5), + DisplayPoint::new(5, 5)..DisplayPoint::new(5, 5), + DisplayPoint::new(6, 5)..DisplayPoint::new(6, 5), + DisplayPoint::new(7, 0)..DisplayPoint::new(7, 0) + ] + ); + }); +} + +#[gpui::test] +fn test_add_selection_above_below(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx); + build_editor(buffer, cx) + }) + .root(cx); + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)]) + }); + }); + view.update(cx, |view, cx| { + view.add_selection_above(&AddSelectionAbove, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_above(&AddSelectionAbove, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] + ); + + view.undo_selection(&UndoSelection, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) + ] + ); + + view.redo_selection(&RedoSelection, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), + DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), + DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) + ] + ); + }); + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)]) + }); + }); + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), + DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), + DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_above(&AddSelectionAbove, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_above(&AddSelectionAbove, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] + ); + }); + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(1, 4)]) + }); + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), + DisplayPoint::new(4, 1)..DisplayPoint::new(4, 4), + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_above(&AddSelectionAbove, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(4, 3)..DisplayPoint::new(1, 1)]) + }); + }); + view.update(cx, |view, cx| { + view.add_selection_above(&AddSelectionAbove, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), + DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selections.display_ranges(cx), + vec![ + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), + DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), + ] + ); + }); +} + +#[gpui::test] +async fn test_select_next(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("abc\nˇabc abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) + .unwrap(); + cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) + .unwrap(); + cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc"); + + cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); + cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); + cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) + .unwrap(); + cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); + + cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); +} + +#[gpui::test] +async fn test_select_previous(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + { + // `Select previous` without a selection (selects wordwise) + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("abc\nˇabc abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); + cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); + } + { + // `Select previous` with a selection + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); + + cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); + + cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + .unwrap(); + cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»"); + } +} + +#[gpui::test] +async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let language = Arc::new(Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::language()), + )); + + let text = r#" + use mod1::mod2::{mod3, mod4}; + + fn fn_1(param1: bool, param2: &str) { + let var1 = "text"; + } + "# + .unindent(); + + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); + view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + .await; + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), + DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), + DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), + ]); + }); + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + }); + assert_eq!( + view.update(cx, |view, cx| { view.selections.display_ranges(cx) }), + &[ + DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), + DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), + DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21), + ] + ); + + view.update(cx, |view, cx| { + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + }); + assert_eq!( + view.update(cx, |view, cx| view.selections.display_ranges(cx)), + &[ + DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), + DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), + ] + ); + + view.update(cx, |view, cx| { + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + }); + assert_eq!( + view.update(cx, |view, cx| view.selections.display_ranges(cx)), + &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] + ); + + // Trying to expand the selected syntax node one more time has no effect. + view.update(cx, |view, cx| { + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + }); + assert_eq!( + view.update(cx, |view, cx| view.selections.display_ranges(cx)), + &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] + ); + + view.update(cx, |view, cx| { + view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); + }); + assert_eq!( + view.update(cx, |view, cx| view.selections.display_ranges(cx)), + &[ + DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), + DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), + ] + ); + + view.update(cx, |view, cx| { + view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); + }); + assert_eq!( + view.update(cx, |view, cx| view.selections.display_ranges(cx)), + &[ + DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), + DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), + DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21), + ] + ); + + view.update(cx, |view, cx| { + view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); + }); + assert_eq!( + view.update(cx, |view, cx| view.selections.display_ranges(cx)), + &[ + DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), + DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), + DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), + ] + ); + + // Trying to shrink the selected syntax node one more time has no effect. + view.update(cx, |view, cx| { + view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); + }); + assert_eq!( + view.update(cx, |view, cx| view.selections.display_ranges(cx)), + &[ + DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), + DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), + DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), + ] + ); + + // Ensure that we keep expanding the selection if the larger selection starts or ends within + // a fold. + view.update(cx, |view, cx| { + view.fold_ranges( + vec![ + Point::new(0, 21)..Point::new(0, 24), + Point::new(3, 20)..Point::new(3, 22), + ], + true, + cx, + ); + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + }); + assert_eq!( + view.update(cx, |view, cx| view.selections.display_ranges(cx)), + &[ + DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), + DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), + DisplayPoint::new(3, 4)..DisplayPoint::new(3, 23), + ] + ); +} + +#[gpui::test] +async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let language = Arc::new( + Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: false, + newline: true, + }, + BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: false, + newline: true, + }, + ], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_indents_query( + r#" + (_ "(" ")" @end) @indent + (_ "{" "}" @end) @indent + "#, + ) + .unwrap(), + ); + + let text = "fn a() {}"; + + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); + editor + .condition(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) + .await; + + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([5..5, 8..8, 9..9])); + editor.newline(&Newline, cx); + assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n"); + assert_eq!( + editor.selections.ranges(cx), + &[ + Point::new(1, 4)..Point::new(1, 4), + Point::new(3, 4)..Point::new(3, 4), + Point::new(5, 0)..Point::new(5, 0) + ] + ); + }); +} + +#[gpui::test] +async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let language = Arc::new(Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }, + BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: true, + newline: true, + }, + BracketPair { + start: "/*".to_string(), + end: " */".to_string(), + close: true, + newline: true, + }, + BracketPair { + start: "[".to_string(), + end: "]".to_string(), + close: false, + newline: true, + }, + BracketPair { + start: "\"".to_string(), + end: "\"".to_string(), + close: true, + newline: false, + }, + ], + ..Default::default() + }, + autoclose_before: "})]".to_string(), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let registry = Arc::new(LanguageRegistry::test()); + registry.add(language.clone()); + cx.update_buffer(|buffer, cx| { + buffer.set_language_registry(registry); + buffer.set_language(Some(language), cx); + }); + + cx.set_state( + &r#" + 🏀ˇ + εˇ + ❤️ˇ + "# + .unindent(), + ); + + // autoclose multiple nested brackets at multiple cursors + cx.update_editor(|view, cx| { + view.handle_input("{", cx); + view.handle_input("{", cx); + view.handle_input("{", cx); + }); + cx.assert_editor_state( + &" + 🏀{{{ˇ}}} + ε{{{ˇ}}} + ❤️{{{ˇ}}} + " + .unindent(), + ); + + // insert a different closing bracket + cx.update_editor(|view, cx| { + view.handle_input(")", cx); + }); + cx.assert_editor_state( + &" + 🏀{{{)ˇ}}} + ε{{{)ˇ}}} + ❤️{{{)ˇ}}} + " + .unindent(), + ); + + // skip over the auto-closed brackets when typing a closing bracket + cx.update_editor(|view, cx| { + view.move_right(&MoveRight, cx); + view.handle_input("}", cx); + view.handle_input("}", cx); + view.handle_input("}", cx); + }); + cx.assert_editor_state( + &" + 🏀{{{)}}}}ˇ + ε{{{)}}}}ˇ + ❤️{{{)}}}}ˇ + " + .unindent(), + ); + + // autoclose multi-character pairs + cx.set_state( + &" + ˇ + ˇ + " + .unindent(), + ); + cx.update_editor(|view, cx| { + view.handle_input("/", cx); + view.handle_input("*", cx); + }); + cx.assert_editor_state( + &" + /*ˇ */ + /*ˇ */ + " + .unindent(), + ); + + // one cursor autocloses a multi-character pair, one cursor + // does not autoclose. + cx.set_state( + &" + /ˇ + ˇ + " + .unindent(), + ); + cx.update_editor(|view, cx| view.handle_input("*", cx)); + cx.assert_editor_state( + &" + /*ˇ */ + *ˇ + " + .unindent(), + ); + + // Don't autoclose if the next character isn't whitespace and isn't + // listed in the language's "autoclose_before" section. + cx.set_state("ˇa b"); + cx.update_editor(|view, cx| view.handle_input("{", cx)); + cx.assert_editor_state("{ˇa b"); + + // Don't autoclose if `close` is false for the bracket pair + cx.set_state("ˇ"); + cx.update_editor(|view, cx| view.handle_input("[", cx)); + cx.assert_editor_state("[ˇ"); + + // Surround with brackets if text is selected + cx.set_state("«aˇ» b"); + cx.update_editor(|view, cx| view.handle_input("{", cx)); + cx.assert_editor_state("{«aˇ»} b"); + + // Autclose pair where the start and end characters are the same + cx.set_state("aˇ"); + cx.update_editor(|view, cx| view.handle_input("\"", cx)); + cx.assert_editor_state("a\"ˇ\""); + cx.update_editor(|view, cx| view.handle_input("\"", cx)); + cx.assert_editor_state("a\"\"ˇ"); +} + +#[gpui::test] +async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let html_language = Arc::new( + Language::new( + LanguageConfig { + name: "HTML".into(), + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "<".into(), + end: ">".into(), + close: true, + ..Default::default() + }, + BracketPair { + start: "{".into(), + end: "}".into(), + close: true, + ..Default::default() + }, + BracketPair { + start: "(".into(), + end: ")".into(), + close: true, + ..Default::default() + }, + ], + ..Default::default() + }, + autoclose_before: "})]>".into(), + ..Default::default() + }, + Some(tree_sitter_html::language()), + ) + .with_injection_query( + r#" + (script_element + (raw_text) @content + (#set! "language" "javascript")) + "#, + ) + .unwrap(), + ); + + let javascript_language = Arc::new(Language::new( + LanguageConfig { + name: "JavaScript".into(), + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "/*".into(), + end: " */".into(), + close: true, + ..Default::default() + }, + BracketPair { + start: "{".into(), + end: "}".into(), + close: true, + ..Default::default() + }, + BracketPair { + start: "(".into(), + end: ")".into(), + close: true, + ..Default::default() + }, + ], + ..Default::default() + }, + autoclose_before: "})]>".into(), + ..Default::default() + }, + Some(tree_sitter_typescript::language_tsx()), + )); + + let registry = Arc::new(LanguageRegistry::test()); + registry.add(html_language.clone()); + registry.add(javascript_language.clone()); + + cx.update_buffer(|buffer, cx| { + buffer.set_language_registry(registry); + buffer.set_language(Some(html_language), cx); + }); + + cx.set_state( + &r#" + ˇ + + ˇ + "# + .unindent(), + ); + + // Precondition: different languages are active at different locations. + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let cursors = editor.selections.ranges::(cx); + let languages = cursors + .iter() + .map(|c| snapshot.language_at(c.start).unwrap().name()) + .collect::>(); + assert_eq!( + languages, + &["HTML".into(), "JavaScript".into(), "HTML".into()] + ); + }); + + // Angle brackets autoclose in HTML, but not JavaScript. + cx.update_editor(|editor, cx| { + editor.handle_input("<", cx); + editor.handle_input("a", cx); + }); + cx.assert_editor_state( + &r#" + + + + "# + .unindent(), + ); + + // Curly braces and parens autoclose in both HTML and JavaScript. + cx.update_editor(|editor, cx| { + editor.handle_input(" b=", cx); + editor.handle_input("{", cx); + editor.handle_input("c", cx); + editor.handle_input("(", cx); + }); + cx.assert_editor_state( + &r#" + + + + "# + .unindent(), + ); + + // Brackets that were already autoclosed are skipped. + cx.update_editor(|editor, cx| { + editor.handle_input(")", cx); + editor.handle_input("d", cx); + editor.handle_input("}", cx); + }); + cx.assert_editor_state( + &r#" + + + + "# + .unindent(), + ); + cx.update_editor(|editor, cx| { + editor.handle_input(">", cx); + }); + cx.assert_editor_state( + &r#" + ˇ + + ˇ + "# + .unindent(), + ); + + // Reset + cx.set_state( + &r#" + ˇ + + ˇ + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| { + editor.handle_input("<", cx); + }); + cx.assert_editor_state( + &r#" + <ˇ> + + <ˇ> + "# + .unindent(), + ); + + // When backspacing, the closing angle brackets are removed. + cx.update_editor(|editor, cx| { + editor.backspace(&Backspace, cx); + }); + cx.assert_editor_state( + &r#" + ˇ + + ˇ + "# + .unindent(), + ); + + // Block comments autoclose in JavaScript, but not HTML. + cx.update_editor(|editor, cx| { + editor.handle_input("/", cx); + editor.handle_input("*", cx); + }); + cx.assert_editor_state( + &r#" + /*ˇ + + /*ˇ + "# + .unindent(), + ); +} + +#[gpui::test] +async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let rust_language = Arc::new( + Language::new( + LanguageConfig { + name: "Rust".into(), + brackets: serde_json::from_value(json!([ + { "start": "{", "end": "}", "close": true, "newline": true }, + { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] }, + ])) + .unwrap(), + autoclose_before: "})]>".into(), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_override_query("(string_literal) @string") + .unwrap(), + ); + + let registry = Arc::new(LanguageRegistry::test()); + registry.add(rust_language.clone()); + + cx.update_buffer(|buffer, cx| { + buffer.set_language_registry(registry); + buffer.set_language(Some(rust_language), cx); + }); + + cx.set_state( + &r#" + let x = ˇ + "# + .unindent(), + ); + + // Inserting a quotation mark. A closing quotation mark is automatically inserted. + cx.update_editor(|editor, cx| { + editor.handle_input("\"", cx); + }); + cx.assert_editor_state( + &r#" + let x = "ˇ" + "# + .unindent(), + ); + + // Inserting another quotation mark. The cursor moves across the existing + // automatically-inserted quotation mark. + cx.update_editor(|editor, cx| { + editor.handle_input("\"", cx); + }); + cx.assert_editor_state( + &r#" + let x = ""ˇ + "# + .unindent(), + ); + + // Reset + cx.set_state( + &r#" + let x = ˇ + "# + .unindent(), + ); + + // Inserting a quotation mark inside of a string. A second quotation mark is not inserted. + cx.update_editor(|editor, cx| { + editor.handle_input("\"", cx); + editor.handle_input(" ", cx); + editor.move_left(&Default::default(), cx); + editor.handle_input("\\", cx); + editor.handle_input("\"", cx); + }); + cx.assert_editor_state( + &r#" + let x = "\"ˇ " + "# + .unindent(), + ); + + // Inserting a closing quotation mark at the position of an automatically-inserted quotation + // mark. Nothing is inserted. + cx.update_editor(|editor, cx| { + editor.move_right(&Default::default(), cx); + editor.handle_input("\"", cx); + }); + cx.assert_editor_state( + &r#" + let x = "\" "ˇ + "# + .unindent(), + ); +} + +#[gpui::test] +async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let language = Arc::new(Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }, + BracketPair { + start: "/* ".to_string(), + end: "*/".to_string(), + close: true, + ..Default::default() + }, + ], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let text = r#" + a + b + c + "# + .unindent(); + + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); + view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + .await; + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1), + ]) + }); + + view.handle_input("{", cx); + view.handle_input("{", cx); + view.handle_input("{", cx); + assert_eq!( + view.text(cx), + " + {{{a}}} + {{{b}}} + {{{c}}} + " + .unindent() + ); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 4), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 4), + DisplayPoint::new(2, 3)..DisplayPoint::new(2, 4) + ] + ); + + view.undo(&Undo, cx); + view.undo(&Undo, cx); + view.undo(&Undo, cx); + assert_eq!( + view.text(cx), + " + a + b + c + " + .unindent() + ); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1) + ] + ); + + // Ensure inserting the first character of a multi-byte bracket pair + // doesn't surround the selections with the bracket. + view.handle_input("/", cx); + assert_eq!( + view.text(cx), + " + / + / + / + " + .unindent() + ); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1) + ] + ); + + view.undo(&Undo, cx); + assert_eq!( + view.text(cx), + " + a + b + c + " + .unindent() + ); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1) + ] + ); + + // Ensure inserting the last character of a multi-byte bracket pair + // doesn't surround the selections with the bracket. + view.handle_input("*", cx); + assert_eq!( + view.text(cx), + " + * + * + * + " + .unindent() + ); + assert_eq!( + view.selections.display_ranges(cx), + [ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1) + ] + ); + }); +} + +#[gpui::test] +async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let language = Arc::new(Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }], + ..Default::default() + }, + autoclose_before: "}".to_string(), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let text = r#" + a + b + c + "# + .unindent(); + + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); + editor + .condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + .await; + + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([ + Point::new(0, 1)..Point::new(0, 1), + Point::new(1, 1)..Point::new(1, 1), + Point::new(2, 1)..Point::new(2, 1), + ]) + }); + + editor.handle_input("{", cx); + editor.handle_input("{", cx); + editor.handle_input("_", cx); + assert_eq!( + editor.text(cx), + " + a{{_}} + b{{_}} + c{{_}} + " + .unindent() + ); + assert_eq!( + editor.selections.ranges::(cx), + [ + Point::new(0, 4)..Point::new(0, 4), + Point::new(1, 4)..Point::new(1, 4), + Point::new(2, 4)..Point::new(2, 4) + ] + ); + + editor.backspace(&Default::default(), cx); + editor.backspace(&Default::default(), cx); + assert_eq!( + editor.text(cx), + " + a{} + b{} + c{} + " + .unindent() + ); + assert_eq!( + editor.selections.ranges::(cx), + [ + Point::new(0, 2)..Point::new(0, 2), + Point::new(1, 2)..Point::new(1, 2), + Point::new(2, 2)..Point::new(2, 2) + ] + ); + + editor.delete_to_previous_word_start(&Default::default(), cx); + assert_eq!( + editor.text(cx), + " + a + b + c + " + .unindent() + ); + assert_eq!( + editor.selections.ranges::(cx), + [ + Point::new(0, 1)..Point::new(0, 1), + Point::new(1, 1)..Point::new(1, 1), + Point::new(2, 1)..Point::new(2, 1) + ] + ); + }); +} + +#[gpui::test] +async fn test_snippets(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let (text, insertion_ranges) = marked_text_ranges( + indoc! {" + a.ˇ b + a.ˇ b + a.ˇ b + "}, + false, + ); + + let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); + + editor.update(cx, |editor, cx| { + let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap(); + + editor + .insert_snippet(&insertion_ranges, snippet, cx) + .unwrap(); + + fn assert(editor: &mut Editor, cx: &mut ViewContext, marked_text: &str) { + let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false); + assert_eq!(editor.text(cx), expected_text); + assert_eq!(editor.selections.ranges::(cx), selection_ranges); + } + + assert( + editor, + cx, + indoc! {" + a.f(«one», two, «three») b + a.f(«one», two, «three») b + a.f(«one», two, «three») b + "}, + ); + + // Can't move earlier than the first tab stop + assert!(!editor.move_to_prev_snippet_tabstop(cx)); + assert( + editor, + cx, + indoc! {" + a.f(«one», two, «three») b + a.f(«one», two, «three») b + a.f(«one», two, «three») b + "}, + ); + + assert!(editor.move_to_next_snippet_tabstop(cx)); + assert( + editor, + cx, + indoc! {" + a.f(one, «two», three) b + a.f(one, «two», three) b + a.f(one, «two», three) b + "}, + ); + + editor.move_to_prev_snippet_tabstop(cx); + assert( + editor, + cx, + indoc! {" + a.f(«one», two, «three») b + a.f(«one», two, «three») b + a.f(«one», two, «three») b + "}, + ); + + assert!(editor.move_to_next_snippet_tabstop(cx)); + assert( + editor, + cx, + indoc! {" + a.f(one, «two», three) b + a.f(one, «two», three) b + a.f(one, «two», three) b + "}, + ); + assert!(editor.move_to_next_snippet_tabstop(cx)); + assert( + editor, + cx, + indoc! {" + a.f(one, two, three)ˇ b + a.f(one, two, three)ˇ b + a.f(one, two, three)ˇ b + "}, + ); + + // As soon as the last tab stop is reached, snippet state is gone + editor.move_to_prev_snippet_tabstop(cx); + assert( + editor, + cx, + indoc! {" + a.f(one, two, three)ˇ b + a.f(one, two, three)ˇ b + a.f(one, two, three)ˇ b + "}, + ); + }); +} + +#[gpui::test] +async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_formatting_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.background()); + fs.insert_file("/file.rs", Default::default()).await; + + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let buffer = project + .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) + .await + .unwrap(); + + cx.foreground().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); + editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + assert!(cx.read(|cx| editor.is_dirty(cx))); + + let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); + fake_server + .handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + assert_eq!(params.options.tab_size, 4); + Ok(Some(vec![lsp::TextEdit::new( + lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), + ", ".to_string(), + )])) + }) + .next() + .await; + cx.foreground().start_waiting(); + save.await.unwrap(); + assert_eq!( + editor.read_with(cx, |editor, cx| editor.text(cx)), + "one, two\nthree\n" + ); + assert!(!cx.read(|cx| editor.is_dirty(cx))); + + editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + assert!(cx.read(|cx| editor.is_dirty(cx))); + + // Ensure we can still save even if formatting hangs. + fake_server.handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + futures::future::pending::<()>().await; + unreachable!() + }); + let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); + cx.foreground().advance_clock(super::FORMAT_TIMEOUT); + cx.foreground().start_waiting(); + save.await.unwrap(); + assert_eq!( + editor.read_with(cx, |editor, cx| editor.text(cx)), + "one\ntwo\nthree\n" + ); + assert!(!cx.read(|cx| editor.is_dirty(cx))); + + // Set rust language override and assert overridden tabsize is sent to language server + update_test_language_settings(cx, |settings| { + settings.languages.insert( + "Rust".into(), + LanguageSettingsContent { + tab_size: NonZeroU32::new(8), + ..Default::default() + }, + ); + }); + + let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); + fake_server + .handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + assert_eq!(params.options.tab_size, 8); + Ok(Some(vec![])) + }) + .next() + .await; + cx.foreground().start_waiting(); + save.await.unwrap(); +} + +#[gpui::test] +async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_range_formatting_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.background()); + fs.insert_file("/file.rs", Default::default()).await; + + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let buffer = project + .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) + .await + .unwrap(); + + cx.foreground().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); + editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + assert!(cx.read(|cx| editor.is_dirty(cx))); + + let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); + fake_server + .handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + assert_eq!(params.options.tab_size, 4); + Ok(Some(vec![lsp::TextEdit::new( + lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), + ", ".to_string(), + )])) + }) + .next() + .await; + cx.foreground().start_waiting(); + save.await.unwrap(); + assert_eq!( + editor.read_with(cx, |editor, cx| editor.text(cx)), + "one, two\nthree\n" + ); + assert!(!cx.read(|cx| editor.is_dirty(cx))); + + editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + assert!(cx.read(|cx| editor.is_dirty(cx))); + + // Ensure we can still save even if formatting hangs. + fake_server.handle_request::( + move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + futures::future::pending::<()>().await; + unreachable!() + }, + ); + let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); + cx.foreground().advance_clock(super::FORMAT_TIMEOUT); + cx.foreground().start_waiting(); + save.await.unwrap(); + assert_eq!( + editor.read_with(cx, |editor, cx| editor.text(cx)), + "one\ntwo\nthree\n" + ); + assert!(!cx.read(|cx| editor.is_dirty(cx))); + + // Set rust language override and assert overridden tabsize is sent to language server + update_test_language_settings(cx, |settings| { + settings.languages.insert( + "Rust".into(), + LanguageSettingsContent { + tab_size: NonZeroU32::new(8), + ..Default::default() + }, + ); + }); + + let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); + fake_server + .handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + assert_eq!(params.options.tab_size, 8); + Ok(Some(vec![])) + }) + .next() + .await; + cx.foreground().start_waiting(); + save.await.unwrap(); +} + +#[gpui::test] +async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer) + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + // Enable Prettier formatting for the same buffer, and ensure + // LSP is called instead of Prettier. + prettier_parser_name: Some("test_parser".to_string()), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_formatting_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.background()); + fs.insert_file("/file.rs", Default::default()).await; + + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + project.update(cx, |project, _| { + project.languages().add(Arc::new(language)); + }); + let buffer = project + .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) + .await + .unwrap(); + + cx.foreground().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); + editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + + let format = editor.update(cx, |editor, cx| { + editor.perform_format(project.clone(), FormatTrigger::Manual, cx) + }); + fake_server + .handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + assert_eq!(params.options.tab_size, 4); + Ok(Some(vec![lsp::TextEdit::new( + lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), + ", ".to_string(), + )])) + }) + .next() + .await; + cx.foreground().start_waiting(); + format.await.unwrap(); + assert_eq!( + editor.read_with(cx, |editor, cx| editor.text(cx)), + "one, two\nthree\n" + ); + + editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + // Ensure we don't lock if formatting hangs. + fake_server.handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + futures::future::pending::<()>().await; + unreachable!() + }); + let format = editor.update(cx, |editor, cx| { + editor.perform_format(project, FormatTrigger::Manual, cx) + }); + cx.foreground().advance_clock(super::FORMAT_TIMEOUT); + cx.foreground().start_waiting(); + format.await.unwrap(); + assert_eq!( + editor.read_with(cx, |editor, cx| editor.text(cx)), + "one\ntwo\nthree\n" + ); +} + +#[gpui::test] +async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + document_formatting_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + one.twoˇ + "}); + + // The format request takes a long time. When it completes, it inserts + // a newline and an indent before the `.` + cx.lsp + .handle_request::(move |_, cx| { + let executor = cx.background(); + async move { + executor.timer(Duration::from_millis(100)).await; + Ok(Some(vec![lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)), + new_text: "\n ".into(), + }])) + } + }); + + // Submit a format request. + let format_1 = cx + .update_editor(|editor, cx| editor.format(&Format, cx)) + .unwrap(); + cx.foreground().run_until_parked(); + + // Submit a second format request. + let format_2 = cx + .update_editor(|editor, cx| editor.format(&Format, cx)) + .unwrap(); + cx.foreground().run_until_parked(); + + // Wait for both format requests to complete + cx.foreground().advance_clock(Duration::from_millis(200)); + cx.foreground().start_waiting(); + format_1.await.unwrap(); + cx.foreground().start_waiting(); + format_2.await.unwrap(); + + // The formatting edits only happens once. + cx.assert_editor_state(indoc! {" + one + .twoˇ + "}); +} + +#[gpui::test] +async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.formatter = Some(language_settings::Formatter::Auto) + }); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + document_formatting_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Set up a buffer white some trailing whitespace and no trailing newline. + cx.set_state( + &[ + "one ", // + "twoˇ", // + "three ", // + "four", // + ] + .join("\n"), + ); + + // Submit a format request. + let format = cx + .update_editor(|editor, cx| editor.format(&Format, cx)) + .unwrap(); + + // Record which buffer changes have been sent to the language server + let buffer_changes = Arc::new(Mutex::new(Vec::new())); + cx.lsp + .handle_notification::({ + let buffer_changes = buffer_changes.clone(); + move |params, _| { + buffer_changes.lock().extend( + params + .content_changes + .into_iter() + .map(|e| (e.range.unwrap(), e.text)), + ); + } + }); + + // Handle formatting requests to the language server. + cx.lsp.handle_request::({ + let buffer_changes = buffer_changes.clone(); + move |_, _| { + // When formatting is requested, trailing whitespace has already been stripped, + // and the trailing newline has already been added. + assert_eq!( + &buffer_changes.lock()[1..], + &[ + ( + lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)), + "".into() + ), + ( + lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)), + "".into() + ), + ( + lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)), + "\n".into() + ), + ] + ); + + // Insert blank lines between each line of the buffer. + async move { + Ok(Some(vec![ + lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)), + new_text: "\n".into(), + }, + lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)), + new_text: "\n".into(), + }, + ])) + } + } + }); + + // After formatting the buffer, the trailing whitespace is stripped, + // a newline is appended, and the edits provided by the language server + // have been applied. + format.await.unwrap(); + cx.assert_editor_state( + &[ + "one", // + "", // + "twoˇ", // + "", // + "three", // + "four", // + "", // + ] + .join("\n"), + ); + + // Undoing the formatting undoes the trailing whitespace removal, the + // trailing newline, and the LSP edits. + cx.update_buffer(|buffer, cx| buffer.undo(cx)); + cx.assert_editor_state( + &[ + "one ", // + "twoˇ", // + "three ", // + "four", // + ] + .join("\n"), + ); +} + +#[gpui::test] +async fn test_completion(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + resolve_provider: Some(true), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + oneˇ + two + three + "}); + cx.simulate_keystroke("."); + handle_completion_request( + &mut cx, + indoc! {" + one.|<> + two + three + "}, + vec!["first_completion", "second_completion"], + ) + .await; + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + let apply_additional_edits = cx.update_editor(|editor, cx| { + editor.context_menu_next(&Default::default(), cx); + editor + .confirm_completion(&ConfirmCompletion::default(), cx) + .unwrap() + }); + cx.assert_editor_state(indoc! {" + one.second_completionˇ + two + three + "}); + + handle_resolve_completion_request( + &mut cx, + Some(vec![ + ( + //This overlaps with the primary completion edit which is + //misbehavior from the LSP spec, test that we filter it out + indoc! {" + one.second_ˇcompletion + two + threeˇ + "}, + "overlapping additional edit", + ), + ( + indoc! {" + one.second_completion + two + threeˇ + "}, + "\nadditional edit", + ), + ]), + ) + .await; + apply_additional_edits.await.unwrap(); + cx.assert_editor_state(indoc! {" + one.second_completionˇ + two + three + additional edit + "}); + + cx.set_state(indoc! {" + one.second_completion + twoˇ + threeˇ + additional edit + "}); + cx.simulate_keystroke(" "); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); + cx.simulate_keystroke("s"); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); + + cx.assert_editor_state(indoc! {" + one.second_completion + two sˇ + three sˇ + additional edit + "}); + handle_completion_request( + &mut cx, + indoc! {" + one.second_completion + two s + three + additional edit + "}, + vec!["fourth_completion", "fifth_completion", "sixth_completion"], + ) + .await; + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + + cx.simulate_keystroke("i"); + + handle_completion_request( + &mut cx, + indoc! {" + one.second_completion + two si + three + additional edit + "}, + vec!["fourth_completion", "fifth_completion", "sixth_completion"], + ) + .await; + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + + let apply_additional_edits = cx.update_editor(|editor, cx| { + editor + .confirm_completion(&ConfirmCompletion::default(), cx) + .unwrap() + }); + cx.assert_editor_state(indoc! {" + one.second_completion + two sixth_completionˇ + three sixth_completionˇ + additional edit + "}); + + handle_resolve_completion_request(&mut cx, None).await; + apply_additional_edits.await.unwrap(); + + cx.update(|cx| { + cx.update_global::(|settings, cx| { + settings.update_user_settings::(cx, |settings| { + settings.show_completions_on_input = Some(false); + }); + }) + }); + cx.set_state("editorˇ"); + cx.simulate_keystroke("."); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); + cx.simulate_keystroke("c"); + cx.simulate_keystroke("l"); + cx.simulate_keystroke("o"); + cx.assert_editor_state("editor.cloˇ"); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); + cx.update_editor(|editor, cx| { + editor.show_completions(&ShowCompletions, cx); + }); + handle_completion_request(&mut cx, "editor.", vec!["close", "clobber"]).await; + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + let apply_additional_edits = cx.update_editor(|editor, cx| { + editor + .confirm_completion(&ConfirmCompletion::default(), cx) + .unwrap() + }); + cx.assert_editor_state("editor.closeˇ"); + handle_resolve_completion_request(&mut cx, None).await; + apply_additional_edits.await.unwrap(); +} + +#[gpui::test] +async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + let language = Arc::new(Language::new( + LanguageConfig { + line_comment: Some("// ".into()), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + + // If multiple selections intersect a line, the line is only toggled once. + cx.set_state(indoc! {" + fn a() { + «//b(); + ˇ»// «c(); + //ˇ» d(); + } + "}); + + cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + «b(); + c(); + ˇ» d(); + } + "}); + + // The comment prefix is inserted at the same column for every line in a + // selection. + cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + // «b(); + // c(); + ˇ»// d(); + } + "}); + + // If a selection ends at the beginning of a line, that line is not toggled. + cx.set_selections_state(indoc! {" + fn a() { + // b(); + «// c(); + ˇ» // d(); + } + "}); + + cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + // b(); + «c(); + ˇ» // d(); + } + "}); + + // If a selection span a single line and is empty, the line is toggled. + cx.set_state(indoc! {" + fn a() { + a(); + b(); + ˇ + } + "}); + + cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + a(); + b(); + //•ˇ + } + "}); + + // If a selection span multiple lines, empty lines are not toggled. + cx.set_state(indoc! {" + fn a() { + «a(); + + c();ˇ» + } + "}); + + cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + // «a(); + + // c();ˇ» + } + "}); +} + +#[gpui::test] +async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let language = Arc::new(Language::new( + LanguageConfig { + line_comment: Some("// ".into()), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let registry = Arc::new(LanguageRegistry::test()); + registry.add(language.clone()); + + let mut cx = EditorTestContext::new(cx).await; + cx.update_buffer(|buffer, cx| { + buffer.set_language_registry(registry); + buffer.set_language(Some(language), cx); + }); + + let toggle_comments = &ToggleComments { + advance_downwards: true, + }; + + // Single cursor on one line -> advance + // Cursor moves horizontally 3 characters as well on non-blank line + cx.set_state(indoc!( + "fn a() { + ˇdog(); + cat(); + }" + )); + cx.update_editor(|editor, cx| { + editor.toggle_comments(toggle_comments, cx); + }); + cx.assert_editor_state(indoc!( + "fn a() { + // dog(); + catˇ(); + }" + )); + + // Single selection on one line -> don't advance + cx.set_state(indoc!( + "fn a() { + «dog()ˇ»; + cat(); + }" + )); + cx.update_editor(|editor, cx| { + editor.toggle_comments(toggle_comments, cx); + }); + cx.assert_editor_state(indoc!( + "fn a() { + // «dog()ˇ»; + cat(); + }" + )); + + // Multiple cursors on one line -> advance + cx.set_state(indoc!( + "fn a() { + ˇdˇog(); + cat(); + }" + )); + cx.update_editor(|editor, cx| { + editor.toggle_comments(toggle_comments, cx); + }); + cx.assert_editor_state(indoc!( + "fn a() { + // dog(); + catˇ(ˇ); + }" + )); + + // Multiple cursors on one line, with selection -> don't advance + cx.set_state(indoc!( + "fn a() { + ˇdˇog«()ˇ»; + cat(); + }" + )); + cx.update_editor(|editor, cx| { + editor.toggle_comments(toggle_comments, cx); + }); + cx.assert_editor_state(indoc!( + "fn a() { + // ˇdˇog«()ˇ»; + cat(); + }" + )); + + // Single cursor on one line -> advance + // Cursor moves to column 0 on blank line + cx.set_state(indoc!( + "fn a() { + ˇdog(); + + cat(); + }" + )); + cx.update_editor(|editor, cx| { + editor.toggle_comments(toggle_comments, cx); + }); + cx.assert_editor_state(indoc!( + "fn a() { + // dog(); + ˇ + cat(); + }" + )); + + // Single cursor on one line -> advance + // Cursor starts and ends at column 0 + cx.set_state(indoc!( + "fn a() { + ˇ dog(); + cat(); + }" + )); + cx.update_editor(|editor, cx| { + editor.toggle_comments(toggle_comments, cx); + }); + cx.assert_editor_state(indoc!( + "fn a() { + // dog(); + ˇ cat(); + }" + )); +} + +#[gpui::test] +async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let html_language = Arc::new( + Language::new( + LanguageConfig { + name: "HTML".into(), + block_comment: Some(("".into())), + ..Default::default() + }, + Some(tree_sitter_html::language()), + ) + .with_injection_query( + r#" + (script_element + (raw_text) @content + (#set! "language" "javascript")) + "#, + ) + .unwrap(), + ); + + let javascript_language = Arc::new(Language::new( + LanguageConfig { + name: "JavaScript".into(), + line_comment: Some("// ".into()), + ..Default::default() + }, + Some(tree_sitter_typescript::language_tsx()), + )); + + let registry = Arc::new(LanguageRegistry::test()); + registry.add(html_language.clone()); + registry.add(javascript_language.clone()); + + cx.update_buffer(|buffer, cx| { + buffer.set_language_registry(registry); + buffer.set_language(Some(html_language), cx); + }); + + // Toggle comments for empty selections + cx.set_state( + &r#" +

A

ˇ +

B

ˇ +

C

ˇ + "# + .unindent(), + ); + cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); + cx.assert_editor_state( + &r#" + + + + "# + .unindent(), + ); + cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); + cx.assert_editor_state( + &r#" +

A

ˇ +

B

ˇ +

C

ˇ + "# + .unindent(), + ); + + // Toggle comments for mixture of empty and non-empty selections, where + // multiple selections occupy a given line. + cx.set_state( + &r#" +

+

ˇ»B

ˇ +

+

ˇ»D

ˇ + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); + cx.assert_editor_state( + &r#" + + + "# + .unindent(), + ); + cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); + cx.assert_editor_state( + &r#" +

+

ˇ»B

ˇ +

+

ˇ»D

ˇ + "# + .unindent(), + ); + + // Toggle comments when different languages are active for different + // selections. + cx.set_state( + &r#" + ˇ + "# + .unindent(), + ); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); + cx.assert_editor_state( + &r#" + + // ˇvar x = new Y(); + + "# + .unindent(), + ); +} + +#[gpui::test] +fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + buffer.clone(), + [ + ExcerptRange { + context: Point::new(0, 0)..Point::new(0, 4), + primary: None, + }, + ExcerptRange { + context: Point::new(1, 0)..Point::new(1, 4), + primary: None, + }, + ], + cx, + ); + assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb"); + multibuffer + }); + + let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); + view.update(cx, |view, cx| { + assert_eq!(view.text(cx), "aaaa\nbbbb"); + view.change_selections(None, cx, |s| { + s.select_ranges([ + Point::new(0, 0)..Point::new(0, 0), + Point::new(1, 0)..Point::new(1, 0), + ]) + }); + + view.handle_input("X", cx); + assert_eq!(view.text(cx), "Xaaaa\nXbbbb"); + assert_eq!( + view.selections.ranges(cx), + [ + Point::new(0, 1)..Point::new(0, 1), + Point::new(1, 1)..Point::new(1, 1), + ] + ); + + // Ensure the cursor's head is respected when deleting across an excerpt boundary. + view.change_selections(None, cx, |s| { + s.select_ranges([Point::new(0, 2)..Point::new(1, 2)]) + }); + view.backspace(&Default::default(), cx); + assert_eq!(view.text(cx), "Xa\nbbb"); + assert_eq!( + view.selections.ranges(cx), + [Point::new(1, 0)..Point::new(1, 0)] + ); + + view.change_selections(None, cx, |s| { + s.select_ranges([Point::new(1, 1)..Point::new(0, 1)]) + }); + view.backspace(&Default::default(), cx); + assert_eq!(view.text(cx), "X\nbb"); + assert_eq!( + view.selections.ranges(cx), + [Point::new(0, 1)..Point::new(0, 1)] + ); + }); +} + +#[gpui::test] +fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let markers = vec![('[', ']').into(), ('(', ')').into()]; + let (initial_text, mut excerpt_ranges) = marked_text_ranges_by( + indoc! {" + [aaaa + (bbbb] + cccc)", + }, + markers.clone(), + ); + let excerpt_ranges = markers.into_iter().map(|marker| { + let context = excerpt_ranges.remove(&marker).unwrap()[0].clone(); + ExcerptRange { + context, + primary: None, + } + }); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, initial_text)); + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts(buffer, excerpt_ranges, cx); + multibuffer + }); + + let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); + view.update(cx, |view, cx| { + let (expected_text, selection_ranges) = marked_text_ranges( + indoc! {" + aaaa + bˇbbb + bˇbbˇb + cccc" + }, + true, + ); + assert_eq!(view.text(cx), expected_text); + view.change_selections(None, cx, |s| s.select_ranges(selection_ranges)); + + view.handle_input("X", cx); + + let (expected_text, expected_selections) = marked_text_ranges( + indoc! {" + aaaa + bXˇbbXb + bXˇbbXˇb + cccc" + }, + false, + ); + assert_eq!(view.text(cx), expected_text); + assert_eq!(view.selections.ranges(cx), expected_selections); + + view.newline(&Newline, cx); + let (expected_text, expected_selections) = marked_text_ranges( + indoc! {" + aaaa + bX + ˇbbX + b + bX + ˇbbX + ˇb + cccc" + }, + false, + ); + assert_eq!(view.text(cx), expected_text); + assert_eq!(view.selections.ranges(cx), expected_selections); + }); +} + +#[gpui::test] +fn test_refresh_selections(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); + let mut excerpt1_id = None; + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + excerpt1_id = multibuffer + .push_excerpts( + buffer.clone(), + [ + ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 4), + primary: None, + }, + ExcerptRange { + context: Point::new(1, 0)..Point::new(2, 4), + primary: None, + }, + ], + cx, + ) + .into_iter() + .next(); + assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc"); + multibuffer + }); + + let editor = cx + .add_window(|cx| { + let mut editor = build_editor(multibuffer.clone(), cx); + let snapshot = editor.snapshot(cx); + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(1, 3)..Point::new(1, 3)]) + }); + editor.begin_selection(Point::new(2, 1).to_display_point(&snapshot), true, 1, cx); + assert_eq!( + editor.selections.ranges(cx), + [ + Point::new(1, 3)..Point::new(1, 3), + Point::new(2, 1)..Point::new(2, 1), + ] + ); + editor + }) + .root(cx); + + // Refreshing selections is a no-op when excerpts haven't changed. + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.refresh()); + assert_eq!( + editor.selections.ranges(cx), + [ + Point::new(1, 3)..Point::new(1, 3), + Point::new(2, 1)..Point::new(2, 1), + ] + ); + }); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx); + }); + editor.update(cx, |editor, cx| { + // Removing an excerpt causes the first selection to become degenerate. + assert_eq!( + editor.selections.ranges(cx), + [ + Point::new(0, 0)..Point::new(0, 0), + Point::new(0, 1)..Point::new(0, 1) + ] + ); + + // Refreshing selections will relocate the first selection to the original buffer + // location. + editor.change_selections(None, cx, |s| s.refresh()); + assert_eq!( + editor.selections.ranges(cx), + [ + Point::new(0, 1)..Point::new(0, 1), + Point::new(0, 3)..Point::new(0, 3) + ] + ); + assert!(editor.selections.pending_anchor().is_some()); + }); +} + +#[gpui::test] +fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); + let mut excerpt1_id = None; + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + excerpt1_id = multibuffer + .push_excerpts( + buffer.clone(), + [ + ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 4), + primary: None, + }, + ExcerptRange { + context: Point::new(1, 0)..Point::new(2, 4), + primary: None, + }, + ], + cx, + ) + .into_iter() + .next(); + assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc"); + multibuffer + }); + + let editor = cx + .add_window(|cx| { + let mut editor = build_editor(multibuffer.clone(), cx); + let snapshot = editor.snapshot(cx); + editor.begin_selection(Point::new(1, 3).to_display_point(&snapshot), false, 1, cx); + assert_eq!( + editor.selections.ranges(cx), + [Point::new(1, 3)..Point::new(1, 3)] + ); + editor + }) + .root(cx); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx); + }); + editor.update(cx, |editor, cx| { + assert_eq!( + editor.selections.ranges(cx), + [Point::new(0, 0)..Point::new(0, 0)] + ); + + // Ensure we don't panic when selections are refreshed and that the pending selection is finalized. + editor.change_selections(None, cx, |s| s.refresh()); + assert_eq!( + editor.selections.ranges(cx), + [Point::new(0, 3)..Point::new(0, 3)] + ); + assert!(editor.selections.pending_anchor().is_some()); + }); +} + +#[gpui::test] +async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let language = Arc::new( + Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }, + BracketPair { + start: "/* ".to_string(), + end: " */".to_string(), + close: true, + newline: true, + }, + ], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_indents_query("") + .unwrap(), + ); + + let text = concat!( + "{ }\n", // + " x\n", // + " /* */\n", // + "x\n", // + "{{} }\n", // + ); + + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); + view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + .await; + + view.update(cx, |view, cx| { + view.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), + DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), + DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4), + ]) + }); + view.newline(&Newline, cx); + + assert_eq!( + view.buffer().read(cx).read(cx).text(), + concat!( + "{ \n", // Suppress rustfmt + "\n", // + "}\n", // + " x\n", // + " /* \n", // + " \n", // + " */\n", // + "x\n", // + "{{} \n", // + "}\n", // + ) + ); + }); +} + +#[gpui::test] +fn test_highlighted_ranges(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); + build_editor(buffer.clone(), cx) + }) + .root(cx); + + editor.update(cx, |editor, cx| { + struct Type1; + struct Type2; + + let buffer = editor.buffer.read(cx).snapshot(cx); + + let anchor_range = + |range: Range| buffer.anchor_after(range.start)..buffer.anchor_after(range.end); + + editor.highlight_background::( + vec![ + anchor_range(Point::new(2, 1)..Point::new(2, 3)), + anchor_range(Point::new(4, 2)..Point::new(4, 4)), + anchor_range(Point::new(6, 3)..Point::new(6, 5)), + anchor_range(Point::new(8, 4)..Point::new(8, 6)), + ], + |_| Color::red(), + cx, + ); + editor.highlight_background::( + vec![ + anchor_range(Point::new(3, 2)..Point::new(3, 5)), + anchor_range(Point::new(5, 3)..Point::new(5, 6)), + anchor_range(Point::new(7, 4)..Point::new(7, 7)), + anchor_range(Point::new(9, 5)..Point::new(9, 8)), + ], + |_| Color::green(), + cx, + ); + + let snapshot = editor.snapshot(cx); + let mut highlighted_ranges = editor.background_highlights_in_range( + anchor_range(Point::new(3, 4)..Point::new(7, 4)), + &snapshot, + theme::current(cx).as_ref(), + ); + // Enforce a consistent ordering based on color without relying on the ordering of the + // highlight's `TypeId` which is non-deterministic. + highlighted_ranges.sort_unstable_by_key(|(_, color)| *color); + assert_eq!( + highlighted_ranges, + &[ + ( + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5), + Color::green(), + ), + ( + DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6), + Color::green(), + ), + ( + DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4), + Color::red(), + ), + ( + DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), + Color::red(), + ), + ] + ); + assert_eq!( + editor.background_highlights_in_range( + anchor_range(Point::new(5, 6)..Point::new(6, 4)), + &snapshot, + theme::current(cx).as_ref(), + ), + &[( + DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), + Color::red(), + )] + ); + }); +} + +#[gpui::test] +async fn test_following(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let fs = FakeFs::new(cx.background()); + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + + let buffer = project.update(cx, |project, cx| { + let buffer = project + .create_buffer(&sample_text(16, 8, 'a'), None, cx) + .unwrap(); + cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)) + }); + let leader = cx + .add_window(|cx| build_editor(buffer.clone(), cx)) + .root(cx); + let follower = cx + .update(|cx| { + cx.add_window( + WindowOptions { + bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))), + ..Default::default() + }, + |cx| build_editor(buffer.clone(), cx), + ) + }) + .root(cx); + + let is_still_following = Rc::new(RefCell::new(true)); + let follower_edit_event_count = Rc::new(RefCell::new(0)); + let pending_update = Rc::new(RefCell::new(None)); + follower.update(cx, { + let update = pending_update.clone(); + let is_still_following = is_still_following.clone(); + let follower_edit_event_count = follower_edit_event_count.clone(); + |_, cx| { + cx.subscribe(&leader, move |_, leader, event, cx| { + leader + .read(cx) + .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); + }) + .detach(); + + cx.subscribe(&follower, move |_, _, event, cx| { + if Editor::should_unfollow_on_event(event, cx) { + *is_still_following.borrow_mut() = false; + } + if let Event::BufferEdited = event { + *follower_edit_event_count.borrow_mut() += 1; + } + }) + .detach(); + } + }); + + // Update the selections only + leader.update(cx, |leader, cx| { + leader.change_selections(None, cx, |s| s.select_ranges([1..1])); + }); + follower + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) + }) + .await + .unwrap(); + follower.read_with(cx, |follower, cx| { + assert_eq!(follower.selections.ranges(cx), vec![1..1]); + }); + assert_eq!(*is_still_following.borrow(), true); + assert_eq!(*follower_edit_event_count.borrow(), 0); + + // Update the scroll position only + leader.update(cx, |leader, cx| { + leader.set_scroll_position(vec2f(1.5, 3.5), cx); + }); + follower + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) + }) + .await + .unwrap(); + assert_eq!( + follower.update(cx, |follower, cx| follower.scroll_position(cx)), + vec2f(1.5, 3.5) + ); + assert_eq!(*is_still_following.borrow(), true); + assert_eq!(*follower_edit_event_count.borrow(), 0); + + // Update the selections and scroll position. The follower's scroll position is updated + // via autoscroll, not via the leader's exact scroll position. + leader.update(cx, |leader, cx| { + leader.change_selections(None, cx, |s| s.select_ranges([0..0])); + leader.request_autoscroll(Autoscroll::newest(), cx); + leader.set_scroll_position(vec2f(1.5, 3.5), cx); + }); + follower + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) + }) + .await + .unwrap(); + follower.update(cx, |follower, cx| { + assert_eq!(follower.scroll_position(cx), vec2f(1.5, 0.0)); + assert_eq!(follower.selections.ranges(cx), vec![0..0]); + }); + assert_eq!(*is_still_following.borrow(), true); + + // Creating a pending selection that precedes another selection + leader.update(cx, |leader, cx| { + leader.change_selections(None, cx, |s| s.select_ranges([1..1])); + leader.begin_selection(DisplayPoint::new(0, 0), true, 1, cx); + }); + follower + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) + }) + .await + .unwrap(); + follower.read_with(cx, |follower, cx| { + assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]); + }); + assert_eq!(*is_still_following.borrow(), true); + + // Extend the pending selection so that it surrounds another selection + leader.update(cx, |leader, cx| { + leader.extend_selection(DisplayPoint::new(0, 2), 1, cx); + }); + follower + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) + }) + .await + .unwrap(); + follower.read_with(cx, |follower, cx| { + assert_eq!(follower.selections.ranges(cx), vec![0..2]); + }); + + // Scrolling locally breaks the follow + follower.update(cx, |follower, cx| { + let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0); + follower.set_scroll_anchor( + ScrollAnchor { + anchor: top_anchor, + offset: vec2f(0.0, 0.5), + }, + cx, + ); + }); + assert_eq!(*is_still_following.borrow(), false); +} + +#[gpui::test] +async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let fs = FakeFs::new(cx.background()); + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .root(cx); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + let leader = pane.update(cx, |_, cx| { + let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); + cx.add_view(|cx| build_editor(multibuffer.clone(), cx)) + }); + + // Start following the editor when it has no excerpts. + let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx)); + let follower_1 = cx + .update(|cx| { + Editor::from_state_proto( + pane.clone(), + workspace.clone(), + ViewId { + creator: Default::default(), + id: 0, + }, + &mut state_message, + cx, + ) + }) + .unwrap() + .await + .unwrap(); + + let update_message = Rc::new(RefCell::new(None)); + follower_1.update(cx, { + let update = update_message.clone(); + |_, cx| { + cx.subscribe(&leader, move |_, leader, event, cx| { + leader + .read(cx) + .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); + }) + .detach(); + } + }); + + let (buffer_1, buffer_2) = project.update(cx, |project, cx| { + ( + project + .create_buffer("abc\ndef\nghi\njkl\n", None, cx) + .unwrap(), + project + .create_buffer("mno\npqr\nstu\nvwx\n", None, cx) + .unwrap(), + ) + }); + + // Insert some excerpts. + leader.update(cx, |leader, cx| { + leader.buffer.update(cx, |multibuffer, cx| { + let excerpt_ids = multibuffer.push_excerpts( + buffer_1.clone(), + [ + ExcerptRange { + context: 1..6, + primary: None, + }, + ExcerptRange { + context: 12..15, + primary: None, + }, + ExcerptRange { + context: 0..3, + primary: None, + }, + ], + cx, + ); + multibuffer.insert_excerpts_after( + excerpt_ids[0], + buffer_2.clone(), + [ + ExcerptRange { + context: 8..12, + primary: None, + }, + ExcerptRange { + context: 0..6, + primary: None, + }, + ], + cx, + ); + }); + }); + + // Apply the update of adding the excerpts. + follower_1 + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx) + }) + .await + .unwrap(); + assert_eq!( + follower_1.read_with(cx, |editor, cx| editor.text(cx)), + leader.read_with(cx, |editor, cx| editor.text(cx)) + ); + update_message.borrow_mut().take(); + + // Start following separately after it already has excerpts. + let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx)); + let follower_2 = cx + .update(|cx| { + Editor::from_state_proto( + pane.clone(), + workspace.clone(), + ViewId { + creator: Default::default(), + id: 0, + }, + &mut state_message, + cx, + ) + }) + .unwrap() + .await + .unwrap(); + assert_eq!( + follower_2.read_with(cx, |editor, cx| editor.text(cx)), + leader.read_with(cx, |editor, cx| editor.text(cx)) + ); + + // Remove some excerpts. + leader.update(cx, |leader, cx| { + leader.buffer.update(cx, |multibuffer, cx| { + let excerpt_ids = multibuffer.excerpt_ids(); + multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx); + multibuffer.remove_excerpts([excerpt_ids[0]], cx); + }); + }); + + // Apply the update of removing the excerpts. + follower_1 + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx) + }) + .await + .unwrap(); + follower_2 + .update(cx, |follower, cx| { + follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx) + }) + .await + .unwrap(); + update_message.borrow_mut().take(); + assert_eq!( + follower_1.read_with(cx, |editor, cx| editor.text(cx)), + leader.read_with(cx, |editor, cx| editor.text(cx)) + ); +} + +#[test] +fn test_combine_syntax_and_fuzzy_match_highlights() { + let string = "abcdefghijklmnop"; + let syntax_ranges = [ + ( + 0..3, + HighlightStyle { + color: Some(Color::red()), + ..Default::default() + }, + ), + ( + 4..8, + HighlightStyle { + color: Some(Color::green()), + ..Default::default() + }, + ), + ]; + let match_indices = [4, 6, 7, 8]; + assert_eq!( + combine_syntax_and_fuzzy_match_highlights( + string, + Default::default(), + syntax_ranges.into_iter(), + &match_indices, + ), + &[ + ( + 0..3, + HighlightStyle { + color: Some(Color::red()), + ..Default::default() + }, + ), + ( + 4..5, + HighlightStyle { + color: Some(Color::green()), + weight: Some(fonts::Weight::BOLD), + ..Default::default() + }, + ), + ( + 5..6, + HighlightStyle { + color: Some(Color::green()), + ..Default::default() + }, + ), + ( + 6..8, + HighlightStyle { + color: Some(Color::green()), + weight: Some(fonts::Weight::BOLD), + ..Default::default() + }, + ), + ( + 8..9, + HighlightStyle { + weight: Some(fonts::Weight::BOLD), + ..Default::default() + }, + ), + ] + ); +} + +#[gpui::test] +async fn go_to_prev_overlapping_diagnostic( + deterministic: Arc, + cx: &mut gpui::TestAppContext, +) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + let project = cx.update_editor(|editor, _| editor.project.clone().unwrap()); + + cx.set_state(indoc! {" + ˇfn func(abc def: i32) -> u32 { + } + "}); + + cx.update(|cx| { + project.update(cx, |project, cx| { + project + .update_diagnostics( + LanguageServerId(0), + lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path("/root/file").unwrap(), + version: None, + diagnostics: vec![ + lsp::Diagnostic { + range: lsp::Range::new( + lsp::Position::new(0, 11), + lsp::Position::new(0, 12), + ), + severity: Some(lsp::DiagnosticSeverity::ERROR), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new( + lsp::Position::new(0, 12), + lsp::Position::new(0, 15), + ), + severity: Some(lsp::DiagnosticSeverity::ERROR), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new( + lsp::Position::new(0, 25), + lsp::Position::new(0, 28), + ), + severity: Some(lsp::DiagnosticSeverity::ERROR), + ..Default::default() + }, + ], + }, + &[], + cx, + ) + .unwrap() + }); + }); + + deterministic.run_until_parked(); + + cx.update_editor(|editor, cx| { + editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); + }); + + cx.assert_editor_state(indoc! {" + fn func(abc def: i32) -> ˇu32 { + } + "}); + + cx.update_editor(|editor, cx| { + editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); + }); + + cx.assert_editor_state(indoc! {" + fn func(abc ˇdef: i32) -> u32 { + } + "}); + + cx.update_editor(|editor, cx| { + editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); + }); + + cx.assert_editor_state(indoc! {" + fn func(abcˇ def: i32) -> u32 { + } + "}); + + cx.update_editor(|editor, cx| { + editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); + }); + + cx.assert_editor_state(indoc! {" + fn func(abc def: i32) -> ˇu32 { + } + "}); +} + +#[gpui::test] +async fn go_to_hunk(deterministic: Arc, cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let diff_base = r#" + use some::mod; + + const A: u32 = 42; + + fn main() { + println!("hello"); + + println!("world"); + } + "# + .unindent(); + + // Edits are modified, removed, modified, added + cx.set_state( + &r#" + use some::modified; + + ˇ + fn main() { + println!("hello there"); + + println!("around the"); + println!("world"); + } + "# + .unindent(), + ); + + cx.set_diff_base(Some(&diff_base)); + deterministic.run_until_parked(); + + cx.update_editor(|editor, cx| { + //Wrap around the bottom of the buffer + for _ in 0..3 { + editor.go_to_hunk(&GoToHunk, cx); + } + }); + + cx.assert_editor_state( + &r#" + ˇuse some::modified; + + + fn main() { + println!("hello there"); + + println!("around the"); + println!("world"); + } + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| { + //Wrap around the top of the buffer + for _ in 0..2 { + editor.go_to_prev_hunk(&GoToPrevHunk, cx); + } + }); + + cx.assert_editor_state( + &r#" + use some::modified; + + + fn main() { + ˇ println!("hello there"); + + println!("around the"); + println!("world"); + } + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| { + editor.go_to_prev_hunk(&GoToPrevHunk, cx); + }); + + cx.assert_editor_state( + &r#" + use some::modified; + + ˇ + fn main() { + println!("hello there"); + + println!("around the"); + println!("world"); + } + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| { + for _ in 0..3 { + editor.go_to_prev_hunk(&GoToPrevHunk, cx); + } + }); + + cx.assert_editor_state( + &r#" + use some::modified; + + + fn main() { + ˇ println!("hello there"); + + println!("around the"); + println!("world"); + } + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| { + editor.fold(&Fold, cx); + + //Make sure that the fold only gets one hunk + for _ in 0..4 { + editor.go_to_hunk(&GoToHunk, cx); + } + }); + + cx.assert_editor_state( + &r#" + ˇuse some::modified; + + + fn main() { + println!("hello there"); + + println!("around the"); + println!("world"); + } + "# + .unindent(), + ); +} + +#[test] +fn test_split_words() { + fn split<'a>(text: &'a str) -> Vec<&'a str> { + split_words(text).collect() + } + + assert_eq!(split("HelloWorld"), &["Hello", "World"]); + assert_eq!(split("hello_world"), &["hello_", "world"]); + assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]); + assert_eq!(split("Hello_World"), &["Hello_", "World"]); + assert_eq!(split("helloWOrld"), &["hello", "WOrld"]); + assert_eq!(split("helloworld"), &["helloworld"]); +} + +#[gpui::test] +async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await; + let mut assert = |before, after| { + let _state_context = cx.set_state(before); + cx.update_editor(|editor, cx| { + editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, cx) + }); + cx.assert_editor_state(after); + }; + + // Outside bracket jumps to outside of matching bracket + assert("console.logˇ(var);", "console.log(var)ˇ;"); + assert("console.log(var)ˇ;", "console.logˇ(var);"); + + // Inside bracket jumps to inside of matching bracket + assert("console.log(ˇvar);", "console.log(varˇ);"); + assert("console.log(varˇ);", "console.log(ˇvar);"); + + // When outside a bracket and inside, favor jumping to the inside bracket + assert( + "console.log('foo', [1, 2, 3]ˇ);", + "console.log(ˇ'foo', [1, 2, 3]);", + ); + assert( + "console.log(ˇ'foo', [1, 2, 3]);", + "console.log('foo', [1, 2, 3]ˇ);", + ); + + // Bias forward if two options are equally likely + assert( + "let result = curried_fun()ˇ();", + "let result = curried_fun()()ˇ;", + ); + + // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller + assert( + indoc! {" + function test() { + console.log('test')ˇ + }"}, + indoc! {" + function test() { + console.logˇ('test') + }"}, + ); +} + +#[gpui::test(iterations = 10)] +async fn test_copilot(deterministic: Arc, cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let (copilot, copilot_lsp) = Copilot::fake(cx); + cx.update(|cx| cx.set_global(copilot)); + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; + + // When inserting, ensure autocompletion is favored over Copilot suggestions. + cx.set_state(indoc! {" + oneˇ + two + three + "}); + cx.simulate_keystroke("."); + let _ = handle_completion_request( + &mut cx, + indoc! {" + one.|<> + two + three + "}, + vec!["completion_a", "completion_b"], + ); + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: "one.copilot1".into(), + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), + ..Default::default() + }], + vec![], + ); + deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + cx.update_editor(|editor, cx| { + assert!(editor.context_menu_visible()); + assert!(!editor.has_active_copilot_suggestion(cx)); + + // Confirming a completion inserts it and hides the context menu, without showing + // the copilot suggestion afterwards. + editor + .confirm_completion(&Default::default(), cx) + .unwrap() + .detach(); + assert!(!editor.context_menu_visible()); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n"); + assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n"); + }); + + // Ensure Copilot suggestions are shown right away if no autocompletion is available. + cx.set_state(indoc! {" + oneˇ + two + three + "}); + cx.simulate_keystroke("."); + let _ = handle_completion_request( + &mut cx, + indoc! {" + one.|<> + two + three + "}, + vec![], + ); + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: "one.copilot1".into(), + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), + ..Default::default() + }], + vec![], + ); + deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + cx.update_editor(|editor, cx| { + assert!(!editor.context_menu_visible()); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.\ntwo\nthree\n"); + }); + + // Reset editor, and ensure autocompletion is still favored over Copilot suggestions. + cx.set_state(indoc! {" + oneˇ + two + three + "}); + cx.simulate_keystroke("."); + let _ = handle_completion_request( + &mut cx, + indoc! {" + one.|<> + two + three + "}, + vec!["completion_a", "completion_b"], + ); + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: "one.copilot1".into(), + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), + ..Default::default() + }], + vec![], + ); + deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + cx.update_editor(|editor, cx| { + assert!(editor.context_menu_visible()); + assert!(!editor.has_active_copilot_suggestion(cx)); + + // When hiding the context menu, the Copilot suggestion becomes visible. + editor.hide_context_menu(cx); + assert!(!editor.context_menu_visible()); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.\ntwo\nthree\n"); + }); + + // Ensure existing completion is interpolated when inserting again. + cx.simulate_keystroke("c"); + deterministic.run_until_parked(); + cx.update_editor(|editor, cx| { + assert!(!editor.context_menu_visible()); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); + }); + + // After debouncing, new Copilot completions should be requested. + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: "one.copilot2".into(), + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)), + ..Default::default() + }], + vec![], + ); + deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + cx.update_editor(|editor, cx| { + assert!(!editor.context_menu_visible()); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); + + // Canceling should remove the active Copilot suggestion. + editor.cancel(&Default::default(), cx); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); + + // After canceling, tabbing shouldn't insert the previously shown suggestion. + editor.tab(&Default::default(), cx); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n"); + + // When undoing the previously active suggestion is shown again. + editor.undo(&Default::default(), cx); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); + }); + + // If an edit occurs outside of this editor, the suggestion is still correctly interpolated. + cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx)); + cx.update_editor(|editor, cx| { + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); + + // Tabbing when there is an active suggestion inserts it. + editor.tab(&Default::default(), cx); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n"); + + // When undoing the previously active suggestion is shown again. + editor.undo(&Default::default(), cx); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); + + // Hide suggestion. + editor.cancel(&Default::default(), cx); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); + }); + + // If an edit occurs outside of this editor but no suggestion is being shown, + // we won't make it visible. + cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx)); + cx.update_editor(|editor, cx| { + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n"); + }); + + // Reset the editor to verify how suggestions behave when tabbing on leading indentation. + cx.update_editor(|editor, cx| { + editor.set_text("fn foo() {\n \n}", cx); + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(1, 2)..Point::new(1, 2)]) + }); + }); + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: " let x = 4;".into(), + range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)), + ..Default::default() + }], + vec![], + ); + + cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx)); + deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + cx.update_editor(|editor, cx| { + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); + assert_eq!(editor.text(cx), "fn foo() {\n \n}"); + + // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion. + editor.tab(&Default::default(), cx); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.text(cx), "fn foo() {\n \n}"); + assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); + + // Tabbing again accepts the suggestion. + editor.tab(&Default::default(), cx); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}"); + assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); + }); +} + +#[gpui::test] +async fn test_copilot_completion_invalidation( + deterministic: Arc, + cx: &mut gpui::TestAppContext, +) { + init_test(cx, |_| {}); + + let (copilot, copilot_lsp) = Copilot::fake(cx); + cx.update(|cx| cx.set_global(copilot)); + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + one + twˇ + three + "}); + + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: "two.foo()".into(), + range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)), + ..Default::default() + }], + vec![], + ); + cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx)); + deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + cx.update_editor(|editor, cx| { + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); + assert_eq!(editor.text(cx), "one\ntw\nthree\n"); + + editor.backspace(&Default::default(), cx); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); + assert_eq!(editor.text(cx), "one\nt\nthree\n"); + + editor.backspace(&Default::default(), cx); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); + assert_eq!(editor.text(cx), "one\n\nthree\n"); + + // Deleting across the original suggestion range invalidates it. + editor.backspace(&Default::default(), cx); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one\nthree\n"); + assert_eq!(editor.text(cx), "one\nthree\n"); + + // Undoing the deletion restores the suggestion. + editor.undo(&Default::default(), cx); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); + assert_eq!(editor.text(cx), "one\n\nthree\n"); + }); +} + +#[gpui::test] +async fn test_copilot_multibuffer( + deterministic: Arc, + cx: &mut gpui::TestAppContext, +) { + init_test(cx, |_| {}); + + let (copilot, copilot_lsp) = Copilot::fake(cx); + cx.update(|cx| cx.set_global(copilot)); + + let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n")); + let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "c = 3\nd = 4\n")); + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(2, 0), + primary: None, + }], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(2, 0), + primary: None, + }], + cx, + ); + multibuffer + }); + let editor = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); + + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: "b = 2 + a".into(), + range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)), + ..Default::default() + }], + vec![], + ); + editor.update(cx, |editor, cx| { + // Ensure copilot suggestions are shown for the first excerpt. + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(1, 5)..Point::new(1, 5)]) + }); + editor.next_copilot_suggestion(&Default::default(), cx); + }); + deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + editor.update(cx, |editor, cx| { + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!( + editor.display_text(cx), + "\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n" + ); + assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n"); + }); + + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: "d = 4 + c".into(), + range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)), + ..Default::default() + }], + vec![], + ); + editor.update(cx, |editor, cx| { + // Move to another excerpt, ensuring the suggestion gets cleared. + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(4, 5)..Point::new(4, 5)]) + }); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!( + editor.display_text(cx), + "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n" + ); + assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n"); + + // Type a character, ensuring we don't even try to interpolate the previous suggestion. + editor.handle_input(" ", cx); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!( + editor.display_text(cx), + "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n" + ); + assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n"); + }); + + // Ensure the new suggestion is displayed when the debounce timeout expires. + deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + editor.update(cx, |editor, cx| { + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!( + editor.display_text(cx), + "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n" + ); + assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n"); + }); +} + +#[gpui::test] +async fn test_copilot_disabled_globs( + deterministic: Arc, + cx: &mut gpui::TestAppContext, +) { + init_test(cx, |settings| { + settings + .copilot + .get_or_insert(Default::default()) + .disabled_globs = Some(vec![".env*".to_string()]); + }); + + let (copilot, copilot_lsp) = Copilot::fake(cx); + cx.update(|cx| cx.set_global(copilot)); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/test", + json!({ + ".env": "SECRET=something\n", + "README.md": "hello\n" + }), + ) + .await; + let project = Project::test(fs, ["/test".as_ref()], cx).await; + + let private_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/test/.env", cx) + }) + .await + .unwrap(); + let public_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/test/README.md", cx) + }) + .await + .unwrap(); + + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + private_buffer.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 0), + primary: None, + }], + cx, + ); + multibuffer.push_excerpts( + public_buffer.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 0), + primary: None, + }], + cx, + ); + multibuffer + }); + let editor = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); + + let mut copilot_requests = copilot_lsp + .handle_request::(move |_params, _cx| async move { + Ok(copilot::request::GetCompletionsResult { + completions: vec![copilot::request::Completion { + text: "next line".into(), + range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)), + ..Default::default() + }], + }) + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |selections| { + selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)]) + }); + editor.next_copilot_suggestion(&Default::default(), cx); + }); + + deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + assert!(copilot_requests.try_next().is_err()); + + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(2, 0)..Point::new(2, 0)]) + }); + editor.next_copilot_suggestion(&Default::default(), cx); + }); + + deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + assert!(copilot_requests.try_next().is_ok()); +} + +#[gpui::test] +async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + brackets: BracketPairConfig { + pairs: vec![BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }], + disabled_scopes_by_bracket_ix: Vec::new(), + }, + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { + first_trigger_character: "{".to_string(), + more_trigger_character: None, + }), + ..Default::default() + }, + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/a", + json!({ + "main.rs": "fn main() { let a = 5; }", + "other.rs": "// Test file", + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .root(cx); + let worktree_id = workspace.update(cx, |workspace, cx| { + workspace.project().read_with(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }); + + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + cx.foreground().run_until_parked(); + cx.foreground().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + let editor_handle = workspace + .update(cx, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + fake_server.handle_request::(|params, _| async move { + assert_eq!( + params.text_document_position.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 21), + ); + + Ok(Some(vec![lsp::TextEdit { + new_text: "]".to_string(), + range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)), + }])) + }); + + editor_handle.update(cx, |editor, cx| { + cx.focus(&editor_handle); + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(0, 21)..Point::new(0, 20)]) + }); + editor.handle_input("{", cx); + }); + + cx.foreground().run_until_parked(); + + buffer.read_with(cx, |buffer, _| { + assert_eq!( + buffer.text(), + "fn main() { let a = {5}; }", + "No extra braces from on type formatting should appear in the buffer" + ) + }); +} + +#[gpui::test] +async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let language_name: Arc = "Rust".into(); + let mut language = Language::new( + LanguageConfig { + name: Arc::clone(&language_name), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + + let server_restarts = Arc::new(AtomicUsize::new(0)); + let closure_restarts = Arc::clone(&server_restarts); + let language_server_name = "test language server"; + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + name: language_server_name, + initialization_options: Some(json!({ + "testOptionValue": true + })), + initializer: Some(Box::new(move |fake_server| { + let task_restarts = Arc::clone(&closure_restarts); + fake_server.handle_request::(move |_, _| { + task_restarts.fetch_add(1, atomic::Ordering::Release); + futures::future::ready(Ok(())) + }); + })), + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/a", + json!({ + "main.rs": "fn main() { let a = 5; }", + "other.rs": "// Test file", + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let _window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let _buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + let _fake_server = fake_servers.next().await.unwrap(); + update_test_language_settings(cx, |language_settings| { + language_settings.languages.insert( + Arc::clone(&language_name), + LanguageSettingsContent { + tab_size: NonZeroU32::new(8), + ..Default::default() + }, + ); + }); + cx.foreground().run_until_parked(); + assert_eq!( + server_restarts.load(atomic::Ordering::Acquire), + 0, + "Should not restart LSP server on an unrelated change" + ); + + update_test_project_settings(cx, |project_settings| { + project_settings.lsp.insert( + "Some other server name".into(), + LspSettings { + initialization_options: Some(json!({ + "some other init value": false + })), + }, + ); + }); + cx.foreground().run_until_parked(); + assert_eq!( + server_restarts.load(atomic::Ordering::Acquire), + 0, + "Should not restart LSP server on an unrelated LSP settings change" + ); + + update_test_project_settings(cx, |project_settings| { + project_settings.lsp.insert( + language_server_name.into(), + LspSettings { + initialization_options: Some(json!({ + "anotherInitValue": false + })), + }, + ); + }); + cx.foreground().run_until_parked(); + assert_eq!( + server_restarts.load(atomic::Ordering::Acquire), + 1, + "Should restart LSP server on a related LSP settings change" + ); + + update_test_project_settings(cx, |project_settings| { + project_settings.lsp.insert( + language_server_name.into(), + LspSettings { + initialization_options: Some(json!({ + "anotherInitValue": false + })), + }, + ); + }); + cx.foreground().run_until_parked(); + assert_eq!( + server_restarts.load(atomic::Ordering::Acquire), + 1, + "Should not restart LSP server on a related LSP settings change that is the same" + ); + + update_test_project_settings(cx, |project_settings| { + project_settings.lsp.insert( + language_server_name.into(), + LspSettings { + initialization_options: None, + }, + ); + }); + cx.foreground().run_until_parked(); + assert_eq!( + server_restarts.load(atomic::Ordering::Acquire), + 2, + "Should restart LSP server on another related LSP settings change" + ); +} + +#[gpui::test] +async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + resolve_provider: Some(true), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"}); + cx.simulate_keystroke("."); + let completion_item = lsp::CompletionItem { + label: "some".into(), + kind: Some(lsp::CompletionItemKind::SNIPPET), + detail: Some("Wrap the expression in an `Option::Some`".to_string()), + documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: "```rust\nSome(2)\n```".to_string(), + })), + deprecated: Some(false), + sort_text: Some("fffffff2".to_string()), + filter_text: Some("some".to_string()), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 22, + }, + end: lsp::Position { + line: 0, + character: 22, + }, + }, + new_text: "Some(2)".to_string(), + })), + additional_text_edits: Some(vec![lsp::TextEdit { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 20, + }, + end: lsp::Position { + line: 0, + character: 22, + }, + }, + new_text: "".to_string(), + }]), + ..Default::default() + }; + + let closure_completion_item = completion_item.clone(); + let mut request = cx.handle_request::(move |_, _, _| { + let task_completion_item = closure_completion_item.clone(); + async move { + Ok(Some(lsp::CompletionResponse::Array(vec![ + task_completion_item, + ]))) + } + }); + + request.next().await; + + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + let apply_additional_edits = cx.update_editor(|editor, cx| { + editor + .confirm_completion(&ConfirmCompletion::default(), cx) + .unwrap() + }); + cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"}); + + cx.handle_request::(move |_, _, _| { + let task_completion_item = completion_item.clone(); + async move { Ok(task_completion_item) } + }) + .next() + .await + .unwrap(); + apply_additional_edits.await.unwrap(); + cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"}); +} + +#[gpui::test] +async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new( + Language::new( + LanguageConfig { + path_suffixes: vec!["jsx".into()], + overrides: [( + "element".into(), + LanguageConfigOverride { + word_characters: Override::Set(['-'].into_iter().collect()), + ..Default::default() + }, + )] + .into_iter() + .collect(), + ..Default::default() + }, + Some(tree_sitter_typescript::language_tsx()), + ) + .with_override_query("(jsx_self_closing_element) @element") + .unwrap(), + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; + + cx.lsp + .handle_request::(move |_, _| async move { + Ok(Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + label: "bg-blue".into(), + ..Default::default() + }, + lsp::CompletionItem { + label: "bg-red".into(), + ..Default::default() + }, + lsp::CompletionItem { + label: "bg-yellow".into(), + ..Default::default() + }, + ]))) + }); + + cx.set_state(r#"

"#); + + // Trigger completion when typing a dash, because the dash is an extra + // word character in the 'element' scope, which contains the cursor. + cx.simulate_keystroke("-"); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, _| { + if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { + assert_eq!( + menu.matches.iter().map(|m| &m.string).collect::>(), + &["bg-red", "bg-blue", "bg-yellow"] + ); + } else { + panic!("expected completion menu to be open"); + } + }); + + cx.simulate_keystroke("l"); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, _| { + if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { + assert_eq!( + menu.matches.iter().map(|m| &m.string).collect::>(), + &["bg-blue", "bg-yellow"] + ); + } else { + panic!("expected completion menu to be open"); + } + }); + + // When filtering completions, consider the character after the '-' to + // be the start of a subword. + cx.set_state(r#"

"#); + cx.simulate_keystroke("l"); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, _| { + if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { + assert_eq!( + menu.matches.iter().map(|m| &m.string).collect::>(), + &["bg-yellow"] + ); + } else { + panic!("expected completion menu to be open"); + } + }); +} + +#[gpui::test] +async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.formatter = Some(language_settings::Formatter::Prettier) + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + prettier_parser_name: Some("test_parser".to_string()), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + + let test_plugin = "test_plugin"; + let _ = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + prettier_plugins: vec![test_plugin], + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.background()); + fs.insert_file("/file.rs", Default::default()).await; + + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX; + project.update(cx, |project, _| { + project.languages().add(Arc::new(language)); + }); + let buffer = project + .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) + .await + .unwrap(); + + let buffer_text = "one\ntwo\nthree\n"; + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); + editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx)); + + let format = editor.update(cx, |editor, cx| { + editor.perform_format(project.clone(), FormatTrigger::Manual, cx) + }); + format.await.unwrap(); + assert_eq!( + editor.read_with(cx, |editor, cx| editor.text(cx)), + buffer_text.to_string() + prettier_format_suffix, + "Test prettier formatting was not applied to the original buffer text", + ); + + update_test_language_settings(cx, |settings| { + settings.defaults.formatter = Some(language_settings::Formatter::Auto) + }); + let format = editor.update(cx, |editor, cx| { + editor.perform_format(project.clone(), FormatTrigger::Manual, cx) + }); + format.await.unwrap(); + assert_eq!( + editor.read_with(cx, |editor, cx| editor.text(cx)), + buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix, + "Autoformatting (via test prettier) was not applied to the original buffer text", + ); +} + +fn empty_range(row: usize, column: usize) -> Range { + let point = DisplayPoint::new(row as u32, column as u32); + point..point +} + +fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewContext) { + let (text, ranges) = marked_text_ranges(marked_text, true); + assert_eq!(view.text(cx), text); + assert_eq!( + view.selections.ranges(cx), + ranges, + "Assert selections are {}", + marked_text + ); +} + +/// Handle completion request passing a marked string specifying where the completion +/// should be triggered from using '|' character, what range should be replaced, and what completions +/// should be returned using '<' and '>' to delimit the range +pub fn handle_completion_request<'a>( + cx: &mut EditorLspTestContext<'a>, + marked_string: &str, + completions: Vec<&'static str>, +) -> impl Future { + let complete_from_marker: TextRangeMarker = '|'.into(); + let replace_range_marker: TextRangeMarker = ('<', '>').into(); + let (_, mut marked_ranges) = marked_text_ranges_by( + marked_string, + vec![complete_from_marker.clone(), replace_range_marker.clone()], + ); + + let complete_from_position = + cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start); + let replace_range = + cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone()); + + let mut request = cx.handle_request::(move |url, params, _| { + let completions = completions.clone(); + async move { + assert_eq!(params.text_document_position.text_document.uri, url.clone()); + assert_eq!( + params.text_document_position.position, + complete_from_position + ); + Ok(Some(lsp::CompletionResponse::Array( + completions + .iter() + .map(|completion_text| lsp::CompletionItem { + label: completion_text.to_string(), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: replace_range, + new_text: completion_text.to_string(), + })), + ..Default::default() + }) + .collect(), + ))) + } + }); + + async move { + request.next().await; + } +} + +fn handle_resolve_completion_request<'a>( + cx: &mut EditorLspTestContext<'a>, + edits: Option>, +) -> impl Future { + let edits = edits.map(|edits| { + edits + .iter() + .map(|(marked_string, new_text)| { + let (_, marked_ranges) = marked_text_ranges(marked_string, false); + let replace_range = cx.to_lsp_range(marked_ranges[0].clone()); + lsp::TextEdit::new(replace_range, new_text.to_string()) + }) + .collect::>() + }); + + let mut request = + cx.handle_request::(move |_, _, _| { + let edits = edits.clone(); + async move { + Ok(lsp::CompletionItem { + additional_text_edits: edits, + ..Default::default() + }) + } + }); + + async move { + request.next().await; + } +} + +fn handle_copilot_completion_request( + lsp: &lsp::FakeLanguageServer, + completions: Vec, + completions_cycling: Vec, +) { + lsp.handle_request::(move |_params, _cx| { + let completions = completions.clone(); + async move { + Ok(copilot::request::GetCompletionsResult { + completions: completions.clone(), + }) + } + }); + lsp.handle_request::(move |_params, _cx| { + let completions_cycling = completions_cycling.clone(); + async move { + Ok(copilot::request::GetCompletionsResult { + completions: completions_cycling.clone(), + }) + } + }); +} + +pub(crate) fn update_test_language_settings( + cx: &mut TestAppContext, + f: impl Fn(&mut AllLanguageSettingsContent), +) { + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, f); + }); + }); +} + +pub(crate) fn update_test_project_settings( + cx: &mut TestAppContext, + f: impl Fn(&mut ProjectSettings), +) { + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, f); + }); + }); +} + +pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) { + cx.foreground().forbid_parking(); + + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + theme::init((), cx); + client::init_settings(cx); + language::init(cx); + Project::init_settings(cx); + workspace::init_settings(cx); + crate::init(cx); + }); + + update_test_language_settings(cx, f); +} diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs new file mode 100644 index 0000000000..7b1155890f --- /dev/null +++ b/crates/editor2/src/element.rs @@ -0,0 +1,3478 @@ +use super::{ + display_map::{BlockContext, ToDisplayPoint}, + Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, SelectPhase, SoftWrap, ToPoint, + MAX_LINE_LEN, +}; +use crate::{ + display_map::{BlockStyle, DisplaySnapshot, FoldStatus, HighlightedChunk, TransformBlock}, + editor_settings::ShowScrollbar, + git::{diff_hunk_to_display, DisplayDiffHunk}, + hover_popover::{ + hide_hover, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, + MIN_POPOVER_LINE_HEIGHT, + }, + link_go_to_definition::{ + go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, + update_inlay_link_and_hover_points, GoToDefinitionTrigger, + }, + mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt, +}; +use collections::{BTreeMap, HashMap}; +use git::diff::DiffHunkStatus; +use gpui::{ + color::Color, + elements::*, + fonts::TextStyle, + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + PathBuilder, + }, + json::{self, ToJson}, + platform::{CursorStyle, Modifiers, MouseButton, MouseButtonEvent, MouseMovedEvent}, + text_layout::{self, Line, RunStyle, TextLayoutCache}, + AnyElement, Axis, CursorRegion, Element, EventContext, FontCache, MouseRegion, Quad, + SizeConstraint, ViewContext, WindowContext, +}; +use itertools::Itertools; +use json::json; +use language::{ + language_settings::ShowWhitespaceSetting, Bias, CursorShape, OffsetUtf16, Selection, +}; +use project::{ + project_settings::{GitGutterSetting, ProjectSettings}, + ProjectPath, +}; +use smallvec::SmallVec; +use std::{ + borrow::Cow, + cmp::{self, Ordering}, + fmt::Write, + iter, + ops::Range, + sync::Arc, +}; +use text::Point; +use theme::SelectionStyle; +use workspace::item::Item; + +enum FoldMarkers {} + +struct SelectionLayout { + head: DisplayPoint, + cursor_shape: CursorShape, + is_newest: bool, + is_local: bool, + range: Range, + active_rows: Range, +} + +impl SelectionLayout { + fn new( + selection: Selection, + line_mode: bool, + cursor_shape: CursorShape, + map: &DisplaySnapshot, + is_newest: bool, + is_local: bool, + ) -> Self { + let point_selection = selection.map(|p| p.to_point(&map.buffer_snapshot)); + let display_selection = point_selection.map(|p| p.to_display_point(map)); + let mut range = display_selection.range(); + let mut head = display_selection.head(); + let mut active_rows = map.prev_line_boundary(point_selection.start).1.row() + ..map.next_line_boundary(point_selection.end).1.row(); + + // vim visual line mode + if line_mode { + let point_range = map.expand_to_line(point_selection.range()); + range = point_range.start.to_display_point(map)..point_range.end.to_display_point(map); + } + + // any vim visual mode (including line mode) + if cursor_shape == CursorShape::Block && !range.is_empty() && !selection.reversed { + if head.column() > 0 { + head = map.clip_point(DisplayPoint::new(head.row(), head.column() - 1), Bias::Left) + } else if head.row() > 0 && head != map.max_point() { + head = map.clip_point( + DisplayPoint::new(head.row() - 1, map.line_len(head.row() - 1)), + Bias::Left, + ); + // updating range.end is a no-op unless you're cursor is + // on the newline containing a multi-buffer divider + // in which case the clip_point may have moved the head up + // an additional row. + range.end = DisplayPoint::new(head.row() + 1, 0); + active_rows.end = head.row(); + } + } + + Self { + head, + cursor_shape, + is_newest, + is_local, + range, + active_rows, + } + } +} + +pub struct EditorElement { + style: Arc, +} + +impl EditorElement { + pub fn new(style: EditorStyle) -> Self { + Self { + style: Arc::new(style), + } + } + + fn attach_mouse_handlers( + position_map: &Arc, + has_popovers: bool, + visible_bounds: RectF, + text_bounds: RectF, + gutter_bounds: RectF, + bounds: RectF, + cx: &mut ViewContext, + ) { + enum EditorElementMouseHandlers {} + let view_id = cx.view_id(); + cx.scene().push_mouse_region( + MouseRegion::new::(view_id, view_id, visible_bounds) + .on_down(MouseButton::Left, { + let position_map = position_map.clone(); + move |event, editor, cx| { + if !Self::mouse_down( + editor, + event.platform_event, + position_map.as_ref(), + text_bounds, + gutter_bounds, + cx, + ) { + cx.propagate_event(); + } + } + }) + .on_down(MouseButton::Right, { + let position_map = position_map.clone(); + move |event, editor, cx| { + if !Self::mouse_right_down( + editor, + event.position, + position_map.as_ref(), + text_bounds, + cx, + ) { + cx.propagate_event(); + } + } + }) + .on_up(MouseButton::Left, { + let position_map = position_map.clone(); + move |event, editor, cx| { + if !Self::mouse_up( + editor, + event.position, + event.cmd, + event.shift, + event.alt, + position_map.as_ref(), + text_bounds, + cx, + ) { + cx.propagate_event() + } + } + }) + .on_drag(MouseButton::Left, { + let position_map = position_map.clone(); + move |event, editor, cx| { + if event.end { + return; + } + + if !Self::mouse_dragged( + editor, + event.platform_event, + position_map.as_ref(), + text_bounds, + cx, + ) { + cx.propagate_event() + } + } + }) + .on_move({ + let position_map = position_map.clone(); + move |event, editor, cx| { + if !Self::mouse_moved( + editor, + event.platform_event, + &position_map, + text_bounds, + cx, + ) { + cx.propagate_event() + } + } + }) + .on_move_out(move |_, editor: &mut Editor, cx| { + if has_popovers { + hide_hover(editor, cx); + } + }) + .on_scroll({ + let position_map = position_map.clone(); + move |event, editor, cx| { + if !Self::scroll( + editor, + event.position, + *event.delta.raw(), + event.delta.precise(), + &position_map, + bounds, + cx, + ) { + cx.propagate_event() + } + } + }), + ); + + enum GutterHandlers {} + let view_id = cx.view_id(); + let region_id = cx.view_id() + 1; + cx.scene().push_mouse_region( + MouseRegion::new::(view_id, region_id, gutter_bounds).on_hover( + |hover, editor: &mut Editor, cx| { + editor.gutter_hover( + &GutterHover { + hovered: hover.started, + }, + cx, + ); + }, + ), + ) + } + + fn mouse_down( + editor: &mut Editor, + MouseButtonEvent { + position, + modifiers: + Modifiers { + shift, + ctrl, + alt, + cmd, + .. + }, + mut click_count, + .. + }: MouseButtonEvent, + position_map: &PositionMap, + text_bounds: RectF, + gutter_bounds: RectF, + cx: &mut EventContext, + ) -> bool { + if gutter_bounds.contains_point(position) { + click_count = 3; // Simulate triple-click when clicking the gutter to select lines + } else if !text_bounds.contains_point(position) { + return false; + } + + let point_for_position = position_map.point_for_position(text_bounds, position); + let position = point_for_position.previous_valid; + if shift && alt { + editor.select( + SelectPhase::BeginColumnar { + position, + goal_column: point_for_position.exact_unclipped.column(), + }, + cx, + ); + } else if shift && !ctrl && !alt && !cmd { + editor.select( + SelectPhase::Extend { + position, + click_count, + }, + cx, + ); + } else { + editor.select( + SelectPhase::Begin { + position, + add: alt, + click_count, + }, + cx, + ); + } + + true + } + + fn mouse_right_down( + editor: &mut Editor, + position: Vector2F, + position_map: &PositionMap, + text_bounds: RectF, + cx: &mut EventContext, + ) -> bool { + if !text_bounds.contains_point(position) { + return false; + } + let point_for_position = position_map.point_for_position(text_bounds, position); + mouse_context_menu::deploy_context_menu( + editor, + position, + point_for_position.previous_valid, + cx, + ); + true + } + + fn mouse_up( + editor: &mut Editor, + position: Vector2F, + cmd: bool, + shift: bool, + alt: bool, + position_map: &PositionMap, + text_bounds: RectF, + cx: &mut EventContext, + ) -> bool { + let end_selection = editor.has_pending_selection(); + let pending_nonempty_selections = editor.has_pending_nonempty_selection(); + + if end_selection { + editor.select(SelectPhase::End, cx); + } + + if !pending_nonempty_selections && cmd && text_bounds.contains_point(position) { + let point = position_map.point_for_position(text_bounds, position); + let could_be_inlay = point.as_valid().is_none(); + if shift || could_be_inlay { + go_to_fetched_type_definition(editor, point, alt, cx); + } else { + go_to_fetched_definition(editor, point, alt, cx); + } + + return true; + } + + end_selection + } + + fn mouse_dragged( + editor: &mut Editor, + MouseMovedEvent { + modifiers: Modifiers { cmd, shift, .. }, + position, + .. + }: MouseMovedEvent, + position_map: &PositionMap, + text_bounds: RectF, + cx: &mut EventContext, + ) -> bool { + // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed + // Don't trigger hover popover if mouse is hovering over context menu + let point = if text_bounds.contains_point(position) { + position_map + .point_for_position(text_bounds, position) + .as_valid() + } else { + None + }; + + update_go_to_definition_link( + editor, + point.map(GoToDefinitionTrigger::Text), + cmd, + shift, + cx, + ); + + if editor.has_pending_selection() { + let mut scroll_delta = Vector2F::zero(); + + let vertical_margin = position_map.line_height.min(text_bounds.height() / 3.0); + let top = text_bounds.origin_y() + vertical_margin; + let bottom = text_bounds.lower_left().y() - vertical_margin; + if position.y() < top { + scroll_delta.set_y(-scale_vertical_mouse_autoscroll_delta(top - position.y())) + } + if position.y() > bottom { + scroll_delta.set_y(scale_vertical_mouse_autoscroll_delta(position.y() - bottom)) + } + + let horizontal_margin = position_map.line_height.min(text_bounds.width() / 3.0); + let left = text_bounds.origin_x() + horizontal_margin; + let right = text_bounds.upper_right().x() - horizontal_margin; + if position.x() < left { + scroll_delta.set_x(-scale_horizontal_mouse_autoscroll_delta( + left - position.x(), + )) + } + if position.x() > right { + scroll_delta.set_x(scale_horizontal_mouse_autoscroll_delta( + position.x() - right, + )) + } + + let point_for_position = position_map.point_for_position(text_bounds, position); + + editor.select( + SelectPhase::Update { + position: point_for_position.previous_valid, + goal_column: point_for_position.exact_unclipped.column(), + scroll_position: (position_map.snapshot.scroll_position() + scroll_delta) + .clamp(Vector2F::zero(), position_map.scroll_max), + }, + cx, + ); + hover_at(editor, point, cx); + true + } else { + hover_at(editor, point, cx); + false + } + } + + fn mouse_moved( + editor: &mut Editor, + MouseMovedEvent { + modifiers: Modifiers { shift, cmd, .. }, + position, + .. + }: MouseMovedEvent, + position_map: &PositionMap, + text_bounds: RectF, + cx: &mut ViewContext, + ) -> bool { + // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed + // Don't trigger hover popover if mouse is hovering over context menu + if text_bounds.contains_point(position) { + let point_for_position = position_map.point_for_position(text_bounds, position); + match point_for_position.as_valid() { + Some(point) => { + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(point)), + cmd, + shift, + cx, + ); + hover_at(editor, Some(point), cx); + } + None => { + update_inlay_link_and_hover_points( + &position_map.snapshot, + point_for_position, + editor, + cmd, + shift, + cx, + ); + } + } + } else { + update_go_to_definition_link(editor, None, cmd, shift, cx); + hover_at(editor, None, cx); + } + + true + } + + fn scroll( + editor: &mut Editor, + position: Vector2F, + mut delta: Vector2F, + precise: bool, + position_map: &PositionMap, + bounds: RectF, + cx: &mut ViewContext, + ) -> bool { + if !bounds.contains_point(position) { + return false; + } + + let line_height = position_map.line_height; + let max_glyph_width = position_map.em_width; + + let axis = if precise { + //Trackpad + position_map.snapshot.ongoing_scroll.filter(&mut delta) + } else { + //Not trackpad + delta *= vec2f(max_glyph_width, line_height); + None //Resets ongoing scroll + }; + + let scroll_position = position_map.snapshot.scroll_position(); + let x = (scroll_position.x() * max_glyph_width - delta.x()) / max_glyph_width; + let y = (scroll_position.y() * line_height - delta.y()) / line_height; + let scroll_position = vec2f(x, y).clamp(Vector2F::zero(), position_map.scroll_max); + editor.scroll(scroll_position, axis, cx); + + true + } + + fn paint_background( + &self, + gutter_bounds: RectF, + text_bounds: RectF, + layout: &LayoutState, + cx: &mut ViewContext, + ) { + let bounds = gutter_bounds.union_rect(text_bounds); + let scroll_top = + layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height; + cx.scene().push_quad(Quad { + bounds: gutter_bounds, + background: Some(self.style.gutter_background), + border: Border::new(0., Color::transparent_black()).into(), + corner_radii: Default::default(), + }); + cx.scene().push_quad(Quad { + bounds: text_bounds, + background: Some(self.style.background), + border: Border::new(0., Color::transparent_black()).into(), + corner_radii: Default::default(), + }); + + if let EditorMode::Full = layout.mode { + let mut active_rows = layout.active_rows.iter().peekable(); + while let Some((start_row, contains_non_empty_selection)) = active_rows.next() { + let mut end_row = *start_row; + while active_rows.peek().map_or(false, |r| { + *r.0 == end_row + 1 && r.1 == contains_non_empty_selection + }) { + active_rows.next().unwrap(); + end_row += 1; + } + + if !contains_non_empty_selection { + let origin = vec2f( + bounds.origin_x(), + bounds.origin_y() + (layout.position_map.line_height * *start_row as f32) + - scroll_top, + ); + let size = vec2f( + bounds.width(), + layout.position_map.line_height * (end_row - start_row + 1) as f32, + ); + cx.scene().push_quad(Quad { + bounds: RectF::new(origin, size), + background: Some(self.style.active_line_background), + border: Border::default().into(), + corner_radii: Default::default(), + }); + } + } + + if let Some(highlighted_rows) = &layout.highlighted_rows { + let origin = vec2f( + bounds.origin_x(), + bounds.origin_y() + + (layout.position_map.line_height * highlighted_rows.start as f32) + - scroll_top, + ); + let size = vec2f( + bounds.width(), + layout.position_map.line_height * highlighted_rows.len() as f32, + ); + cx.scene().push_quad(Quad { + bounds: RectF::new(origin, size), + background: Some(self.style.highlighted_line_background), + border: Border::default().into(), + corner_radii: Default::default(), + }); + } + + let scroll_left = + layout.position_map.snapshot.scroll_position().x() * layout.position_map.em_width; + + for (wrap_position, active) in layout.wrap_guides.iter() { + let x = + (text_bounds.origin_x() + wrap_position + layout.position_map.em_width / 2.) + - scroll_left; + + if x < text_bounds.origin_x() + || (layout.show_scrollbars && x > self.scrollbar_left(&bounds)) + { + continue; + } + + let color = if *active { + self.style.active_wrap_guide + } else { + self.style.wrap_guide + }; + cx.scene().push_quad(Quad { + bounds: RectF::new( + vec2f(x, text_bounds.origin_y()), + vec2f(1., text_bounds.height()), + ), + background: Some(color), + border: Border::new(0., Color::transparent_black()).into(), + corner_radii: Default::default(), + }); + } + } + } + + fn paint_gutter( + &mut self, + bounds: RectF, + visible_bounds: RectF, + layout: &mut LayoutState, + editor: &mut Editor, + cx: &mut ViewContext, + ) { + let line_height = layout.position_map.line_height; + + let scroll_position = layout.position_map.snapshot.scroll_position(); + let scroll_top = scroll_position.y() * line_height; + + let show_gutter = matches!( + settings::get::(cx).git.git_gutter, + Some(GitGutterSetting::TrackedFiles) + ); + + if show_gutter { + Self::paint_diff_hunks(bounds, layout, cx); + } + + for (ix, line) in layout.line_number_layouts.iter().enumerate() { + if let Some(line) = line { + let line_origin = bounds.origin() + + vec2f( + bounds.width() - line.width() - layout.gutter_padding, + ix as f32 * line_height - (scroll_top % line_height), + ); + + line.paint(line_origin, visible_bounds, line_height, cx); + } + } + + for (ix, fold_indicator) in layout.fold_indicators.iter_mut().enumerate() { + if let Some(indicator) = fold_indicator.as_mut() { + let position = vec2f( + bounds.width() - layout.gutter_padding, + ix as f32 * line_height - (scroll_top % line_height), + ); + let centering_offset = vec2f( + (layout.gutter_padding + layout.gutter_margin - indicator.size().x()) / 2., + (line_height - indicator.size().y()) / 2., + ); + + let indicator_origin = bounds.origin() + position + centering_offset; + + indicator.paint(indicator_origin, visible_bounds, editor, cx); + } + } + + if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() { + let mut x = 0.; + let mut y = *row as f32 * line_height - scroll_top; + x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.; + y += (line_height - indicator.size().y()) / 2.; + indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, editor, cx); + } + } + + fn paint_diff_hunks(bounds: RectF, layout: &mut LayoutState, cx: &mut ViewContext) { + let diff_style = &theme::current(cx).editor.diff.clone(); + let line_height = layout.position_map.line_height; + + let scroll_position = layout.position_map.snapshot.scroll_position(); + let scroll_top = scroll_position.y() * line_height; + + for hunk in &layout.display_hunks { + let (display_row_range, status) = match hunk { + //TODO: This rendering is entirely a horrible hack + &DisplayDiffHunk::Folded { display_row: row } => { + let start_y = row as f32 * line_height - scroll_top; + let end_y = start_y + line_height; + + let width = diff_style.removed_width_em * line_height; + let highlight_origin = bounds.origin() + vec2f(-width, start_y); + let highlight_size = vec2f(width * 2., end_y - start_y); + let highlight_bounds = RectF::new(highlight_origin, highlight_size); + + cx.scene().push_quad(Quad { + bounds: highlight_bounds, + background: Some(diff_style.modified), + border: Border::new(0., Color::transparent_black()).into(), + corner_radii: (1. * line_height).into(), + }); + + continue; + } + + DisplayDiffHunk::Unfolded { + display_row_range, + status, + } => (display_row_range, status), + }; + + let color = match status { + DiffHunkStatus::Added => diff_style.inserted, + DiffHunkStatus::Modified => diff_style.modified, + + //TODO: This rendering is entirely a horrible hack + DiffHunkStatus::Removed => { + let row = display_row_range.start; + + let offset = line_height / 2.; + let start_y = row as f32 * line_height - offset - scroll_top; + let end_y = start_y + line_height; + + let width = diff_style.removed_width_em * line_height; + let highlight_origin = bounds.origin() + vec2f(-width, start_y); + let highlight_size = vec2f(width * 2., end_y - start_y); + let highlight_bounds = RectF::new(highlight_origin, highlight_size); + + cx.scene().push_quad(Quad { + bounds: highlight_bounds, + background: Some(diff_style.deleted), + border: Border::new(0., Color::transparent_black()).into(), + corner_radii: (1. * line_height).into(), + }); + + continue; + } + }; + + let start_row = display_row_range.start; + let end_row = display_row_range.end; + + let start_y = start_row as f32 * line_height - scroll_top; + let end_y = end_row as f32 * line_height - scroll_top; + + let width = diff_style.width_em * line_height; + let highlight_origin = bounds.origin() + vec2f(-width, start_y); + let highlight_size = vec2f(width * 2., end_y - start_y); + let highlight_bounds = RectF::new(highlight_origin, highlight_size); + + cx.scene().push_quad(Quad { + bounds: highlight_bounds, + background: Some(color), + border: Border::new(0., Color::transparent_black()).into(), + corner_radii: (diff_style.corner_radius * line_height).into(), + }); + } + } + + fn paint_text( + &mut self, + bounds: RectF, + visible_bounds: RectF, + layout: &mut LayoutState, + editor: &mut Editor, + cx: &mut ViewContext, + ) { + let style = &self.style; + let scroll_position = layout.position_map.snapshot.scroll_position(); + let start_row = layout.visible_display_row_range.start; + let scroll_top = scroll_position.y() * layout.position_map.line_height; + let max_glyph_width = layout.position_map.em_width; + let scroll_left = scroll_position.x() * max_glyph_width; + let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.); + let line_end_overshoot = 0.15 * layout.position_map.line_height; + let whitespace_setting = editor.buffer.read(cx).settings_at(0, cx).show_whitespaces; + + cx.scene().push_layer(Some(bounds)); + + cx.scene().push_cursor_region(CursorRegion { + bounds, + style: if !editor.link_go_to_definition_state.definitions.is_empty() { + CursorStyle::PointingHand + } else { + CursorStyle::IBeam + }, + }); + + let fold_corner_radius = + self.style.folds.ellipses.corner_radius_factor * layout.position_map.line_height; + for (id, range, color) in layout.fold_ranges.iter() { + self.paint_highlighted_range( + range.clone(), + *color, + fold_corner_radius, + fold_corner_radius * 2., + layout, + content_origin, + scroll_top, + scroll_left, + bounds, + cx, + ); + + for bound in range_to_bounds( + &range, + content_origin, + scroll_left, + scroll_top, + &layout.visible_display_row_range, + line_end_overshoot, + &layout.position_map, + ) { + cx.scene().push_cursor_region(CursorRegion { + bounds: bound, + style: CursorStyle::PointingHand, + }); + + let display_row = range.start.row(); + + let buffer_row = DisplayPoint::new(display_row, 0) + .to_point(&layout.position_map.snapshot.display_snapshot) + .row; + + let view_id = cx.view_id(); + cx.scene().push_mouse_region( + MouseRegion::new::(view_id, *id as usize, bound) + .on_click(MouseButton::Left, move |_, editor: &mut Editor, cx| { + editor.unfold_at(&UnfoldAt { buffer_row }, cx) + }) + .with_notify_on_hover(true) + .with_notify_on_click(true), + ) + } + } + + for (range, color) in &layout.highlighted_ranges { + self.paint_highlighted_range( + range.clone(), + *color, + 0., + line_end_overshoot, + layout, + content_origin, + scroll_top, + scroll_left, + bounds, + cx, + ); + } + + let mut cursors = SmallVec::<[Cursor; 32]>::new(); + let corner_radius = 0.15 * layout.position_map.line_height; + let mut invisible_display_ranges = SmallVec::<[Range; 32]>::new(); + + for (selection_style, selections) in &layout.selections { + for selection in selections { + self.paint_highlighted_range( + selection.range.clone(), + selection_style.selection, + corner_radius, + corner_radius * 2., + layout, + content_origin, + scroll_top, + scroll_left, + bounds, + cx, + ); + + if selection.is_local && !selection.range.is_empty() { + invisible_display_ranges.push(selection.range.clone()); + } + if !selection.is_local || editor.show_local_cursors(cx) { + let cursor_position = selection.head; + if layout + .visible_display_row_range + .contains(&cursor_position.row()) + { + let cursor_row_layout = &layout.position_map.line_layouts + [(cursor_position.row() - start_row) as usize] + .line; + let cursor_column = cursor_position.column() as usize; + + let cursor_character_x = cursor_row_layout.x_for_index(cursor_column); + let mut block_width = + cursor_row_layout.x_for_index(cursor_column + 1) - cursor_character_x; + if block_width == 0.0 { + block_width = layout.position_map.em_width; + } + let block_text = if let CursorShape::Block = selection.cursor_shape { + layout + .position_map + .snapshot + .chars_at(cursor_position) + .next() + .and_then(|(character, _)| { + let font_id = + cursor_row_layout.font_for_index(cursor_column)?; + let text = character.to_string(); + + Some(cx.text_layout_cache().layout_str( + &text, + cursor_row_layout.font_size(), + &[( + text.chars().count(), + RunStyle { + font_id, + color: style.background, + underline: Default::default(), + }, + )], + )) + }) + } else { + None + }; + + let x = cursor_character_x - scroll_left; + let y = cursor_position.row() as f32 * layout.position_map.line_height + - scroll_top; + if selection.is_newest { + editor.pixel_position_of_newest_cursor = Some(vec2f( + bounds.origin_x() + x + block_width / 2., + bounds.origin_y() + y + layout.position_map.line_height / 2., + )); + } + cursors.push(Cursor { + color: selection_style.cursor, + block_width, + origin: vec2f(x, y), + line_height: layout.position_map.line_height, + shape: selection.cursor_shape, + block_text, + }); + } + } + } + } + + if let Some(visible_text_bounds) = bounds.intersection(visible_bounds) { + for (ix, line_with_invisibles) in layout.position_map.line_layouts.iter().enumerate() { + let row = start_row + ix as u32; + line_with_invisibles.draw( + layout, + row, + scroll_top, + content_origin, + scroll_left, + visible_text_bounds, + whitespace_setting, + &invisible_display_ranges, + visible_bounds, + cx, + ) + } + } + + cx.scene().push_layer(Some(bounds)); + for cursor in cursors { + cursor.paint(content_origin, cx); + } + cx.scene().pop_layer(); + + if let Some((position, context_menu)) = layout.context_menu.as_mut() { + cx.scene().push_stacking_context(None, None); + let cursor_row_layout = + &layout.position_map.line_layouts[(position.row() - start_row) as usize].line; + let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left; + let y = (position.row() + 1) as f32 * layout.position_map.line_height - scroll_top; + let mut list_origin = content_origin + vec2f(x, y); + let list_width = context_menu.size().x(); + let list_height = context_menu.size().y(); + + // Snap the right edge of the list to the right edge of the window if + // its horizontal bounds overflow. + if list_origin.x() + list_width > cx.window_size().x() { + list_origin.set_x((cx.window_size().x() - list_width).max(0.)); + } + + if list_origin.y() + list_height > bounds.max_y() { + list_origin.set_y(list_origin.y() - layout.position_map.line_height - list_height); + } + + context_menu.paint( + list_origin, + RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor + editor, + cx, + ); + + cx.scene().pop_stacking_context(); + } + + if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() { + cx.scene().push_stacking_context(None, None); + + // This is safe because we check on layout whether the required row is available + let hovered_row_layout = + &layout.position_map.line_layouts[(position.row() - start_row) as usize].line; + + // Minimum required size: Take the first popover, and add 1.5 times the minimum popover + // height. This is the size we will use to decide whether to render popovers above or below + // the hovered line. + let first_size = hover_popovers[0].size(); + let height_to_reserve = first_size.y() + + 1.5 * MIN_POPOVER_LINE_HEIGHT as f32 * layout.position_map.line_height; + + // Compute Hovered Point + let x = hovered_row_layout.x_for_index(position.column() as usize) - scroll_left; + let y = position.row() as f32 * layout.position_map.line_height - scroll_top; + let hovered_point = content_origin + vec2f(x, y); + + if hovered_point.y() - height_to_reserve > 0.0 { + // There is enough space above. Render popovers above the hovered point + let mut current_y = hovered_point.y(); + for hover_popover in hover_popovers { + let size = hover_popover.size(); + let mut popover_origin = vec2f(hovered_point.x(), current_y - size.y()); + + let x_out_of_bounds = bounds.max_x() - (popover_origin.x() + size.x()); + if x_out_of_bounds < 0.0 { + popover_origin.set_x(popover_origin.x() + x_out_of_bounds); + } + + hover_popover.paint( + popover_origin, + RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor + editor, + cx, + ); + + current_y = popover_origin.y() - HOVER_POPOVER_GAP; + } + } else { + // There is not enough space above. Render popovers below the hovered point + let mut current_y = hovered_point.y() + layout.position_map.line_height; + for hover_popover in hover_popovers { + let size = hover_popover.size(); + let mut popover_origin = vec2f(hovered_point.x(), current_y); + + let x_out_of_bounds = bounds.max_x() - (popover_origin.x() + size.x()); + if x_out_of_bounds < 0.0 { + popover_origin.set_x(popover_origin.x() + x_out_of_bounds); + } + + hover_popover.paint( + popover_origin, + RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor + editor, + cx, + ); + + current_y = popover_origin.y() + size.y() + HOVER_POPOVER_GAP; + } + } + + cx.scene().pop_stacking_context(); + } + + cx.scene().pop_layer(); + } + + fn scrollbar_left(&self, bounds: &RectF) -> f32 { + bounds.max_x() - self.style.theme.scrollbar.width + } + + fn paint_scrollbar( + &mut self, + bounds: RectF, + layout: &mut LayoutState, + editor: &Editor, + cx: &mut ViewContext, + ) { + enum ScrollbarMouseHandlers {} + if layout.mode != EditorMode::Full { + return; + } + + let style = &self.style.theme.scrollbar; + + let top = bounds.min_y(); + let bottom = bounds.max_y(); + let right = bounds.max_x(); + let left = self.scrollbar_left(&bounds); + let row_range = &layout.scrollbar_row_range; + let max_row = layout.max_row as f32 + (row_range.end - row_range.start); + + let mut height = bounds.height(); + let mut first_row_y_offset = 0.0; + + // Impose a minimum height on the scrollbar thumb + let row_height = height / max_row; + let min_thumb_height = + style.min_height_factor * cx.font_cache.line_height(self.style.text.font_size); + let thumb_height = (row_range.end - row_range.start) * row_height; + if thumb_height < min_thumb_height { + first_row_y_offset = (min_thumb_height - thumb_height) / 2.0; + height -= min_thumb_height - thumb_height; + } + + let y_for_row = |row: f32| -> f32 { top + first_row_y_offset + row * row_height }; + + let thumb_top = y_for_row(row_range.start) - first_row_y_offset; + let thumb_bottom = y_for_row(row_range.end) + first_row_y_offset; + let track_bounds = RectF::from_points(vec2f(left, top), vec2f(right, bottom)); + let thumb_bounds = RectF::from_points(vec2f(left, thumb_top), vec2f(right, thumb_bottom)); + + if layout.show_scrollbars { + cx.scene().push_quad(Quad { + bounds: track_bounds, + border: style.track.border.into(), + background: style.track.background_color, + ..Default::default() + }); + let scrollbar_settings = settings::get::(cx).scrollbar; + let theme = theme::current(cx); + let scrollbar_theme = &theme.editor.scrollbar; + if layout.is_singleton && scrollbar_settings.selections { + let start_anchor = Anchor::min(); + let end_anchor = Anchor::max(); + let color = scrollbar_theme.selections; + let border = Border { + width: 1., + color: style.thumb.border.color, + overlay: false, + top: false, + right: true, + bottom: false, + left: true, + }; + let mut push_region = |start: DisplayPoint, end: DisplayPoint| { + let start_y = y_for_row(start.row() as f32); + let mut end_y = y_for_row(end.row() as f32); + if end_y - start_y < 1. { + end_y = start_y + 1.; + } + let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y)); + + cx.scene().push_quad(Quad { + bounds, + background: Some(color), + border: border.into(), + corner_radii: style.thumb.corner_radii.into(), + }) + }; + let background_ranges = editor + .background_highlight_row_ranges::( + start_anchor..end_anchor, + &layout.position_map.snapshot, + 50000, + ); + for row in background_ranges { + let start = row.start(); + let end = row.end(); + push_region(*start, *end); + } + } + + if layout.is_singleton && scrollbar_settings.git_diff { + let diff_style = scrollbar_theme.git.clone(); + for hunk in layout + .position_map + .snapshot + .buffer_snapshot + .git_diff_hunks_in_range(0..(max_row.floor() as u32)) + { + let start_display = Point::new(hunk.buffer_range.start, 0) + .to_display_point(&layout.position_map.snapshot.display_snapshot); + let end_display = Point::new(hunk.buffer_range.end, 0) + .to_display_point(&layout.position_map.snapshot.display_snapshot); + let start_y = y_for_row(start_display.row() as f32); + let mut end_y = if hunk.buffer_range.start == hunk.buffer_range.end { + y_for_row((end_display.row() + 1) as f32) + } else { + y_for_row((end_display.row()) as f32) + }; + + if end_y - start_y < 1. { + end_y = start_y + 1.; + } + let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y)); + + let color = match hunk.status() { + DiffHunkStatus::Added => diff_style.inserted, + DiffHunkStatus::Modified => diff_style.modified, + DiffHunkStatus::Removed => diff_style.deleted, + }; + + let border = Border { + width: 1., + color: style.thumb.border.color, + overlay: false, + top: false, + right: true, + bottom: false, + left: true, + }; + + cx.scene().push_quad(Quad { + bounds, + background: Some(color), + border: border.into(), + corner_radii: style.thumb.corner_radii.into(), + }) + } + } + + cx.scene().push_quad(Quad { + bounds: thumb_bounds, + border: style.thumb.border.into(), + background: style.thumb.background_color, + corner_radii: style.thumb.corner_radii.into(), + }); + } + + cx.scene().push_cursor_region(CursorRegion { + bounds: track_bounds, + style: CursorStyle::Arrow, + }); + let region_id = cx.view_id(); + cx.scene().push_mouse_region( + MouseRegion::new::(region_id, region_id, track_bounds) + .on_move(move |event, editor: &mut Editor, cx| { + if event.pressed_button.is_none() { + editor.scroll_manager.show_scrollbar(cx); + } + }) + .on_down(MouseButton::Left, { + let row_range = row_range.clone(); + move |event, editor: &mut Editor, cx| { + let y = event.position.y(); + if y < thumb_top || thumb_bottom < y { + let center_row = ((y - top) * max_row as f32 / height).round() as u32; + let top_row = center_row + .saturating_sub((row_range.end - row_range.start) as u32 / 2); + let mut position = editor.scroll_position(cx); + position.set_y(top_row as f32); + editor.set_scroll_position(position, cx); + } else { + editor.scroll_manager.show_scrollbar(cx); + } + } + }) + .on_drag(MouseButton::Left, { + move |event, editor: &mut Editor, cx| { + if event.end { + return; + } + + let y = event.prev_mouse_position.y(); + let new_y = event.position.y(); + if thumb_top < y && y < thumb_bottom { + let mut position = editor.scroll_position(cx); + position.set_y(position.y() + (new_y - y) * (max_row as f32) / height); + if position.y() < 0.0 { + position.set_y(0.); + } + editor.set_scroll_position(position, cx); + } + } + }), + ); + } + + #[allow(clippy::too_many_arguments)] + fn paint_highlighted_range( + &self, + range: Range, + color: Color, + corner_radius: f32, + line_end_overshoot: f32, + layout: &LayoutState, + content_origin: Vector2F, + scroll_top: f32, + scroll_left: f32, + bounds: RectF, + cx: &mut ViewContext, + ) { + let start_row = layout.visible_display_row_range.start; + let end_row = layout.visible_display_row_range.end; + if range.start != range.end { + let row_range = if range.end.column() == 0 { + cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row) + } else { + cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row) + }; + + let highlighted_range = HighlightedRange { + color, + line_height: layout.position_map.line_height, + corner_radius, + start_y: content_origin.y() + + row_range.start as f32 * layout.position_map.line_height + - scroll_top, + lines: row_range + .into_iter() + .map(|row| { + let line_layout = + &layout.position_map.line_layouts[(row - start_row) as usize].line; + HighlightedRangeLine { + start_x: if row == range.start.row() { + content_origin.x() + + line_layout.x_for_index(range.start.column() as usize) + - scroll_left + } else { + content_origin.x() - scroll_left + }, + end_x: if row == range.end.row() { + content_origin.x() + + line_layout.x_for_index(range.end.column() as usize) + - scroll_left + } else { + content_origin.x() + line_layout.width() + line_end_overshoot + - scroll_left + }, + } + }) + .collect(), + }; + + highlighted_range.paint(bounds, cx); + } + } + + fn paint_blocks( + &mut self, + bounds: RectF, + visible_bounds: RectF, + layout: &mut LayoutState, + editor: &mut Editor, + cx: &mut ViewContext, + ) { + let scroll_position = layout.position_map.snapshot.scroll_position(); + let scroll_left = scroll_position.x() * layout.position_map.em_width; + let scroll_top = scroll_position.y() * layout.position_map.line_height; + + for block in &mut layout.blocks { + let mut origin = bounds.origin() + + vec2f( + 0., + block.row as f32 * layout.position_map.line_height - scroll_top, + ); + if !matches!(block.style, BlockStyle::Sticky) { + origin += vec2f(-scroll_left, 0.); + } + block.element.paint(origin, visible_bounds, editor, cx); + } + } + + fn column_pixels(&self, column: usize, cx: &ViewContext) -> f32 { + let style = &self.style; + + cx.text_layout_cache() + .layout_str( + " ".repeat(column).as_str(), + style.text.font_size, + &[( + column, + RunStyle { + font_id: style.text.font_id, + color: Color::black(), + underline: Default::default(), + }, + )], + ) + .width() + } + + fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext) -> f32 { + let digit_count = (snapshot.max_buffer_row() as f32 + 1.).log10().floor() as usize + 1; + self.column_pixels(digit_count, cx) + } + + //Folds contained in a hunk are ignored apart from shrinking visual size + //If a fold contains any hunks then that fold line is marked as modified + fn layout_git_gutters( + &self, + display_rows: Range, + snapshot: &EditorSnapshot, + ) -> Vec { + let buffer_snapshot = &snapshot.buffer_snapshot; + + let buffer_start_row = DisplayPoint::new(display_rows.start, 0) + .to_point(snapshot) + .row; + let buffer_end_row = DisplayPoint::new(display_rows.end, 0) + .to_point(snapshot) + .row; + + buffer_snapshot + .git_diff_hunks_in_range(buffer_start_row..buffer_end_row) + .map(|hunk| diff_hunk_to_display(hunk, snapshot)) + .dedup() + .collect() + } + + fn calculate_relative_line_numbers( + &self, + snapshot: &EditorSnapshot, + rows: &Range, + relative_to: Option, + ) -> HashMap { + let mut relative_rows: HashMap = Default::default(); + let Some(relative_to) = relative_to else { + return relative_rows; + }; + + let start = rows.start.min(relative_to); + let end = rows.end.max(relative_to); + + let buffer_rows = snapshot + .buffer_rows(start) + .take(1 + (end - start) as usize) + .collect::>(); + + let head_idx = relative_to - start; + let mut delta = 1; + let mut i = head_idx + 1; + while i < buffer_rows.len() as u32 { + if buffer_rows[i as usize].is_some() { + if rows.contains(&(i + start)) { + relative_rows.insert(i + start, delta); + } + delta += 1; + } + i += 1; + } + delta = 1; + i = head_idx.min(buffer_rows.len() as u32 - 1); + while i > 0 && buffer_rows[i as usize].is_none() { + i -= 1; + } + + while i > 0 { + i -= 1; + if buffer_rows[i as usize].is_some() { + if rows.contains(&(i + start)) { + relative_rows.insert(i + start, delta); + } + delta += 1; + } + } + + relative_rows + } + + fn layout_line_numbers( + &self, + rows: Range, + active_rows: &BTreeMap, + newest_selection_head: DisplayPoint, + is_singleton: bool, + snapshot: &EditorSnapshot, + cx: &ViewContext, + ) -> ( + Vec>, + Vec>, + ) { + let style = &self.style; + let include_line_numbers = snapshot.mode == EditorMode::Full; + let mut line_number_layouts = Vec::with_capacity(rows.len()); + let mut fold_statuses = Vec::with_capacity(rows.len()); + let mut line_number = String::new(); + let is_relative = settings::get::(cx).relative_line_numbers; + let relative_to = if is_relative { + Some(newest_selection_head.row()) + } else { + None + }; + + let relative_rows = self.calculate_relative_line_numbers(&snapshot, &rows, relative_to); + + for (ix, row) in snapshot + .buffer_rows(rows.start) + .take((rows.end - rows.start) as usize) + .enumerate() + { + let display_row = rows.start + ix as u32; + let (active, color) = if active_rows.contains_key(&display_row) { + (true, style.line_number_active) + } else { + (false, style.line_number) + }; + if let Some(buffer_row) = row { + if include_line_numbers { + line_number.clear(); + let default_number = buffer_row + 1; + let number = relative_rows + .get(&(ix as u32 + rows.start)) + .unwrap_or(&default_number); + write!(&mut line_number, "{}", number).unwrap(); + line_number_layouts.push(Some(cx.text_layout_cache().layout_str( + &line_number, + style.text.font_size, + &[( + line_number.len(), + RunStyle { + font_id: style.text.font_id, + color, + underline: Default::default(), + }, + )], + ))); + fold_statuses.push( + is_singleton + .then(|| { + snapshot + .fold_for_line(buffer_row) + .map(|fold_status| (fold_status, buffer_row, active)) + }) + .flatten(), + ) + } + } else { + fold_statuses.push(None); + line_number_layouts.push(None); + } + } + + (line_number_layouts, fold_statuses) + } + + fn layout_lines( + &mut self, + rows: Range, + line_number_layouts: &[Option], + snapshot: &EditorSnapshot, + cx: &ViewContext, + ) -> Vec { + if rows.start >= rows.end { + return Vec::new(); + } + + // When the editor is empty and unfocused, then show the placeholder. + if snapshot.is_empty() { + let placeholder_style = self + .style + .placeholder_text + .as_ref() + .unwrap_or(&self.style.text); + let placeholder_text = snapshot.placeholder_text(); + let placeholder_lines = placeholder_text + .as_ref() + .map_or("", AsRef::as_ref) + .split('\n') + .skip(rows.start as usize) + .chain(iter::repeat("")) + .take(rows.len()); + placeholder_lines + .map(|line| { + cx.text_layout_cache().layout_str( + line, + placeholder_style.font_size, + &[( + line.len(), + RunStyle { + font_id: placeholder_style.font_id, + color: placeholder_style.color, + underline: Default::default(), + }, + )], + ) + }) + .map(|line| LineWithInvisibles { + line, + invisibles: Vec::new(), + }) + .collect() + } else { + let style = &self.style; + let chunks = snapshot.highlighted_chunks(rows.clone(), true, style); + + LineWithInvisibles::from_chunks( + chunks, + &style.text, + cx.text_layout_cache(), + cx.font_cache(), + MAX_LINE_LEN, + rows.len() as usize, + line_number_layouts, + snapshot.mode, + ) + } + } + + #[allow(clippy::too_many_arguments)] + fn layout_blocks( + &mut self, + rows: Range, + snapshot: &EditorSnapshot, + editor_width: f32, + scroll_width: f32, + gutter_padding: f32, + gutter_width: f32, + em_width: f32, + text_x: f32, + line_height: f32, + style: &EditorStyle, + line_layouts: &[LineWithInvisibles], + editor: &mut Editor, + cx: &mut ViewContext, + ) -> (f32, Vec) { + let mut block_id = 0; + let scroll_x = snapshot.scroll_anchor.offset.x(); + let (fixed_blocks, non_fixed_blocks) = snapshot + .blocks_in_range(rows.clone()) + .partition::, _>(|(_, block)| match block { + TransformBlock::ExcerptHeader { .. } => false, + TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed, + }); + let mut render_block = |block: &TransformBlock, width: f32, block_id: usize| { + let mut element = match block { + TransformBlock::Custom(block) => { + let align_to = block + .position() + .to_point(&snapshot.buffer_snapshot) + .to_display_point(snapshot); + let anchor_x = text_x + + if rows.contains(&align_to.row()) { + line_layouts[(align_to.row() - rows.start) as usize] + .line + .x_for_index(align_to.column() as usize) + } else { + layout_line(align_to.row(), snapshot, style, cx.text_layout_cache()) + .x_for_index(align_to.column() as usize) + }; + + block.render(&mut BlockContext { + view_context: cx, + anchor_x, + gutter_padding, + line_height, + scroll_x, + gutter_width, + em_width, + block_id, + }) + } + TransformBlock::ExcerptHeader { + id, + buffer, + range, + starts_new_buffer, + .. + } => { + let tooltip_style = theme::current(cx).tooltip.clone(); + let include_root = editor + .project + .as_ref() + .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) + .unwrap_or_default(); + let jump_icon = project::File::from_dyn(buffer.file()).map(|file| { + let jump_path = ProjectPath { + worktree_id: file.worktree_id(cx), + path: file.path.clone(), + }; + let jump_anchor = range + .primary + .as_ref() + .map_or(range.context.start, |primary| primary.start); + let jump_position = language::ToPoint::to_point(&jump_anchor, buffer); + + enum JumpIcon {} + MouseEventHandler::new::((*id).into(), cx, |state, _| { + let style = style.jump_icon.style_for(state); + Svg::new("icons/arrow_up_right.svg") + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .contained() + .with_style(style.container) + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, editor, cx| { + if let Some(workspace) = editor + .workspace + .as_ref() + .and_then(|(workspace, _)| workspace.upgrade(cx)) + { + workspace.update(cx, |workspace, cx| { + Editor::jump( + workspace, + jump_path.clone(), + jump_position, + jump_anchor, + cx, + ); + }); + } + }) + .with_tooltip::( + (*id).into(), + "Jump to Buffer".to_string(), + Some(Box::new(crate::OpenExcerpts)), + tooltip_style.clone(), + cx, + ) + .aligned() + .flex_float() + }); + + if *starts_new_buffer { + let editor_font_size = style.text.font_size; + let style = &style.diagnostic_path_header; + let font_size = (style.text_scale_factor * editor_font_size).round(); + + let path = buffer.resolve_file_path(cx, include_root); + let mut filename = None; + let mut parent_path = None; + // Can't use .and_then() because `.file_name()` and `.parent()` return references :( + if let Some(path) = path { + filename = path.file_name().map(|f| f.to_string_lossy().to_string()); + parent_path = + path.parent().map(|p| p.to_string_lossy().to_string() + "/"); + } + + Flex::row() + .with_child( + Label::new( + filename.unwrap_or_else(|| "untitled".to_string()), + style.filename.text.clone().with_font_size(font_size), + ) + .contained() + .with_style(style.filename.container) + .aligned(), + ) + .with_children(parent_path.map(|path| { + Label::new(path, style.path.text.clone().with_font_size(font_size)) + .contained() + .with_style(style.path.container) + .aligned() + })) + .with_children(jump_icon) + .contained() + .with_style(style.container) + .with_padding_left(gutter_padding) + .with_padding_right(gutter_padding) + .expanded() + .into_any_named("path header block") + } else { + let text_style = style.text.clone(); + Flex::row() + .with_child(Label::new("⋯", text_style)) + .with_children(jump_icon) + .contained() + .with_padding_left(gutter_padding) + .with_padding_right(gutter_padding) + .expanded() + .into_any_named("collapsed context") + } + } + }; + + element.layout( + SizeConstraint { + min: Vector2F::zero(), + max: vec2f(width, block.height() as f32 * line_height), + }, + editor, + cx, + ); + element + }; + + let mut fixed_block_max_width = 0f32; + let mut blocks = Vec::new(); + for (row, block) in fixed_blocks { + let element = render_block(block, f32::INFINITY, block_id); + block_id += 1; + fixed_block_max_width = fixed_block_max_width.max(element.size().x() + em_width); + blocks.push(BlockLayout { + row, + element, + style: BlockStyle::Fixed, + }); + } + for (row, block) in non_fixed_blocks { + let style = match block { + TransformBlock::Custom(block) => block.style(), + TransformBlock::ExcerptHeader { .. } => BlockStyle::Sticky, + }; + let width = match style { + BlockStyle::Sticky => editor_width, + BlockStyle::Flex => editor_width + .max(fixed_block_max_width) + .max(gutter_width + scroll_width), + BlockStyle::Fixed => unreachable!(), + }; + let element = render_block(block, width, block_id); + block_id += 1; + blocks.push(BlockLayout { + row, + element, + style, + }); + } + ( + scroll_width.max(fixed_block_max_width - gutter_width), + blocks, + ) + } +} + +#[derive(Debug)] +pub struct LineWithInvisibles { + pub line: Line, + invisibles: Vec, +} + +impl LineWithInvisibles { + fn from_chunks<'a>( + chunks: impl Iterator>, + text_style: &TextStyle, + text_layout_cache: &TextLayoutCache, + font_cache: &Arc, + max_line_len: usize, + max_line_count: usize, + line_number_layouts: &[Option], + editor_mode: EditorMode, + ) -> Vec { + let mut layouts = Vec::with_capacity(max_line_count); + let mut line = String::new(); + let mut invisibles = Vec::new(); + let mut styles = Vec::new(); + let mut non_whitespace_added = false; + let mut row = 0; + let mut line_exceeded_max_len = false; + for highlighted_chunk in chunks.chain([HighlightedChunk { + chunk: "\n", + style: None, + is_tab: false, + }]) { + for (ix, mut line_chunk) in highlighted_chunk.chunk.split('\n').enumerate() { + if ix > 0 { + layouts.push(Self { + line: text_layout_cache.layout_str(&line, text_style.font_size, &styles), + invisibles: invisibles.drain(..).collect(), + }); + + line.clear(); + styles.clear(); + row += 1; + line_exceeded_max_len = false; + non_whitespace_added = false; + if row == max_line_count { + return layouts; + } + } + + if !line_chunk.is_empty() && !line_exceeded_max_len { + let text_style = if let Some(style) = highlighted_chunk.style { + text_style + .clone() + .highlight(style, font_cache) + .map(Cow::Owned) + .unwrap_or_else(|_| Cow::Borrowed(text_style)) + } else { + Cow::Borrowed(text_style) + }; + + if line.len() + line_chunk.len() > max_line_len { + let mut chunk_len = max_line_len - line.len(); + while !line_chunk.is_char_boundary(chunk_len) { + chunk_len -= 1; + } + line_chunk = &line_chunk[..chunk_len]; + line_exceeded_max_len = true; + } + + styles.push(( + line_chunk.len(), + RunStyle { + font_id: text_style.font_id, + color: text_style.color, + underline: text_style.underline, + }, + )); + + if editor_mode == EditorMode::Full { + // Line wrap pads its contents with fake whitespaces, + // avoid printing them + let inside_wrapped_string = line_number_layouts + .get(row) + .and_then(|layout| layout.as_ref()) + .is_none(); + if highlighted_chunk.is_tab { + if non_whitespace_added || !inside_wrapped_string { + invisibles.push(Invisible::Tab { + line_start_offset: line.len(), + }); + } + } else { + invisibles.extend( + line_chunk + .chars() + .enumerate() + .filter(|(_, line_char)| { + let is_whitespace = line_char.is_whitespace(); + non_whitespace_added |= !is_whitespace; + is_whitespace + && (non_whitespace_added || !inside_wrapped_string) + }) + .map(|(whitespace_index, _)| Invisible::Whitespace { + line_offset: line.len() + whitespace_index, + }), + ) + } + } + + line.push_str(line_chunk); + } + } + } + + layouts + } + + fn draw( + &self, + layout: &LayoutState, + row: u32, + scroll_top: f32, + content_origin: Vector2F, + scroll_left: f32, + visible_text_bounds: RectF, + whitespace_setting: ShowWhitespaceSetting, + selection_ranges: &[Range], + visible_bounds: RectF, + cx: &mut ViewContext, + ) { + let line_height = layout.position_map.line_height; + let line_y = row as f32 * line_height - scroll_top; + + self.line.paint( + content_origin + vec2f(-scroll_left, line_y), + visible_text_bounds, + line_height, + cx, + ); + + self.draw_invisibles( + &selection_ranges, + layout, + content_origin, + scroll_left, + line_y, + row, + visible_bounds, + line_height, + whitespace_setting, + cx, + ); + } + + fn draw_invisibles( + &self, + selection_ranges: &[Range], + layout: &LayoutState, + content_origin: Vector2F, + scroll_left: f32, + line_y: f32, + row: u32, + visible_bounds: RectF, + line_height: f32, + whitespace_setting: ShowWhitespaceSetting, + cx: &mut ViewContext, + ) { + let allowed_invisibles_regions = match whitespace_setting { + ShowWhitespaceSetting::None => return, + ShowWhitespaceSetting::Selection => Some(selection_ranges), + ShowWhitespaceSetting::All => None, + }; + + for invisible in &self.invisibles { + let (&token_offset, invisible_symbol) = match invisible { + Invisible::Tab { line_start_offset } => (line_start_offset, &layout.tab_invisible), + Invisible::Whitespace { line_offset } => (line_offset, &layout.space_invisible), + }; + + let x_offset = self.line.x_for_index(token_offset); + let invisible_offset = + (layout.position_map.em_width - invisible_symbol.width()).max(0.0) / 2.0; + let origin = content_origin + vec2f(-scroll_left + x_offset + invisible_offset, line_y); + + if let Some(allowed_regions) = allowed_invisibles_regions { + let invisible_point = DisplayPoint::new(row, token_offset as u32); + if !allowed_regions + .iter() + .any(|region| region.start <= invisible_point && invisible_point < region.end) + { + continue; + } + } + invisible_symbol.paint(origin, visible_bounds, line_height, cx); + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Invisible { + Tab { line_start_offset: usize }, + Whitespace { line_offset: usize }, +} + +impl Element for EditorElement { + type LayoutState = LayoutState; + type PaintState = (); + + fn layout( + &mut self, + constraint: SizeConstraint, + editor: &mut Editor, + cx: &mut ViewContext, + ) -> (Vector2F, Self::LayoutState) { + let mut size = constraint.max; + if size.x().is_infinite() { + unimplemented!("we don't yet handle an infinite width constraint on buffer elements"); + } + + let snapshot = editor.snapshot(cx); + let style = self.style.clone(); + + let line_height = (style.text.font_size * style.line_height_scalar).round(); + + let gutter_padding; + let gutter_width; + let gutter_margin; + if snapshot.show_gutter { + let em_width = style.text.em_width(cx.font_cache()); + gutter_padding = (em_width * style.gutter_padding_factor).round(); + gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0; + gutter_margin = -style.text.descent(cx.font_cache()); + } else { + gutter_padding = 0.0; + gutter_width = 0.0; + gutter_margin = 0.0; + }; + + let text_width = size.x() - gutter_width; + let em_width = style.text.em_width(cx.font_cache()); + let em_advance = style.text.em_advance(cx.font_cache()); + let overscroll = vec2f(em_width, 0.); + let snapshot = { + editor.set_visible_line_count(size.y() / line_height, cx); + + let editor_width = text_width - gutter_margin - overscroll.x() - em_width; + let wrap_width = match editor.soft_wrap_mode(cx) { + SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance, + SoftWrap::EditorWidth => editor_width, + SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance), + }; + + if editor.set_wrap_width(Some(wrap_width), cx) { + editor.snapshot(cx) + } else { + snapshot + } + }; + + let wrap_guides = editor + .wrap_guides(cx) + .iter() + .map(|(guide, active)| (self.column_pixels(*guide, cx), *active)) + .collect(); + + let scroll_height = (snapshot.max_point().row() + 1) as f32 * line_height; + if let EditorMode::AutoHeight { max_lines } = snapshot.mode { + size.set_y( + scroll_height + .min(constraint.max_along(Axis::Vertical)) + .max(constraint.min_along(Axis::Vertical)) + .max(line_height) + .min(line_height * max_lines as f32), + ) + } else if let EditorMode::SingleLine = snapshot.mode { + size.set_y(line_height.max(constraint.min_along(Axis::Vertical))) + } else if size.y().is_infinite() { + size.set_y(scroll_height); + } + let gutter_size = vec2f(gutter_width, size.y()); + let text_size = vec2f(text_width, size.y()); + + let autoscroll_horizontally = editor.autoscroll_vertically(size.y(), line_height, cx); + let mut snapshot = editor.snapshot(cx); + + let scroll_position = snapshot.scroll_position(); + // The scroll position is a fractional point, the whole number of which represents + // the top of the window in terms of display rows. + let start_row = scroll_position.y() as u32; + let height_in_lines = size.y() / line_height; + let max_row = snapshot.max_point().row(); + + // Add 1 to ensure selections bleed off screen + let end_row = 1 + cmp::min( + (scroll_position.y() + height_in_lines).ceil() as u32, + max_row, + ); + + let start_anchor = if start_row == 0 { + Anchor::min() + } else { + snapshot + .buffer_snapshot + .anchor_before(DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left)) + }; + let end_anchor = if end_row > max_row { + Anchor::max() + } else { + snapshot + .buffer_snapshot + .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right)) + }; + + let mut selections: Vec<(SelectionStyle, Vec)> = Vec::new(); + let mut active_rows = BTreeMap::new(); + let mut fold_ranges = Vec::new(); + let is_singleton = editor.is_singleton(cx); + + let highlighted_rows = editor.highlighted_rows(); + let theme = theme::current(cx); + let highlighted_ranges = editor.background_highlights_in_range( + start_anchor..end_anchor, + &snapshot.display_snapshot, + theme.as_ref(), + ); + + fold_ranges.extend( + snapshot + .folds_in_range(start_anchor..end_anchor) + .map(|anchor| { + let start = anchor.start.to_point(&snapshot.buffer_snapshot); + ( + start.row, + start.to_display_point(&snapshot.display_snapshot) + ..anchor.end.to_display_point(&snapshot), + ) + }), + ); + + let mut newest_selection_head = None; + + if editor.show_local_selections { + let mut local_selections: Vec> = editor + .selections + .disjoint_in_range(start_anchor..end_anchor, cx); + local_selections.extend(editor.selections.pending(cx)); + let mut layouts = Vec::new(); + let newest = editor.selections.newest(cx); + for selection in local_selections.drain(..) { + let is_empty = selection.start == selection.end; + let is_newest = selection == newest; + + let layout = SelectionLayout::new( + selection, + editor.selections.line_mode, + editor.cursor_shape, + &snapshot.display_snapshot, + is_newest, + true, + ); + if is_newest { + newest_selection_head = Some(layout.head); + } + + for row in cmp::max(layout.active_rows.start, start_row) + ..=cmp::min(layout.active_rows.end, end_row) + { + let contains_non_empty_selection = active_rows.entry(row).or_insert(!is_empty); + *contains_non_empty_selection |= !is_empty; + } + layouts.push(layout); + } + + selections.push((style.selection, layouts)); + } + + if let Some(collaboration_hub) = &editor.collaboration_hub { + // When following someone, render the local selections in their color. + if let Some(leader_id) = editor.leader_peer_id { + if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id) { + if let Some(participant_index) = collaboration_hub + .user_participant_indices(cx) + .get(&collaborator.user_id) + { + if let Some((local_selection_style, _)) = selections.first_mut() { + *local_selection_style = + style.selection_style_for_room_participant(participant_index.0); + } + } + } + } + + let mut remote_selections = HashMap::default(); + for selection in snapshot.remote_selections_in_range( + &(start_anchor..end_anchor), + collaboration_hub.as_ref(), + cx, + ) { + let selection_style = if let Some(participant_index) = selection.participant_index { + style.selection_style_for_room_participant(participant_index.0) + } else { + style.absent_selection + }; + + // Don't re-render the leader's selections, since the local selections + // match theirs. + if Some(selection.peer_id) == editor.leader_peer_id { + continue; + } + + remote_selections + .entry(selection.replica_id) + .or_insert((selection_style, Vec::new())) + .1 + .push(SelectionLayout::new( + selection.selection, + selection.line_mode, + selection.cursor_shape, + &snapshot.display_snapshot, + false, + false, + )); + } + + selections.extend(remote_selections.into_values()); + } + + let scrollbar_settings = &settings::get::(cx).scrollbar; + let show_scrollbars = match scrollbar_settings.show { + ShowScrollbar::Auto => { + // Git + (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs()) + || + // Selections + (is_singleton && scrollbar_settings.selections && !highlighted_ranges.is_empty()) + // Scrollmanager + || editor.scroll_manager.scrollbars_visible() + } + ShowScrollbar::System => editor.scroll_manager.scrollbars_visible(), + ShowScrollbar::Always => true, + ShowScrollbar::Never => false, + }; + + let fold_ranges: Vec<(BufferRow, Range, Color)> = fold_ranges + .into_iter() + .map(|(id, fold)| { + let color = self + .style + .folds + .ellipses + .background + .style_for(&mut cx.mouse_state::(id as usize)) + .color; + + (id, fold, color) + }) + .collect(); + + let head_for_relative = newest_selection_head.unwrap_or_else(|| { + let newest = editor.selections.newest::(cx); + SelectionLayout::new( + newest, + editor.selections.line_mode, + editor.cursor_shape, + &snapshot.display_snapshot, + true, + true, + ) + .head + }); + + let (line_number_layouts, fold_statuses) = self.layout_line_numbers( + start_row..end_row, + &active_rows, + head_for_relative, + is_singleton, + &snapshot, + cx, + ); + + let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot); + + let scrollbar_row_range = scroll_position.y()..(scroll_position.y() + height_in_lines); + + let mut max_visible_line_width = 0.0; + let line_layouts = + self.layout_lines(start_row..end_row, &line_number_layouts, &snapshot, cx); + for line_with_invisibles in &line_layouts { + if line_with_invisibles.line.width() > max_visible_line_width { + max_visible_line_width = line_with_invisibles.line.width(); + } + } + + let style = self.style.clone(); + let longest_line_width = layout_line( + snapshot.longest_row(), + &snapshot, + &style, + cx.text_layout_cache(), + ) + .width(); + let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.x(); + let em_width = style.text.em_width(cx.font_cache()); + let (scroll_width, blocks) = self.layout_blocks( + start_row..end_row, + &snapshot, + size.x(), + scroll_width, + gutter_padding, + gutter_width, + em_width, + gutter_width + gutter_margin, + line_height, + &style, + &line_layouts, + editor, + cx, + ); + + let scroll_max = vec2f( + ((scroll_width - text_size.x()) / em_width).max(0.0), + max_row as f32, + ); + + let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x()); + + let autoscrolled = if autoscroll_horizontally { + editor.autoscroll_horizontally( + start_row, + text_size.x(), + scroll_width, + em_width, + &line_layouts, + cx, + ) + } else { + false + }; + + if clamped || autoscrolled { + snapshot = editor.snapshot(cx); + } + + let style = editor.style(cx); + + let mut context_menu = None; + let mut code_actions_indicator = None; + if let Some(newest_selection_head) = newest_selection_head { + if (start_row..end_row).contains(&newest_selection_head.row()) { + if editor.context_menu_visible() { + context_menu = + editor.render_context_menu(newest_selection_head, style.clone(), cx); + } + + let active = matches!( + editor.context_menu.read().as_ref(), + Some(crate::ContextMenu::CodeActions(_)) + ); + + code_actions_indicator = editor + .render_code_actions_indicator(&style, active, cx) + .map(|indicator| (newest_selection_head.row(), indicator)); + } + } + + let visible_rows = start_row..start_row + line_layouts.len() as u32; + let mut hover = editor.hover_state.render( + &snapshot, + &style, + visible_rows, + editor.workspace.as_ref().map(|(w, _)| w.clone()), + cx, + ); + let mode = editor.mode; + + let mut fold_indicators = editor.render_fold_indicators( + fold_statuses, + &style, + editor.gutter_hovered, + line_height, + gutter_margin, + cx, + ); + + if let Some((_, context_menu)) = context_menu.as_mut() { + context_menu.layout( + SizeConstraint { + min: Vector2F::zero(), + max: vec2f( + cx.window_size().x() * 0.7, + (12. * line_height).min((size.y() - line_height) / 2.), + ), + }, + editor, + cx, + ); + } + + if let Some((_, indicator)) = code_actions_indicator.as_mut() { + indicator.layout( + SizeConstraint::strict_along( + Axis::Vertical, + line_height * style.code_actions.vertical_scale, + ), + editor, + cx, + ); + } + + for fold_indicator in fold_indicators.iter_mut() { + if let Some(indicator) = fold_indicator.as_mut() { + indicator.layout( + SizeConstraint::strict_along( + Axis::Vertical, + line_height * style.code_actions.vertical_scale, + ), + editor, + cx, + ); + } + } + + if let Some((_, hover_popovers)) = hover.as_mut() { + for hover_popover in hover_popovers.iter_mut() { + hover_popover.layout( + SizeConstraint { + min: Vector2F::zero(), + max: vec2f( + (120. * em_width) // Default size + .min(size.x() / 2.) // Shrink to half of the editor width + .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters + (16. * line_height) // Default size + .min(size.y() / 2.) // Shrink to half of the editor height + .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines + ), + }, + editor, + cx, + ); + } + } + + let invisible_symbol_font_size = self.style.text.font_size / 2.0; + let invisible_symbol_style = RunStyle { + color: self.style.whitespace, + font_id: self.style.text.font_id, + underline: Default::default(), + }; + + ( + size, + LayoutState { + mode, + position_map: Arc::new(PositionMap { + size, + scroll_max, + line_layouts, + line_height, + em_width, + em_advance, + snapshot, + }), + visible_display_row_range: start_row..end_row, + wrap_guides, + gutter_size, + gutter_padding, + text_size, + scrollbar_row_range, + show_scrollbars, + is_singleton, + max_row, + gutter_margin, + active_rows, + highlighted_rows, + highlighted_ranges, + fold_ranges, + line_number_layouts, + display_hunks, + blocks, + selections, + context_menu, + code_actions_indicator, + fold_indicators, + tab_invisible: cx.text_layout_cache().layout_str( + "→", + invisible_symbol_font_size, + &[("→".len(), invisible_symbol_style)], + ), + space_invisible: cx.text_layout_cache().layout_str( + "•", + invisible_symbol_font_size, + &[("•".len(), invisible_symbol_style)], + ), + hover_popovers: hover, + }, + ) + } + + fn paint( + &mut self, + bounds: RectF, + visible_bounds: RectF, + layout: &mut Self::LayoutState, + editor: &mut Editor, + cx: &mut ViewContext, + ) -> Self::PaintState { + let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); + cx.scene().push_layer(Some(visible_bounds)); + + let gutter_bounds = RectF::new(bounds.origin(), layout.gutter_size); + let text_bounds = RectF::new( + bounds.origin() + vec2f(layout.gutter_size.x(), 0.0), + layout.text_size, + ); + + Self::attach_mouse_handlers( + &layout.position_map, + layout.hover_popovers.is_some(), + visible_bounds, + text_bounds, + gutter_bounds, + bounds, + cx, + ); + + self.paint_background(gutter_bounds, text_bounds, layout, cx); + if layout.gutter_size.x() > 0. { + self.paint_gutter(gutter_bounds, visible_bounds, layout, editor, cx); + } + self.paint_text(text_bounds, visible_bounds, layout, editor, cx); + + cx.scene().push_layer(Some(bounds)); + if !layout.blocks.is_empty() { + self.paint_blocks(bounds, visible_bounds, layout, editor, cx); + } + self.paint_scrollbar(bounds, layout, &editor, cx); + cx.scene().pop_layer(); + cx.scene().pop_layer(); + } + + fn rect_for_text_range( + &self, + range_utf16: Range, + bounds: RectF, + _: RectF, + layout: &Self::LayoutState, + _: &Self::PaintState, + _: &Editor, + _: &ViewContext, + ) -> Option { + let text_bounds = RectF::new( + bounds.origin() + vec2f(layout.gutter_size.x(), 0.0), + layout.text_size, + ); + let content_origin = text_bounds.origin() + vec2f(layout.gutter_margin, 0.); + let scroll_position = layout.position_map.snapshot.scroll_position(); + let start_row = scroll_position.y() as u32; + let scroll_top = scroll_position.y() * layout.position_map.line_height; + let scroll_left = scroll_position.x() * layout.position_map.em_width; + + let range_start = OffsetUtf16(range_utf16.start) + .to_display_point(&layout.position_map.snapshot.display_snapshot); + if range_start.row() < start_row { + return None; + } + + let line = &layout + .position_map + .line_layouts + .get((range_start.row() - start_row) as usize)? + .line; + let range_start_x = line.x_for_index(range_start.column() as usize); + let range_start_y = range_start.row() as f32 * layout.position_map.line_height; + Some(RectF::new( + content_origin + + vec2f( + range_start_x, + range_start_y + layout.position_map.line_height, + ) + - vec2f(scroll_left, scroll_top), + vec2f( + layout.position_map.em_width, + layout.position_map.line_height, + ), + )) + } + + fn debug( + &self, + bounds: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &Editor, + _: &ViewContext, + ) -> json::Value { + json!({ + "type": "BufferElement", + "bounds": bounds.to_json() + }) + } +} + +type BufferRow = u32; + +pub struct LayoutState { + position_map: Arc, + gutter_size: Vector2F, + gutter_padding: f32, + gutter_margin: f32, + text_size: Vector2F, + mode: EditorMode, + wrap_guides: SmallVec<[(f32, bool); 2]>, + visible_display_row_range: Range, + active_rows: BTreeMap, + highlighted_rows: Option>, + line_number_layouts: Vec>, + display_hunks: Vec, + blocks: Vec, + highlighted_ranges: Vec<(Range, Color)>, + fold_ranges: Vec<(BufferRow, Range, Color)>, + selections: Vec<(SelectionStyle, Vec)>, + scrollbar_row_range: Range, + show_scrollbars: bool, + is_singleton: bool, + max_row: u32, + context_menu: Option<(DisplayPoint, AnyElement)>, + code_actions_indicator: Option<(u32, AnyElement)>, + hover_popovers: Option<(DisplayPoint, Vec>)>, + fold_indicators: Vec>>, + tab_invisible: Line, + space_invisible: Line, +} + +struct PositionMap { + size: Vector2F, + line_height: f32, + scroll_max: Vector2F, + em_width: f32, + em_advance: f32, + line_layouts: Vec, + snapshot: EditorSnapshot, +} + +#[derive(Debug, Copy, Clone)] +pub struct PointForPosition { + pub previous_valid: DisplayPoint, + pub next_valid: DisplayPoint, + pub exact_unclipped: DisplayPoint, + pub column_overshoot_after_line_end: u32, +} + +impl PointForPosition { + #[cfg(test)] + pub fn valid(valid: DisplayPoint) -> Self { + Self { + previous_valid: valid, + next_valid: valid, + exact_unclipped: valid, + column_overshoot_after_line_end: 0, + } + } + + pub fn as_valid(&self) -> Option { + if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped { + Some(self.previous_valid) + } else { + None + } + } +} + +impl PositionMap { + fn point_for_position(&self, text_bounds: RectF, position: Vector2F) -> PointForPosition { + let scroll_position = self.snapshot.scroll_position(); + let position = position - text_bounds.origin(); + let y = position.y().max(0.0).min(self.size.y()); + let x = position.x() + (scroll_position.x() * self.em_width); + let row = (y / self.line_height + scroll_position.y()) as u32; + let (column, x_overshoot_after_line_end) = if let Some(line) = self + .line_layouts + .get(row as usize - scroll_position.y() as usize) + .map(|line_with_spaces| &line_with_spaces.line) + { + if let Some(ix) = line.index_for_x(x) { + (ix as u32, 0.0) + } else { + (line.len() as u32, 0f32.max(x - line.width())) + } + } else { + (0, x) + }; + + let mut exact_unclipped = DisplayPoint::new(row, column); + let previous_valid = self.snapshot.clip_point(exact_unclipped, Bias::Left); + let next_valid = self.snapshot.clip_point(exact_unclipped, Bias::Right); + + let column_overshoot_after_line_end = (x_overshoot_after_line_end / self.em_advance) as u32; + *exact_unclipped.column_mut() += column_overshoot_after_line_end; + PointForPosition { + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end, + } + } +} + +struct BlockLayout { + row: u32, + element: AnyElement, + style: BlockStyle, +} + +fn layout_line( + row: u32, + snapshot: &EditorSnapshot, + style: &EditorStyle, + layout_cache: &TextLayoutCache, +) -> text_layout::Line { + let mut line = snapshot.line(row); + + if line.len() > MAX_LINE_LEN { + let mut len = MAX_LINE_LEN; + while !line.is_char_boundary(len) { + len -= 1; + } + + line.truncate(len); + } + + layout_cache.layout_str( + &line, + style.text.font_size, + &[( + snapshot.line_len(row) as usize, + RunStyle { + font_id: style.text.font_id, + color: Color::black(), + underline: Default::default(), + }, + )], + ) +} + +#[derive(Debug)] +pub struct Cursor { + origin: Vector2F, + block_width: f32, + line_height: f32, + color: Color, + shape: CursorShape, + block_text: Option, +} + +impl Cursor { + pub fn new( + origin: Vector2F, + block_width: f32, + line_height: f32, + color: Color, + shape: CursorShape, + block_text: Option, + ) -> Cursor { + Cursor { + origin, + block_width, + line_height, + color, + shape, + block_text, + } + } + + pub fn bounding_rect(&self, origin: Vector2F) -> RectF { + RectF::new( + self.origin + origin, + vec2f(self.block_width, self.line_height), + ) + } + + pub fn paint(&self, origin: Vector2F, cx: &mut WindowContext) { + let bounds = match self.shape { + CursorShape::Bar => RectF::new(self.origin + origin, vec2f(2.0, self.line_height)), + CursorShape::Block | CursorShape::Hollow => RectF::new( + self.origin + origin, + vec2f(self.block_width, self.line_height), + ), + CursorShape::Underscore => RectF::new( + self.origin + origin + Vector2F::new(0.0, self.line_height - 2.0), + vec2f(self.block_width, 2.0), + ), + }; + + //Draw background or border quad + if matches!(self.shape, CursorShape::Hollow) { + cx.scene().push_quad(Quad { + bounds, + background: None, + border: Border::all(1., self.color).into(), + corner_radii: Default::default(), + }); + } else { + cx.scene().push_quad(Quad { + bounds, + background: Some(self.color), + border: Default::default(), + corner_radii: Default::default(), + }); + } + + if let Some(block_text) = &self.block_text { + block_text.paint(self.origin + origin, bounds, self.line_height, cx); + } + } + + pub fn shape(&self) -> CursorShape { + self.shape + } +} + +#[derive(Debug)] +pub struct HighlightedRange { + pub start_y: f32, + pub line_height: f32, + pub lines: Vec, + pub color: Color, + pub corner_radius: f32, +} + +#[derive(Debug)] +pub struct HighlightedRangeLine { + pub start_x: f32, + pub end_x: f32, +} + +impl HighlightedRange { + pub fn paint(&self, bounds: RectF, cx: &mut WindowContext) { + if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x { + self.paint_lines(self.start_y, &self.lines[0..1], bounds, cx); + self.paint_lines( + self.start_y + self.line_height, + &self.lines[1..], + bounds, + cx, + ); + } else { + self.paint_lines(self.start_y, &self.lines, bounds, cx); + } + } + + fn paint_lines( + &self, + start_y: f32, + lines: &[HighlightedRangeLine], + bounds: RectF, + cx: &mut WindowContext, + ) { + if lines.is_empty() { + return; + } + + let mut path = PathBuilder::new(); + let first_line = lines.first().unwrap(); + let last_line = lines.last().unwrap(); + + let first_top_left = vec2f(first_line.start_x, start_y); + let first_top_right = vec2f(first_line.end_x, start_y); + + let curve_height = vec2f(0., self.corner_radius); + let curve_width = |start_x: f32, end_x: f32| { + let max = (end_x - start_x) / 2.; + let width = if max < self.corner_radius { + max + } else { + self.corner_radius + }; + + vec2f(width, 0.) + }; + + let top_curve_width = curve_width(first_line.start_x, first_line.end_x); + path.reset(first_top_right - top_curve_width); + path.curve_to(first_top_right + curve_height, first_top_right); + + let mut iter = lines.iter().enumerate().peekable(); + while let Some((ix, line)) = iter.next() { + let bottom_right = vec2f(line.end_x, start_y + (ix + 1) as f32 * self.line_height); + + if let Some((_, next_line)) = iter.peek() { + let next_top_right = vec2f(next_line.end_x, bottom_right.y()); + + match next_top_right.x().partial_cmp(&bottom_right.x()).unwrap() { + Ordering::Equal => { + path.line_to(bottom_right); + } + Ordering::Less => { + let curve_width = curve_width(next_top_right.x(), bottom_right.x()); + path.line_to(bottom_right - curve_height); + if self.corner_radius > 0. { + path.curve_to(bottom_right - curve_width, bottom_right); + } + path.line_to(next_top_right + curve_width); + if self.corner_radius > 0. { + path.curve_to(next_top_right + curve_height, next_top_right); + } + } + Ordering::Greater => { + let curve_width = curve_width(bottom_right.x(), next_top_right.x()); + path.line_to(bottom_right - curve_height); + if self.corner_radius > 0. { + path.curve_to(bottom_right + curve_width, bottom_right); + } + path.line_to(next_top_right - curve_width); + if self.corner_radius > 0. { + path.curve_to(next_top_right + curve_height, next_top_right); + } + } + } + } else { + let curve_width = curve_width(line.start_x, line.end_x); + path.line_to(bottom_right - curve_height); + if self.corner_radius > 0. { + path.curve_to(bottom_right - curve_width, bottom_right); + } + + let bottom_left = vec2f(line.start_x, bottom_right.y()); + path.line_to(bottom_left + curve_width); + if self.corner_radius > 0. { + path.curve_to(bottom_left - curve_height, bottom_left); + } + } + } + + if first_line.start_x > last_line.start_x { + let curve_width = curve_width(last_line.start_x, first_line.start_x); + let second_top_left = vec2f(last_line.start_x, start_y + self.line_height); + path.line_to(second_top_left + curve_height); + if self.corner_radius > 0. { + path.curve_to(second_top_left + curve_width, second_top_left); + } + let first_bottom_left = vec2f(first_line.start_x, second_top_left.y()); + path.line_to(first_bottom_left - curve_width); + if self.corner_radius > 0. { + path.curve_to(first_bottom_left - curve_height, first_bottom_left); + } + } + + path.line_to(first_top_left + curve_height); + if self.corner_radius > 0. { + path.curve_to(first_top_left + top_curve_width, first_top_left); + } + path.line_to(first_top_right - top_curve_width); + + cx.scene().push_path(path.build(self.color, Some(bounds))); + } +} + +fn range_to_bounds( + range: &Range, + content_origin: Vector2F, + scroll_left: f32, + scroll_top: f32, + visible_row_range: &Range, + line_end_overshoot: f32, + position_map: &PositionMap, +) -> impl Iterator { + let mut bounds: SmallVec<[RectF; 1]> = SmallVec::new(); + + if range.start == range.end { + return bounds.into_iter(); + } + + let start_row = visible_row_range.start; + let end_row = visible_row_range.end; + + let row_range = if range.end.column() == 0 { + cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row) + } else { + cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row) + }; + + let first_y = + content_origin.y() + row_range.start as f32 * position_map.line_height - scroll_top; + + for (idx, row) in row_range.enumerate() { + let line_layout = &position_map.line_layouts[(row - start_row) as usize].line; + + let start_x = if row == range.start.row() { + content_origin.x() + line_layout.x_for_index(range.start.column() as usize) + - scroll_left + } else { + content_origin.x() - scroll_left + }; + + let end_x = if row == range.end.row() { + content_origin.x() + line_layout.x_for_index(range.end.column() as usize) - scroll_left + } else { + content_origin.x() + line_layout.width() + line_end_overshoot - scroll_left + }; + + bounds.push(RectF::from_points( + vec2f(start_x, first_y + position_map.line_height * idx as f32), + vec2f(end_x, first_y + position_map.line_height * (idx + 1) as f32), + )) + } + + bounds.into_iter() +} + +pub fn scale_vertical_mouse_autoscroll_delta(delta: f32) -> f32 { + delta.powf(1.5) / 100.0 +} + +fn scale_horizontal_mouse_autoscroll_delta(delta: f32) -> f32 { + delta.powf(1.2) / 300.0 +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + display_map::{BlockDisposition, BlockProperties}, + editor_tests::{init_test, update_test_language_settings}, + Editor, MultiBuffer, + }; + use gpui::TestAppContext; + use language::language_settings; + use log::info; + use std::{num::NonZeroU32, sync::Arc}; + use util::test::sample_text; + + #[gpui::test] + fn test_layout_line_numbers(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); + Editor::new(EditorMode::Full, buffer, None, None, cx) + }) + .root(cx); + let element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); + + let layouts = editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + element + .layout_line_numbers( + 0..6, + &Default::default(), + DisplayPoint::new(0, 0), + false, + &snapshot, + cx, + ) + .0 + }); + assert_eq!(layouts.len(), 6); + + let relative_rows = editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + element.calculate_relative_line_numbers(&snapshot, &(0..6), Some(3)) + }); + assert_eq!(relative_rows[&0], 3); + assert_eq!(relative_rows[&1], 2); + assert_eq!(relative_rows[&2], 1); + // current line has no relative number + assert_eq!(relative_rows[&4], 1); + assert_eq!(relative_rows[&5], 2); + + // works if cursor is before screen + let relative_rows = editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + + element.calculate_relative_line_numbers(&snapshot, &(3..6), Some(1)) + }); + assert_eq!(relative_rows.len(), 3); + assert_eq!(relative_rows[&3], 2); + assert_eq!(relative_rows[&4], 3); + assert_eq!(relative_rows[&5], 4); + + // works if cursor is after screen + let relative_rows = editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + + element.calculate_relative_line_numbers(&snapshot, &(0..3), Some(6)) + }); + assert_eq!(relative_rows.len(), 3); + assert_eq!(relative_rows[&0], 5); + assert_eq!(relative_rows[&1], 4); + assert_eq!(relative_rows[&2], 3); + } + + #[gpui::test] + async fn test_vim_visual_selections(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx); + Editor::new(EditorMode::Full, buffer, None, None, cx) + }) + .root(cx); + let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); + let (_, state) = editor.update(cx, |editor, cx| { + editor.cursor_shape = CursorShape::Block; + editor.change_selections(None, cx, |s| { + s.select_ranges([ + Point::new(0, 0)..Point::new(1, 0), + Point::new(3, 2)..Point::new(3, 3), + Point::new(5, 6)..Point::new(6, 0), + ]); + }); + element.layout( + SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)), + editor, + cx, + ) + }); + assert_eq!(state.selections.len(), 1); + let local_selections = &state.selections[0].1; + assert_eq!(local_selections.len(), 3); + // moves cursor back one line + assert_eq!(local_selections[0].head, DisplayPoint::new(0, 6)); + assert_eq!( + local_selections[0].range, + DisplayPoint::new(0, 0)..DisplayPoint::new(1, 0) + ); + + // moves cursor back one column + assert_eq!( + local_selections[1].range, + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 3) + ); + assert_eq!(local_selections[1].head, DisplayPoint::new(3, 2)); + + // leaves cursor on the max point + assert_eq!( + local_selections[2].range, + DisplayPoint::new(5, 6)..DisplayPoint::new(6, 0) + ); + assert_eq!(local_selections[2].head, DisplayPoint::new(6, 0)); + + // active lines does not include 1 (even though the range of the selection does) + assert_eq!( + state.active_rows.keys().cloned().collect::>(), + vec![0, 3, 5, 6] + ); + + // multi-buffer support + // in DisplayPoint co-ordinates, this is what we're dealing with: + // 0: [[file + // 1: header]] + // 2: aaaaaa + // 3: bbbbbb + // 4: cccccc + // 5: + // 6: ... + // 7: ffffff + // 8: gggggg + // 9: hhhhhh + // 10: + // 11: [[file + // 12: header]] + // 13: bbbbbb + // 14: cccccc + // 15: dddddd + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_multi( + [ + ( + &(sample_text(8, 6, 'a') + "\n"), + vec![ + Point::new(0, 0)..Point::new(3, 0), + Point::new(4, 0)..Point::new(7, 0), + ], + ), + ( + &(sample_text(8, 6, 'a') + "\n"), + vec![Point::new(1, 0)..Point::new(3, 0)], + ), + ], + cx, + ); + Editor::new(EditorMode::Full, buffer, None, None, cx) + }) + .root(cx); + let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); + let (_, state) = editor.update(cx, |editor, cx| { + editor.cursor_shape = CursorShape::Block; + editor.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(4, 0)..DisplayPoint::new(7, 0), + DisplayPoint::new(10, 0)..DisplayPoint::new(13, 0), + ]); + }); + element.layout( + SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)), + editor, + cx, + ) + }); + + assert_eq!(state.selections.len(), 1); + let local_selections = &state.selections[0].1; + assert_eq!(local_selections.len(), 2); + + // moves cursor on excerpt boundary back a line + // and doesn't allow selection to bleed through + assert_eq!( + local_selections[0].range, + DisplayPoint::new(4, 0)..DisplayPoint::new(6, 0) + ); + assert_eq!(local_selections[0].head, DisplayPoint::new(5, 0)); + + // moves cursor on buffer boundary back two lines + // and doesn't allow selection to bleed through + assert_eq!( + local_selections[1].range, + DisplayPoint::new(10, 0)..DisplayPoint::new(11, 0) + ); + assert_eq!(local_selections[1].head, DisplayPoint::new(10, 0)); + } + + #[gpui::test] + fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("", cx); + Editor::new(EditorMode::Full, buffer, None, None, cx) + }) + .root(cx); + + editor.update(cx, |editor, cx| { + editor.set_placeholder_text("hello", cx); + editor.insert_blocks( + [BlockProperties { + style: BlockStyle::Fixed, + disposition: BlockDisposition::Above, + height: 3, + position: Anchor::min(), + render: Arc::new(|_| Empty::new().into_any()), + }], + None, + cx, + ); + + // Blur the editor so that it displays placeholder text. + cx.blur(); + }); + + let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); + let (size, mut state) = editor.update(cx, |editor, cx| { + element.layout( + SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)), + editor, + cx, + ) + }); + + assert_eq!(state.position_map.line_layouts.len(), 4); + assert_eq!( + state + .line_number_layouts + .iter() + .map(Option::is_some) + .collect::>(), + &[false, false, false, true] + ); + + // Don't panic. + let bounds = RectF::new(Default::default(), size); + editor.update(cx, |editor, cx| { + element.paint(bounds, bounds, &mut state, editor, cx); + }); + } + + #[gpui::test] + fn test_all_invisibles_drawing(cx: &mut TestAppContext) { + const TAB_SIZE: u32 = 4; + + let input_text = "\t \t|\t| a b"; + let expected_invisibles = vec![ + Invisible::Tab { + line_start_offset: 0, + }, + Invisible::Whitespace { + line_offset: TAB_SIZE as usize, + }, + Invisible::Tab { + line_start_offset: TAB_SIZE as usize + 1, + }, + Invisible::Tab { + line_start_offset: TAB_SIZE as usize * 2 + 1, + }, + Invisible::Whitespace { + line_offset: TAB_SIZE as usize * 3 + 1, + }, + Invisible::Whitespace { + line_offset: TAB_SIZE as usize * 3 + 3, + }, + ]; + assert_eq!( + expected_invisibles.len(), + input_text + .chars() + .filter(|initial_char| initial_char.is_whitespace()) + .count(), + "Hardcoded expected invisibles differ from the actual ones in '{input_text}'" + ); + + init_test(cx, |s| { + s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All); + s.defaults.tab_size = NonZeroU32::new(TAB_SIZE); + }); + + let actual_invisibles = + collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, 500.0); + + assert_eq!(expected_invisibles, actual_invisibles); + } + + #[gpui::test] + fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) { + init_test(cx, |s| { + s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All); + s.defaults.tab_size = NonZeroU32::new(4); + }); + + for editor_mode_without_invisibles in [ + EditorMode::SingleLine, + EditorMode::AutoHeight { max_lines: 100 }, + ] { + let invisibles = collect_invisibles_from_new_editor( + cx, + editor_mode_without_invisibles, + "\t\t\t| | a b", + 500.0, + ); + assert!(invisibles.is_empty(), + "For editor mode {editor_mode_without_invisibles:?} no invisibles was expected but got {invisibles:?}"); + } + } + + #[gpui::test] + fn test_wrapped_invisibles_drawing(cx: &mut TestAppContext) { + let tab_size = 4; + let input_text = "a\tbcd ".repeat(9); + let repeated_invisibles = [ + Invisible::Tab { + line_start_offset: 1, + }, + Invisible::Whitespace { + line_offset: tab_size as usize + 3, + }, + Invisible::Whitespace { + line_offset: tab_size as usize + 4, + }, + Invisible::Whitespace { + line_offset: tab_size as usize + 5, + }, + ]; + let expected_invisibles = std::iter::once(repeated_invisibles) + .cycle() + .take(9) + .flatten() + .collect::>(); + assert_eq!( + expected_invisibles.len(), + input_text + .chars() + .filter(|initial_char| initial_char.is_whitespace()) + .count(), + "Hardcoded expected invisibles differ from the actual ones in '{input_text}'" + ); + info!("Expected invisibles: {expected_invisibles:?}"); + + init_test(cx, |_| {}); + + // Put the same string with repeating whitespace pattern into editors of various size, + // take deliberately small steps during resizing, to put all whitespace kinds near the wrap point. + let resize_step = 10.0; + let mut editor_width = 200.0; + while editor_width <= 1000.0 { + update_test_language_settings(cx, |s| { + s.defaults.tab_size = NonZeroU32::new(tab_size); + s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All); + s.defaults.preferred_line_length = Some(editor_width as u32); + s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength); + }); + + let actual_invisibles = + collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, editor_width); + + // Whatever the editor size is, ensure it has the same invisible kinds in the same order + // (no good guarantees about the offsets: wrapping could trigger padding and its tests should check the offsets). + let mut i = 0; + for (actual_index, actual_invisible) in actual_invisibles.iter().enumerate() { + i = actual_index; + match expected_invisibles.get(i) { + Some(expected_invisible) => match (expected_invisible, actual_invisible) { + (Invisible::Whitespace { .. }, Invisible::Whitespace { .. }) + | (Invisible::Tab { .. }, Invisible::Tab { .. }) => {} + _ => { + panic!("At index {i}, expected invisible {expected_invisible:?} does not match actual {actual_invisible:?} by kind. Actual invisibles: {actual_invisibles:?}") + } + }, + None => panic!("Unexpected extra invisible {actual_invisible:?} at index {i}"), + } + } + let missing_expected_invisibles = &expected_invisibles[i + 1..]; + assert!( + missing_expected_invisibles.is_empty(), + "Missing expected invisibles after index {i}: {missing_expected_invisibles:?}" + ); + + editor_width += resize_step; + } + } + + fn collect_invisibles_from_new_editor( + cx: &mut TestAppContext, + editor_mode: EditorMode, + input_text: &str, + editor_width: f32, + ) -> Vec { + info!( + "Creating editor with mode {editor_mode:?}, width {editor_width} and text '{input_text}'" + ); + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple(&input_text, cx); + Editor::new(editor_mode, buffer, None, None, cx) + }) + .root(cx); + + let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); + let (_, layout_state) = editor.update(cx, |editor, cx| { + editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx); + editor.set_wrap_width(Some(editor_width), cx); + + element.layout( + SizeConstraint::new(vec2f(editor_width, 500.), vec2f(editor_width, 500.)), + editor, + cx, + ) + }); + + layout_state + .position_map + .line_layouts + .iter() + .map(|line_with_invisibles| &line_with_invisibles.invisibles) + .flatten() + .cloned() + .collect() + } +} diff --git a/crates/editor2/src/git.rs b/crates/editor2/src/git.rs new file mode 100644 index 0000000000..f8c6ef9a1f --- /dev/null +++ b/crates/editor2/src/git.rs @@ -0,0 +1,282 @@ +use std::ops::Range; + +use git::diff::{DiffHunk, DiffHunkStatus}; +use language::Point; + +use crate::{ + display_map::{DisplaySnapshot, ToDisplayPoint}, + AnchorRangeExt, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DisplayDiffHunk { + Folded { + display_row: u32, + }, + + Unfolded { + display_row_range: Range, + status: DiffHunkStatus, + }, +} + +impl DisplayDiffHunk { + pub fn start_display_row(&self) -> u32 { + match self { + &DisplayDiffHunk::Folded { display_row } => display_row, + DisplayDiffHunk::Unfolded { + display_row_range, .. + } => display_row_range.start, + } + } + + pub fn contains_display_row(&self, display_row: u32) -> bool { + let range = match self { + &DisplayDiffHunk::Folded { display_row } => display_row..=display_row, + + DisplayDiffHunk::Unfolded { + display_row_range, .. + } => display_row_range.start..=display_row_range.end, + }; + + range.contains(&display_row) + } +} + +pub fn diff_hunk_to_display(hunk: DiffHunk, snapshot: &DisplaySnapshot) -> DisplayDiffHunk { + let hunk_start_point = Point::new(hunk.buffer_range.start, 0); + let hunk_start_point_sub = Point::new(hunk.buffer_range.start.saturating_sub(1), 0); + let hunk_end_point_sub = Point::new( + hunk.buffer_range + .end + .saturating_sub(1) + .max(hunk.buffer_range.start), + 0, + ); + + let is_removal = hunk.status() == DiffHunkStatus::Removed; + + let folds_start = Point::new(hunk.buffer_range.start.saturating_sub(2), 0); + let folds_end = Point::new(hunk.buffer_range.end + 2, 0); + let folds_range = folds_start..folds_end; + + let containing_fold = snapshot.folds_in_range(folds_range).find(|fold_range| { + let fold_point_range = fold_range.to_point(&snapshot.buffer_snapshot); + let fold_point_range = fold_point_range.start..=fold_point_range.end; + + let folded_start = fold_point_range.contains(&hunk_start_point); + let folded_end = fold_point_range.contains(&hunk_end_point_sub); + let folded_start_sub = fold_point_range.contains(&hunk_start_point_sub); + + (folded_start && folded_end) || (is_removal && folded_start_sub) + }); + + if let Some(fold) = containing_fold { + let row = fold.start.to_display_point(snapshot).row(); + DisplayDiffHunk::Folded { display_row: row } + } else { + let start = hunk_start_point.to_display_point(snapshot).row(); + + let hunk_end_row = hunk.buffer_range.end.max(hunk.buffer_range.start); + let hunk_end_point = Point::new(hunk_end_row, 0); + let end = hunk_end_point.to_display_point(snapshot).row(); + + DisplayDiffHunk::Unfolded { + display_row_range: start..end, + status: hunk.status(), + } + } +} + +#[cfg(any(test, feature = "test_support"))] +mod tests { + use crate::editor_tests::init_test; + use crate::Point; + use gpui::TestAppContext; + use multi_buffer::{ExcerptRange, MultiBuffer}; + use project::{FakeFs, Project}; + use unindent::Unindent; + #[gpui::test] + async fn test_diff_hunks_in_range(cx: &mut TestAppContext) { + use git::diff::DiffHunkStatus; + init_test(cx, |_| {}); + + let fs = FakeFs::new(cx.background()); + let project = Project::test(fs, [], cx).await; + + // buffer has two modified hunks with two rows each + let buffer_1 = project + .update(cx, |project, cx| { + project.create_buffer( + " + 1.zero + 1.ONE + 1.TWO + 1.three + 1.FOUR + 1.FIVE + 1.six + " + .unindent() + .as_str(), + None, + cx, + ) + }) + .unwrap(); + buffer_1.update(cx, |buffer, cx| { + buffer.set_diff_base( + Some( + " + 1.zero + 1.one + 1.two + 1.three + 1.four + 1.five + 1.six + " + .unindent(), + ), + cx, + ); + }); + + // buffer has a deletion hunk and an insertion hunk + let buffer_2 = project + .update(cx, |project, cx| { + project.create_buffer( + " + 2.zero + 2.one + 2.two + 2.three + 2.four + 2.five + 2.six + " + .unindent() + .as_str(), + None, + cx, + ) + }) + .unwrap(); + buffer_2.update(cx, |buffer, cx| { + buffer.set_diff_base( + Some( + " + 2.zero + 2.one + 2.one-and-a-half + 2.two + 2.three + 2.four + 2.six + " + .unindent(), + ), + cx, + ); + }); + + cx.foreground().run_until_parked(); + + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + buffer_1.clone(), + [ + // excerpt ends in the middle of a modified hunk + ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 5), + primary: Default::default(), + }, + // excerpt begins in the middle of a modified hunk + ExcerptRange { + context: Point::new(5, 0)..Point::new(6, 5), + primary: Default::default(), + }, + ], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ + // excerpt ends at a deletion + ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 5), + primary: Default::default(), + }, + // excerpt starts at a deletion + ExcerptRange { + context: Point::new(2, 0)..Point::new(2, 5), + primary: Default::default(), + }, + // excerpt fully contains a deletion hunk + ExcerptRange { + context: Point::new(1, 0)..Point::new(2, 5), + primary: Default::default(), + }, + // excerpt fully contains an insertion hunk + ExcerptRange { + context: Point::new(4, 0)..Point::new(6, 5), + primary: Default::default(), + }, + ], + cx, + ); + multibuffer + }); + + let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx)); + + assert_eq!( + snapshot.text(), + " + 1.zero + 1.ONE + 1.FIVE + 1.six + 2.zero + 2.one + 2.two + 2.one + 2.two + 2.four + 2.five + 2.six" + .unindent() + ); + + let expected = [ + (DiffHunkStatus::Modified, 1..2), + (DiffHunkStatus::Modified, 2..3), + //TODO: Define better when and where removed hunks show up at range extremities + (DiffHunkStatus::Removed, 6..6), + (DiffHunkStatus::Removed, 8..8), + (DiffHunkStatus::Added, 10..11), + ]; + + assert_eq!( + snapshot + .git_diff_hunks_in_range(0..12) + .map(|hunk| (hunk.status(), hunk.buffer_range)) + .collect::>(), + &expected, + ); + + assert_eq!( + snapshot + .git_diff_hunks_in_range_rev(0..12) + .map(|hunk| (hunk.status(), hunk.buffer_range)) + .collect::>(), + expected + .iter() + .rev() + .cloned() + .collect::>() + .as_slice(), + ); + } +} diff --git a/crates/editor2/src/highlight_matching_bracket.rs b/crates/editor2/src/highlight_matching_bracket.rs new file mode 100644 index 0000000000..a0baf6882f --- /dev/null +++ b/crates/editor2/src/highlight_matching_bracket.rs @@ -0,0 +1,138 @@ +use gpui::ViewContext; + +use crate::{Editor, RangeToAnchorExt}; + +enum MatchingBracketHighlight {} + +pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewContext) { + editor.clear_background_highlights::(cx); + + let newest_selection = editor.selections.newest::(cx); + // Don't highlight brackets if the selection isn't empty + if !newest_selection.is_empty() { + return; + } + + let head = newest_selection.head(); + let snapshot = editor.snapshot(cx); + if let Some((opening_range, closing_range)) = snapshot + .buffer_snapshot + .innermost_enclosing_bracket_ranges(head..head) + { + editor.highlight_background::( + vec![ + opening_range.to_anchors(&snapshot.buffer_snapshot), + closing_range.to_anchors(&snapshot.buffer_snapshot), + ], + |theme| theme.editor.document_highlight_read_background, + cx, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext}; + use indoc::indoc; + use language::{BracketPair, BracketPairConfig, Language, LanguageConfig}; + + #[gpui::test] + async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new( + Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: false, + newline: true, + }, + BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: false, + newline: true, + }, + ], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_brackets_query(indoc! {r#" + ("{" @open "}" @close) + ("(" @open ")" @close) + "#}) + .unwrap(), + Default::default(), + cx, + ) + .await; + + // positioning cursor inside bracket highlights both + cx.set_state(indoc! {r#" + pub fn test("Test ˇargument") { + another_test(1, 2, 3); + } + "#}); + cx.assert_editor_background_highlights::(indoc! {r#" + pub fn test«(»"Test argument"«)» { + another_test(1, 2, 3); + } + "#}); + + cx.set_state(indoc! {r#" + pub fn test("Test argument") { + another_test(1, ˇ2, 3); + } + "#}); + cx.assert_editor_background_highlights::(indoc! {r#" + pub fn test("Test argument") { + another_test«(»1, 2, 3«)»; + } + "#}); + + cx.set_state(indoc! {r#" + pub fn test("Test argument") { + anotherˇ_test(1, 2, 3); + } + "#}); + cx.assert_editor_background_highlights::(indoc! {r#" + pub fn test("Test argument") «{» + another_test(1, 2, 3); + «}» + "#}); + + // positioning outside of brackets removes highlight + cx.set_state(indoc! {r#" + pub fˇn test("Test argument") { + another_test(1, 2, 3); + } + "#}); + cx.assert_editor_background_highlights::(indoc! {r#" + pub fn test("Test argument") { + another_test(1, 2, 3); + } + "#}); + + // non empty selection dismisses highlight + cx.set_state(indoc! {r#" + pub fn test("Te«st argˇ»ument") { + another_test(1, 2, 3); + } + "#}); + cx.assert_editor_background_highlights::(indoc! {r#" + pub fn test("Test argument") { + another_test(1, 2, 3); + } + "#}); + } +} diff --git a/crates/editor2/src/hover_popover.rs b/crates/editor2/src/hover_popover.rs new file mode 100644 index 0000000000..5b3985edf9 --- /dev/null +++ b/crates/editor2/src/hover_popover.rs @@ -0,0 +1,1329 @@ +use crate::{ + display_map::{InlayOffset, ToDisplayPoint}, + link_go_to_definition::{InlayHighlight, RangeInEditor}, + Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle, + ExcerptId, RangeToAnchorExt, +}; +use futures::FutureExt; +use gpui::{ + actions, + elements::{Flex, MouseEventHandler, Padding, ParentElement, Text}, + platform::{CursorStyle, MouseButton}, + AnyElement, AppContext, Element, ModelHandle, Task, ViewContext, WeakViewHandle, +}; +use language::{ + markdown, Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, ParsedMarkdown, +}; +use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; +use std::{ops::Range, sync::Arc, time::Duration}; +use util::TryFutureExt; +use workspace::Workspace; + +pub const HOVER_DELAY_MILLIS: u64 = 350; +pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200; + +pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.; +pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.; +pub const HOVER_POPOVER_GAP: f32 = 10.; + +actions!(editor, [Hover]); + +pub fn init(cx: &mut AppContext) { + cx.add_action(hover); +} + +/// Bindable action which uses the most recent selection head to trigger a hover +pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext) { + let head = editor.selections.newest_display(cx).head(); + show_hover(editor, head, true, cx); +} + +/// The internal hover action dispatches between `show_hover` or `hide_hover` +/// depending on whether a point to hover over is provided. +pub fn hover_at(editor: &mut Editor, point: Option, cx: &mut ViewContext) { + if settings::get::(cx).hover_popover_enabled { + if let Some(point) = point { + show_hover(editor, point, false, cx); + } else { + hide_hover(editor, cx); + } + } +} + +pub struct InlayHover { + pub excerpt: ExcerptId, + pub range: InlayHighlight, + pub tooltip: HoverBlock, +} + +pub fn find_hovered_hint_part( + label_parts: Vec, + hint_start: InlayOffset, + hovered_offset: InlayOffset, +) -> Option<(InlayHintLabelPart, Range)> { + if hovered_offset >= hint_start { + let mut hovered_character = (hovered_offset - hint_start).0; + let mut part_start = hint_start; + for part in label_parts { + let part_len = part.value.chars().count(); + if hovered_character > part_len { + hovered_character -= part_len; + part_start.0 += part_len; + } else { + let part_end = InlayOffset(part_start.0 + part_len); + return Some((part, part_start..part_end)); + } + } + } + None +} + +pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext) { + if settings::get::(cx).hover_popover_enabled { + if editor.pending_rename.is_some() { + return; + } + + let Some(project) = editor.project.clone() else { + return; + }; + + if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { + if let RangeInEditor::Inlay(range) = symbol_range { + if range == &inlay_hover.range { + // Hover triggered from same location as last time. Don't show again. + return; + } + } + hide_hover(editor, cx); + } + + let task = cx.spawn(|this, mut cx| { + async move { + cx.background() + .timer(Duration::from_millis(HOVER_DELAY_MILLIS)) + .await; + this.update(&mut cx, |this, _| { + this.hover_state.diagnostic_popover = None; + })?; + + let language_registry = project.update(&mut cx, |p, _| p.languages().clone()); + let blocks = vec![inlay_hover.tooltip]; + let parsed_content = parse_blocks(&blocks, &language_registry, None).await; + + let hover_popover = InfoPopover { + project: project.clone(), + symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()), + blocks, + parsed_content, + }; + + this.update(&mut cx, |this, cx| { + // Highlight the selected symbol using a background highlight + this.highlight_inlay_background::( + vec![inlay_hover.range], + |theme| theme.editor.hover_popover.highlight, + cx, + ); + this.hover_state.info_popover = Some(hover_popover); + cx.notify(); + })?; + + anyhow::Ok(()) + } + .log_err() + }); + + editor.hover_state.info_task = Some(task); + } +} + +/// Hides the type information popup. +/// Triggered by the `Hover` action when the cursor is not over a symbol or when the +/// selections changed. +pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext) -> bool { + let did_hide = editor.hover_state.info_popover.take().is_some() + | editor.hover_state.diagnostic_popover.take().is_some(); + + editor.hover_state.info_task = None; + editor.hover_state.triggered_from = None; + + editor.clear_background_highlights::(cx); + + if did_hide { + cx.notify(); + } + + did_hide +} + +/// Queries the LSP and shows type info and documentation +/// about the symbol the mouse is currently hovering over. +/// Triggered by the `Hover` action when the cursor may be over a symbol. +fn show_hover( + editor: &mut Editor, + point: DisplayPoint, + ignore_timeout: bool, + cx: &mut ViewContext, +) { + if editor.pending_rename.is_some() { + return; + } + + let snapshot = editor.snapshot(cx); + let multibuffer_offset = point.to_offset(&snapshot.display_snapshot, Bias::Left); + + let (buffer, buffer_position) = if let Some(output) = editor + .buffer + .read(cx) + .text_anchor_for_position(multibuffer_offset, cx) + { + output + } else { + return; + }; + + let excerpt_id = if let Some((excerpt_id, _, _)) = editor + .buffer() + .read(cx) + .excerpt_containing(multibuffer_offset, cx) + { + excerpt_id + } else { + return; + }; + + let project = if let Some(project) = editor.project.clone() { + project + } else { + return; + }; + + if !ignore_timeout { + if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { + if symbol_range + .as_text_range() + .map(|range| { + range + .to_offset(&snapshot.buffer_snapshot) + .contains(&multibuffer_offset) + }) + .unwrap_or(false) + { + // Hover triggered from same location as last time. Don't show again. + return; + } else { + hide_hover(editor, cx); + } + } + } + + // Get input anchor + let anchor = snapshot + .buffer_snapshot + .anchor_at(multibuffer_offset, Bias::Left); + + // Don't request again if the location is the same as the previous request + if let Some(triggered_from) = &editor.hover_state.triggered_from { + if triggered_from + .cmp(&anchor, &snapshot.buffer_snapshot) + .is_eq() + { + return; + } + } + + let task = cx.spawn(|this, mut cx| { + async move { + // If we need to delay, delay a set amount initially before making the lsp request + let delay = if !ignore_timeout { + // Construct delay task to wait for later + let total_delay = Some( + cx.background() + .timer(Duration::from_millis(HOVER_DELAY_MILLIS)), + ); + + cx.background() + .timer(Duration::from_millis(HOVER_REQUEST_DELAY_MILLIS)) + .await; + total_delay + } else { + None + }; + + // query the LSP for hover info + let hover_request = cx.update(|cx| { + project.update(cx, |project, cx| { + project.hover(&buffer, buffer_position, cx) + }) + }); + + if let Some(delay) = delay { + delay.await; + } + + // If there's a diagnostic, assign it on the hover state and notify + let local_diagnostic = snapshot + .buffer_snapshot + .diagnostics_in_range::<_, usize>(multibuffer_offset..multibuffer_offset, false) + // Find the entry with the most specific range + .min_by_key(|entry| entry.range.end - entry.range.start) + .map(|entry| DiagnosticEntry { + diagnostic: entry.diagnostic, + range: entry.range.to_anchors(&snapshot.buffer_snapshot), + }); + + // Pull the primary diagnostic out so we can jump to it if the popover is clicked + let primary_diagnostic = local_diagnostic.as_ref().and_then(|local_diagnostic| { + snapshot + .buffer_snapshot + .diagnostic_group::(local_diagnostic.diagnostic.group_id) + .find(|diagnostic| diagnostic.diagnostic.is_primary) + .map(|entry| DiagnosticEntry { + diagnostic: entry.diagnostic, + range: entry.range.to_anchors(&snapshot.buffer_snapshot), + }) + }); + + this.update(&mut cx, |this, _| { + this.hover_state.diagnostic_popover = + local_diagnostic.map(|local_diagnostic| DiagnosticPopover { + local_diagnostic, + primary_diagnostic, + }); + })?; + + let hover_result = hover_request.await.ok().flatten(); + let hover_popover = match hover_result { + Some(hover_result) if !hover_result.is_empty() => { + // Create symbol range of anchors for highlighting and filtering of future requests. + let range = if let Some(range) = hover_result.range { + let start = snapshot + .buffer_snapshot + .anchor_in_excerpt(excerpt_id.clone(), range.start); + let end = snapshot + .buffer_snapshot + .anchor_in_excerpt(excerpt_id.clone(), range.end); + + start..end + } else { + anchor..anchor + }; + + let language_registry = project.update(&mut cx, |p, _| p.languages().clone()); + let blocks = hover_result.contents; + let language = hover_result.language; + let parsed_content = parse_blocks(&blocks, &language_registry, language).await; + + Some(InfoPopover { + project: project.clone(), + symbol_range: RangeInEditor::Text(range), + blocks, + parsed_content, + }) + } + + _ => None, + }; + + this.update(&mut cx, |this, cx| { + if let Some(symbol_range) = hover_popover + .as_ref() + .and_then(|hover_popover| hover_popover.symbol_range.as_text_range()) + { + // Highlight the selected symbol using a background highlight + this.highlight_background::( + vec![symbol_range], + |theme| theme.editor.hover_popover.highlight, + cx, + ); + } else { + this.clear_background_highlights::(cx); + } + + this.hover_state.info_popover = hover_popover; + cx.notify(); + })?; + + Ok::<_, anyhow::Error>(()) + } + .log_err() + }); + + editor.hover_state.info_task = Some(task); +} + +async fn parse_blocks( + blocks: &[HoverBlock], + language_registry: &Arc, + language: Option>, +) -> markdown::ParsedMarkdown { + let mut text = String::new(); + let mut highlights = Vec::new(); + let mut region_ranges = Vec::new(); + let mut regions = Vec::new(); + + for block in blocks { + match &block.kind { + HoverBlockKind::PlainText => { + markdown::new_paragraph(&mut text, &mut Vec::new()); + text.push_str(&block.text); + } + + HoverBlockKind::Markdown => { + markdown::parse_markdown_block( + &block.text, + language_registry, + language.clone(), + &mut text, + &mut highlights, + &mut region_ranges, + &mut regions, + ) + .await + } + + HoverBlockKind::Code { language } => { + if let Some(language) = language_registry + .language_for_name(language) + .now_or_never() + .and_then(Result::ok) + { + markdown::highlight_code(&mut text, &mut highlights, &block.text, &language); + } else { + text.push_str(&block.text); + } + } + } + } + + ParsedMarkdown { + text: text.trim().to_string(), + highlights, + region_ranges, + regions, + } +} + +#[derive(Default)] +pub struct HoverState { + pub info_popover: Option, + pub diagnostic_popover: Option, + pub triggered_from: Option, + pub info_task: Option>>, +} + +impl HoverState { + pub fn visible(&self) -> bool { + self.info_popover.is_some() || self.diagnostic_popover.is_some() + } + + pub fn render( + &mut self, + snapshot: &EditorSnapshot, + style: &EditorStyle, + visible_rows: Range, + workspace: Option>, + cx: &mut ViewContext, + ) -> Option<(DisplayPoint, Vec>)> { + // If there is a diagnostic, position the popovers based on that. + // Otherwise use the start of the hover range + let anchor = self + .diagnostic_popover + .as_ref() + .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start) + .or_else(|| { + self.info_popover + .as_ref() + .map(|info_popover| match &info_popover.symbol_range { + RangeInEditor::Text(range) => &range.start, + RangeInEditor::Inlay(range) => &range.inlay_position, + }) + })?; + let point = anchor.to_display_point(&snapshot.display_snapshot); + + // Don't render if the relevant point isn't on screen + if !self.visible() || !visible_rows.contains(&point.row()) { + return None; + } + + let mut elements = Vec::new(); + + if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() { + elements.push(diagnostic_popover.render(style, cx)); + } + if let Some(info_popover) = self.info_popover.as_mut() { + elements.push(info_popover.render(style, workspace, cx)); + } + + Some((point, elements)) + } +} + +#[derive(Debug, Clone)] +pub struct InfoPopover { + pub project: ModelHandle, + symbol_range: RangeInEditor, + pub blocks: Vec, + parsed_content: ParsedMarkdown, +} + +impl InfoPopover { + pub fn render( + &mut self, + style: &EditorStyle, + workspace: Option>, + cx: &mut ViewContext, + ) -> AnyElement { + MouseEventHandler::new::(0, cx, |_, cx| { + Flex::column() + .scrollable::(0, None, cx) + .with_child(crate::render_parsed_markdown::( + &self.parsed_content, + style, + workspace, + cx, + )) + .contained() + .with_style(style.hover_popover.container) + }) + .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath. + .with_cursor_style(CursorStyle::Arrow) + .with_padding(Padding { + bottom: HOVER_POPOVER_GAP, + top: HOVER_POPOVER_GAP, + ..Default::default() + }) + .into_any() + } +} + +#[derive(Debug, Clone)] +pub struct DiagnosticPopover { + local_diagnostic: DiagnosticEntry, + primary_diagnostic: Option>, +} + +impl DiagnosticPopover { + pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext) -> AnyElement { + enum PrimaryDiagnostic {} + + let mut text_style = style.hover_popover.prose.clone(); + text_style.font_size = style.text.font_size; + let diagnostic_source_style = style.hover_popover.diagnostic_source_highlight.clone(); + + let text = match &self.local_diagnostic.diagnostic.source { + Some(source) => Text::new( + format!("{source}: {}", self.local_diagnostic.diagnostic.message), + text_style, + ) + .with_highlights(vec![(0..source.len(), diagnostic_source_style)]), + + None => Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style), + }; + + let container_style = match self.local_diagnostic.diagnostic.severity { + DiagnosticSeverity::HINT => style.hover_popover.info_container, + DiagnosticSeverity::INFORMATION => style.hover_popover.info_container, + DiagnosticSeverity::WARNING => style.hover_popover.warning_container, + DiagnosticSeverity::ERROR => style.hover_popover.error_container, + _ => style.hover_popover.container, + }; + + let tooltip_style = theme::current(cx).tooltip.clone(); + + MouseEventHandler::new::(0, cx, |_, _| { + text.with_soft_wrap(true) + .contained() + .with_style(container_style) + }) + .with_padding(Padding { + top: HOVER_POPOVER_GAP, + bottom: HOVER_POPOVER_GAP, + ..Default::default() + }) + .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath. + .on_click(MouseButton::Left, |_, this, cx| { + this.go_to_diagnostic(&Default::default(), cx) + }) + .with_cursor_style(CursorStyle::PointingHand) + .with_tooltip::( + 0, + "Go To Diagnostic".to_string(), + Some(Box::new(crate::GoToDiagnostic)), + tooltip_style, + cx, + ) + .into_any() + } + + pub fn activation_info(&self) -> (usize, Anchor) { + let entry = self + .primary_diagnostic + .as_ref() + .unwrap_or(&self.local_diagnostic); + + (entry.diagnostic.group_id, entry.range.start.clone()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + editor_tests::init_test, + element::PointForPosition, + inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, + link_go_to_definition::update_inlay_link_and_hover_points, + test::editor_lsp_test_context::EditorLspTestContext, + InlayId, + }; + use collections::BTreeSet; + use gpui::fonts::{HighlightStyle, Underline, Weight}; + use indoc::indoc; + use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; + use lsp::LanguageServerId; + use project::{HoverBlock, HoverBlockKind}; + use smol::stream::StreamExt; + use unindent::Unindent; + use util::test::marked_text_ranges; + + #[gpui::test] + async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Basic hover delays and then pops without moving the mouse + cx.set_state(indoc! {" + fn ˇtest() { println!(); } + "}); + let hover_point = cx.display_point(indoc! {" + fn test() { printˇln!(); } + "}); + + cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx)); + assert!(!cx.editor(|editor, _| editor.hover_state.visible())); + + // After delay, hover should be visible. + let symbol_range = cx.lsp_range(indoc! {" + fn test() { «println!»(); } + "}); + let mut requests = + cx.handle_request::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: "some basic docs".to_string(), + }), + range: Some(symbol_range), + })) + }); + cx.foreground() + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + requests.next().await; + + cx.editor(|editor, _| { + assert!(editor.hover_state.visible()); + assert_eq!( + editor.hover_state.info_popover.clone().unwrap().blocks, + vec![HoverBlock { + text: "some basic docs".to_string(), + kind: HoverBlockKind::Markdown, + },] + ) + }); + + // Mouse moved with no hover response dismisses + let hover_point = cx.display_point(indoc! {" + fn teˇst() { println!(); } + "}); + let mut request = cx + .lsp + .handle_request::(|_, _| async move { Ok(None) }); + cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx)); + cx.foreground() + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + request.next().await; + cx.editor(|editor, _| { + assert!(!editor.hover_state.visible()); + }); + } + + #[gpui::test] + async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Hover with keyboard has no delay + cx.set_state(indoc! {" + fˇn test() { println!(); } + "}); + cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); + let symbol_range = cx.lsp_range(indoc! {" + «fn» test() { println!(); } + "}); + cx.handle_request::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: "some other basic docs".to_string(), + }), + range: Some(symbol_range), + })) + }) + .next() + .await; + + cx.condition(|editor, _| editor.hover_state.visible()).await; + cx.editor(|editor, _| { + assert_eq!( + editor.hover_state.info_popover.clone().unwrap().blocks, + vec![HoverBlock { + text: "some other basic docs".to_string(), + kind: HoverBlockKind::Markdown, + }] + ) + }); + } + + #[gpui::test] + async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Hover with keyboard has no delay + cx.set_state(indoc! {" + fˇn test() { println!(); } + "}); + cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); + let symbol_range = cx.lsp_range(indoc! {" + «fn» test() { println!(); } + "}); + cx.handle_request::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Array(vec![ + lsp::MarkedString::String("regular text for hover to show".to_string()), + lsp::MarkedString::String("".to_string()), + lsp::MarkedString::LanguageString(lsp::LanguageString { + language: "Rust".to_string(), + value: "".to_string(), + }), + ]), + range: Some(symbol_range), + })) + }) + .next() + .await; + + cx.condition(|editor, _| editor.hover_state.visible()).await; + cx.editor(|editor, _| { + assert_eq!( + editor.hover_state.info_popover.clone().unwrap().blocks, + vec![HoverBlock { + text: "regular text for hover to show".to_string(), + kind: HoverBlockKind::Markdown, + }], + "No empty string hovers should be shown" + ); + }); + } + + #[gpui::test] + async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Hover with keyboard has no delay + cx.set_state(indoc! {" + fˇn test() { println!(); } + "}); + cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); + let symbol_range = cx.lsp_range(indoc! {" + «fn» test() { println!(); } + "}); + + let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n"; + let markdown_string = format!("\n```rust\n{code_str}```"); + + let closure_markdown_string = markdown_string.clone(); + cx.handle_request::(move |_, _, _| { + let future_markdown_string = closure_markdown_string.clone(); + async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: future_markdown_string, + }), + range: Some(symbol_range), + })) + } + }) + .next() + .await; + + cx.condition(|editor, _| editor.hover_state.visible()).await; + cx.editor(|editor, _| { + let blocks = editor.hover_state.info_popover.clone().unwrap().blocks; + assert_eq!( + blocks, + vec![HoverBlock { + text: markdown_string, + kind: HoverBlockKind::Markdown, + }], + ); + + let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); + assert_eq!( + rendered.text, + code_str.trim(), + "Should not have extra line breaks at end of rendered hover" + ); + }); + } + + #[gpui::test] + async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Hover with just diagnostic, pops DiagnosticPopover immediately and then + // info popover once request completes + cx.set_state(indoc! {" + fn teˇst() { println!(); } + "}); + + // Send diagnostic to client + let range = cx.text_anchor_range(indoc! {" + fn «test»() { println!(); } + "}); + cx.update_buffer(|buffer, cx| { + let snapshot = buffer.text_snapshot(); + let set = DiagnosticSet::from_sorted_entries( + vec![DiagnosticEntry { + range, + diagnostic: Diagnostic { + message: "A test diagnostic message.".to_string(), + ..Default::default() + }, + }], + &snapshot, + ); + buffer.update_diagnostics(LanguageServerId(0), set, cx); + }); + + // Hover pops diagnostic immediately + cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); + cx.foreground().run_until_parked(); + + cx.editor(|Editor { hover_state, .. }, _| { + assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none()) + }); + + // Info Popover shows after request responded to + let range = cx.lsp_range(indoc! {" + fn «test»() { println!(); } + "}); + cx.handle_request::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: "some new docs".to_string(), + }), + range: Some(range), + })) + }); + cx.foreground() + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + + cx.foreground().run_until_parked(); + cx.editor(|Editor { hover_state, .. }, _| { + hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some() + }); + } + + #[gpui::test] + fn test_render_blocks(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + cx.add_window(|cx| { + let editor = Editor::single_line(None, cx); + let style = editor.style(cx); + + struct Row { + blocks: Vec, + expected_marked_text: String, + expected_styles: Vec, + } + + let rows = &[ + // Strong emphasis + Row { + blocks: vec![HoverBlock { + text: "one **two** three".to_string(), + kind: HoverBlockKind::Markdown, + }], + expected_marked_text: "one «two» three".to_string(), + expected_styles: vec![HighlightStyle { + weight: Some(Weight::BOLD), + ..Default::default() + }], + }, + // Links + Row { + blocks: vec![HoverBlock { + text: "one [two](https://the-url) three".to_string(), + kind: HoverBlockKind::Markdown, + }], + expected_marked_text: "one «two» three".to_string(), + expected_styles: vec![HighlightStyle { + underline: Some(Underline { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + }], + }, + // Lists + Row { + blocks: vec![HoverBlock { + text: " + lists: + * one + - a + - b + * two + - [c](https://the-url) + - d" + .unindent(), + kind: HoverBlockKind::Markdown, + }], + expected_marked_text: " + lists: + - one + - a + - b + - two + - «c» + - d" + .unindent(), + expected_styles: vec![HighlightStyle { + underline: Some(Underline { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + }], + }, + // Multi-paragraph list items + Row { + blocks: vec![HoverBlock { + text: " + * one two + three + + * four five + * six seven + eight + + nine + * ten + * six" + .unindent(), + kind: HoverBlockKind::Markdown, + }], + expected_marked_text: " + - one two three + - four five + - six seven eight + + nine + - ten + - six" + .unindent(), + expected_styles: vec![HighlightStyle { + underline: Some(Underline { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + }], + }, + ]; + + for Row { + blocks, + expected_marked_text, + expected_styles, + } in &rows[0..] + { + let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); + + let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); + let expected_highlights = ranges + .into_iter() + .zip(expected_styles.iter().cloned()) + .collect::>(); + assert_eq!( + rendered.text, expected_text, + "wrong text for input {blocks:?}" + ); + + let rendered_highlights: Vec<_> = rendered + .highlights + .iter() + .filter_map(|(range, highlight)| { + let highlight = highlight.to_highlight_style(&style.syntax)?; + Some((range.clone(), highlight)) + }) + .collect(); + + assert_eq!( + rendered_highlights, expected_highlights, + "wrong highlights for input {blocks:?}" + ); + } + + editor + }); + } + + #[gpui::test] + async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Right( + lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions { + resolve_provider: Some(true), + ..Default::default() + }), + )), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variableˇ = TestNewType(TestStruct); + } + "}); + + let hint_start_offset = cx.ranges(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variableˇ = TestNewType(TestStruct); + } + "})[0] + .start; + let hint_position = cx.to_lsp(hint_start_offset); + let new_type_target_range = cx.lsp_range(indoc! {" + struct TestStruct; + + // ================== + + struct «TestNewType»(T); + + fn main() { + let variable = TestNewType(TestStruct); + } + "}); + let struct_target_range = cx.lsp_range(indoc! {" + struct «TestStruct»; + + // ================== + + struct TestNewType(T); + + fn main() { + let variable = TestNewType(TestStruct); + } + "}); + + let uri = cx.buffer_lsp_url.clone(); + let new_type_label = "TestNewType"; + let struct_label = "TestStruct"; + let entire_hint_label = ": TestNewType"; + let closure_uri = uri.clone(); + cx.lsp + .handle_request::(move |params, _| { + let task_uri = closure_uri.clone(); + async move { + assert_eq!(params.text_document.uri, task_uri); + Ok(Some(vec![lsp::InlayHint { + position: hint_position, + label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { + value: entire_hint_label.to_string(), + ..Default::default() + }]), + kind: Some(lsp::InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: Some(false), + padding_right: Some(false), + data: None, + }])) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let expected_layers = vec![entire_hint_label.to_string()]; + assert_eq!(expected_layers, cached_hint_labels(editor)); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + }); + + let inlay_range = cx + .ranges(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variable« »= TestNewType(TestStruct); + } + "}) + .get(0) + .cloned() + .unwrap(); + let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let previous_valid = inlay_range.start.to_display_point(&snapshot); + let next_valid = inlay_range.end.to_display_point(&snapshot); + assert_eq!(previous_valid.row(), next_valid.row()); + assert!(previous_valid.column() < next_valid.column()); + let exact_unclipped = DisplayPoint::new( + previous_valid.row(), + previous_valid.column() + + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2) + as u32, + ); + PointForPosition { + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end: 0, + } + }); + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + new_type_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + + let resolve_closure_uri = uri.clone(); + cx.lsp + .handle_request::( + move |mut hint_to_resolve, _| { + let mut resolved_hint_positions = BTreeSet::new(); + let task_uri = resolve_closure_uri.clone(); + async move { + let inserted = resolved_hint_positions.insert(hint_to_resolve.position); + assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice"); + + // `: TestNewType` + hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![ + lsp::InlayHintLabelPart { + value: ": ".to_string(), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: new_type_label.to_string(), + location: Some(lsp::Location { + uri: task_uri.clone(), + range: new_type_target_range, + }), + tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!( + "A tooltip for `{new_type_label}`" + ))), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: "<".to_string(), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: struct_label.to_string(), + location: Some(lsp::Location { + uri: task_uri, + range: struct_target_range, + }), + tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent( + lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: format!("A tooltip for `{struct_label}`"), + }, + )), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: ">".to_string(), + ..Default::default() + }, + ]); + + Ok(hint_to_resolve) + } + }, + ) + .next() + .await; + cx.foreground().run_until_parked(); + + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + new_type_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.foreground() + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let hover_state = &editor.hover_state; + assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); + let popover = hover_state.info_popover.as_ref().unwrap(); + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + assert_eq!( + popover.symbol_range, + RangeInEditor::Inlay(InlayHighlight { + inlay: InlayId::Hint(0), + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), + range: ": ".len()..": ".len() + new_type_label.len(), + }), + "Popover range should match the new type label part" + ); + assert_eq!( + popover.parsed_content.text, + format!("A tooltip for `{new_type_label}`"), + "Rendered text should not anyhow alter backticks" + ); + }); + + let struct_hint_part_hover_position = cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let previous_valid = inlay_range.start.to_display_point(&snapshot); + let next_valid = inlay_range.end.to_display_point(&snapshot); + assert_eq!(previous_valid.row(), next_valid.row()); + assert!(previous_valid.column() < next_valid.column()); + let exact_unclipped = DisplayPoint::new( + previous_valid.row(), + previous_valid.column() + + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2) + as u32, + ); + PointForPosition { + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end: 0, + } + }); + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + struct_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.foreground() + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let hover_state = &editor.hover_state; + assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); + let popover = hover_state.info_popover.as_ref().unwrap(); + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + assert_eq!( + popover.symbol_range, + RangeInEditor::Inlay(InlayHighlight { + inlay: InlayId::Hint(0), + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), + range: ": ".len() + new_type_label.len() + "<".len() + ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(), + }), + "Popover range should match the struct label part" + ); + assert_eq!( + popover.parsed_content.text, + format!("A tooltip for {struct_label}"), + "Rendered markdown element should remove backticks from text" + ); + }); + } +} diff --git a/crates/editor2/src/inlay_hint_cache.rs b/crates/editor2/src/inlay_hint_cache.rs new file mode 100644 index 0000000000..6b2712e7bf --- /dev/null +++ b/crates/editor2/src/inlay_hint_cache.rs @@ -0,0 +1,3349 @@ +use std::{ + cmp, + ops::{ControlFlow, Range}, + sync::Arc, + time::Duration, +}; + +use crate::{ + display_map::Inlay, Anchor, Editor, ExcerptId, InlayId, MultiBuffer, MultiBufferSnapshot, +}; +use anyhow::Context; +use clock::Global; +use futures::future; +use gpui::{ModelContext, ModelHandle, Task, ViewContext}; +use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot}; +use parking_lot::RwLock; +use project::{InlayHint, ResolveState}; + +use collections::{hash_map, HashMap, HashSet}; +use language::language_settings::InlayHintSettings; +use smol::lock::Semaphore; +use sum_tree::Bias; +use text::{ToOffset, ToPoint}; +use util::post_inc; + +pub struct InlayHintCache { + hints: HashMap>>, + allowed_hint_kinds: HashSet>, + version: usize, + pub(super) enabled: bool, + update_tasks: HashMap, + lsp_request_limiter: Arc, +} + +#[derive(Debug)] +struct TasksForRanges { + tasks: Vec>, + sorted_ranges: Vec>, +} + +#[derive(Debug)] +pub struct CachedExcerptHints { + version: usize, + buffer_version: Global, + buffer_id: u64, + ordered_hints: Vec, + hints_by_id: HashMap, +} + +#[derive(Debug, Clone, Copy)] +pub enum InvalidationStrategy { + RefreshRequested, + BufferEdited, + None, +} + +#[derive(Debug, Default)] +pub struct InlaySplice { + pub to_remove: Vec, + pub to_insert: Vec, +} + +#[derive(Debug)] +struct ExcerptHintsUpdate { + excerpt_id: ExcerptId, + remove_from_visible: Vec, + remove_from_cache: HashSet, + add_to_cache: Vec, +} + +#[derive(Debug, Clone, Copy)] +struct ExcerptQuery { + buffer_id: u64, + excerpt_id: ExcerptId, + cache_version: usize, + invalidate: InvalidationStrategy, + reason: &'static str, +} + +impl InvalidationStrategy { + fn should_invalidate(&self) -> bool { + matches!( + self, + InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited + ) + } +} + +impl TasksForRanges { + fn new(query_ranges: QueryRanges, task: Task<()>) -> Self { + let mut sorted_ranges = Vec::new(); + sorted_ranges.extend(query_ranges.before_visible); + sorted_ranges.extend(query_ranges.visible); + sorted_ranges.extend(query_ranges.after_visible); + Self { + tasks: vec![task], + sorted_ranges, + } + } + + fn update_cached_tasks( + &mut self, + buffer_snapshot: &BufferSnapshot, + query_ranges: QueryRanges, + invalidate: InvalidationStrategy, + spawn_task: impl FnOnce(QueryRanges) -> Task<()>, + ) { + let query_ranges = if invalidate.should_invalidate() { + self.tasks.clear(); + self.sorted_ranges.clear(); + query_ranges + } else { + let mut non_cached_query_ranges = query_ranges; + non_cached_query_ranges.before_visible = non_cached_query_ranges + .before_visible + .into_iter() + .flat_map(|query_range| { + self.remove_cached_ranges_from_query(buffer_snapshot, query_range) + }) + .collect(); + non_cached_query_ranges.visible = non_cached_query_ranges + .visible + .into_iter() + .flat_map(|query_range| { + self.remove_cached_ranges_from_query(buffer_snapshot, query_range) + }) + .collect(); + non_cached_query_ranges.after_visible = non_cached_query_ranges + .after_visible + .into_iter() + .flat_map(|query_range| { + self.remove_cached_ranges_from_query(buffer_snapshot, query_range) + }) + .collect(); + non_cached_query_ranges + }; + + if !query_ranges.is_empty() { + self.tasks.push(spawn_task(query_ranges)); + } + } + + fn remove_cached_ranges_from_query( + &mut self, + buffer_snapshot: &BufferSnapshot, + query_range: Range, + ) -> Vec> { + let mut ranges_to_query = Vec::new(); + let mut latest_cached_range = None::<&mut Range>; + for cached_range in self + .sorted_ranges + .iter_mut() + .skip_while(|cached_range| { + cached_range + .end + .cmp(&query_range.start, buffer_snapshot) + .is_lt() + }) + .take_while(|cached_range| { + cached_range + .start + .cmp(&query_range.end, buffer_snapshot) + .is_le() + }) + { + match latest_cached_range { + Some(latest_cached_range) => { + if latest_cached_range.end.offset.saturating_add(1) < cached_range.start.offset + { + ranges_to_query.push(latest_cached_range.end..cached_range.start); + cached_range.start = latest_cached_range.end; + } + } + None => { + if query_range + .start + .cmp(&cached_range.start, buffer_snapshot) + .is_lt() + { + ranges_to_query.push(query_range.start..cached_range.start); + cached_range.start = query_range.start; + } + } + } + latest_cached_range = Some(cached_range); + } + + match latest_cached_range { + Some(latest_cached_range) => { + if latest_cached_range.end.offset.saturating_add(1) < query_range.end.offset { + ranges_to_query.push(latest_cached_range.end..query_range.end); + latest_cached_range.end = query_range.end; + } + } + None => { + ranges_to_query.push(query_range.clone()); + self.sorted_ranges.push(query_range); + self.sorted_ranges + .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot)); + } + } + + ranges_to_query + } + + fn invalidate_range(&mut self, buffer: &BufferSnapshot, range: &Range) { + self.sorted_ranges = self + .sorted_ranges + .drain(..) + .filter_map(|mut cached_range| { + if cached_range.start.cmp(&range.end, buffer).is_gt() + || cached_range.end.cmp(&range.start, buffer).is_lt() + { + Some(vec![cached_range]) + } else if cached_range.start.cmp(&range.start, buffer).is_ge() + && cached_range.end.cmp(&range.end, buffer).is_le() + { + None + } else if range.start.cmp(&cached_range.start, buffer).is_ge() + && range.end.cmp(&cached_range.end, buffer).is_le() + { + Some(vec![ + cached_range.start..range.start, + range.end..cached_range.end, + ]) + } else if cached_range.start.cmp(&range.start, buffer).is_ge() { + cached_range.start = range.end; + Some(vec![cached_range]) + } else { + cached_range.end = range.start; + Some(vec![cached_range]) + } + }) + .flatten() + .collect(); + } +} + +impl InlayHintCache { + pub fn new(inlay_hint_settings: InlayHintSettings) -> Self { + Self { + allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(), + enabled: inlay_hint_settings.enabled, + hints: HashMap::default(), + update_tasks: HashMap::default(), + version: 0, + lsp_request_limiter: Arc::new(Semaphore::new(MAX_CONCURRENT_LSP_REQUESTS)), + } + } + + pub fn update_settings( + &mut self, + multi_buffer: &ModelHandle, + new_hint_settings: InlayHintSettings, + visible_hints: Vec, + cx: &mut ViewContext, + ) -> ControlFlow> { + let new_allowed_hint_kinds = new_hint_settings.enabled_inlay_hint_kinds(); + match (self.enabled, new_hint_settings.enabled) { + (false, false) => { + self.allowed_hint_kinds = new_allowed_hint_kinds; + ControlFlow::Break(None) + } + (true, true) => { + if new_allowed_hint_kinds == self.allowed_hint_kinds { + ControlFlow::Break(None) + } else { + let new_splice = self.new_allowed_hint_kinds_splice( + multi_buffer, + &visible_hints, + &new_allowed_hint_kinds, + cx, + ); + if new_splice.is_some() { + self.version += 1; + self.allowed_hint_kinds = new_allowed_hint_kinds; + } + ControlFlow::Break(new_splice) + } + } + (true, false) => { + self.enabled = new_hint_settings.enabled; + self.allowed_hint_kinds = new_allowed_hint_kinds; + if self.hints.is_empty() { + ControlFlow::Break(None) + } else { + self.clear(); + ControlFlow::Break(Some(InlaySplice { + to_remove: visible_hints.iter().map(|inlay| inlay.id).collect(), + to_insert: Vec::new(), + })) + } + } + (false, true) => { + self.enabled = new_hint_settings.enabled; + self.allowed_hint_kinds = new_allowed_hint_kinds; + ControlFlow::Continue(()) + } + } + } + + pub fn spawn_hint_refresh( + &mut self, + reason: &'static str, + excerpts_to_query: HashMap, Global, Range)>, + invalidate: InvalidationStrategy, + cx: &mut ViewContext, + ) -> Option { + if !self.enabled { + return None; + } + + let mut invalidated_hints = Vec::new(); + if invalidate.should_invalidate() { + self.update_tasks + .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id)); + self.hints.retain(|cached_excerpt, cached_hints| { + let retain = excerpts_to_query.contains_key(cached_excerpt); + if !retain { + invalidated_hints.extend(cached_hints.read().ordered_hints.iter().copied()); + } + retain + }); + } + if excerpts_to_query.is_empty() && invalidated_hints.is_empty() { + return None; + } + + let cache_version = self.version + 1; + cx.spawn(|editor, mut cx| async move { + editor + .update(&mut cx, |editor, cx| { + spawn_new_update_tasks( + editor, + reason, + excerpts_to_query, + invalidate, + cache_version, + cx, + ) + }) + .ok(); + }) + .detach(); + + if invalidated_hints.is_empty() { + None + } else { + Some(InlaySplice { + to_remove: invalidated_hints, + to_insert: Vec::new(), + }) + } + } + + fn new_allowed_hint_kinds_splice( + &self, + multi_buffer: &ModelHandle, + visible_hints: &[Inlay], + new_kinds: &HashSet>, + cx: &mut ViewContext, + ) -> Option { + let old_kinds = &self.allowed_hint_kinds; + if new_kinds == old_kinds { + return None; + } + + let mut to_remove = Vec::new(); + let mut to_insert = Vec::new(); + let mut shown_hints_to_remove = visible_hints.iter().fold( + HashMap::>::default(), + |mut current_hints, inlay| { + current_hints + .entry(inlay.position.excerpt_id) + .or_default() + .push((inlay.position, inlay.id)); + current_hints + }, + ); + + let multi_buffer = multi_buffer.read(cx); + let multi_buffer_snapshot = multi_buffer.snapshot(cx); + + for (excerpt_id, excerpt_cached_hints) in &self.hints { + let shown_excerpt_hints_to_remove = + shown_hints_to_remove.entry(*excerpt_id).or_default(); + let excerpt_cached_hints = excerpt_cached_hints.read(); + let mut excerpt_cache = excerpt_cached_hints.ordered_hints.iter().fuse().peekable(); + shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| { + let Some(buffer) = shown_anchor + .buffer_id + .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) + else { + return false; + }; + let buffer_snapshot = buffer.read(cx).snapshot(); + loop { + match excerpt_cache.peek() { + Some(&cached_hint_id) => { + let cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id]; + if cached_hint_id == shown_hint_id { + excerpt_cache.next(); + return !new_kinds.contains(&cached_hint.kind); + } + + match cached_hint + .position + .cmp(&shown_anchor.text_anchor, &buffer_snapshot) + { + cmp::Ordering::Less | cmp::Ordering::Equal => { + if !old_kinds.contains(&cached_hint.kind) + && new_kinds.contains(&cached_hint.kind) + { + to_insert.push(Inlay::hint( + cached_hint_id.id(), + multi_buffer_snapshot.anchor_in_excerpt( + *excerpt_id, + cached_hint.position, + ), + &cached_hint, + )); + } + excerpt_cache.next(); + } + cmp::Ordering::Greater => return true, + } + } + None => return true, + } + } + }); + + for cached_hint_id in excerpt_cache { + let maybe_missed_cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id]; + let cached_hint_kind = maybe_missed_cached_hint.kind; + if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) { + to_insert.push(Inlay::hint( + cached_hint_id.id(), + multi_buffer_snapshot + .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position), + &maybe_missed_cached_hint, + )); + } + } + } + + to_remove.extend( + shown_hints_to_remove + .into_values() + .flatten() + .map(|(_, hint_id)| hint_id), + ); + if to_remove.is_empty() && to_insert.is_empty() { + None + } else { + Some(InlaySplice { + to_remove, + to_insert, + }) + } + } + + pub fn remove_excerpts(&mut self, excerpts_removed: Vec) -> Option { + let mut to_remove = Vec::new(); + for excerpt_to_remove in excerpts_removed { + self.update_tasks.remove(&excerpt_to_remove); + if let Some(cached_hints) = self.hints.remove(&excerpt_to_remove) { + let cached_hints = cached_hints.read(); + to_remove.extend(cached_hints.ordered_hints.iter().copied()); + } + } + if to_remove.is_empty() { + None + } else { + self.version += 1; + Some(InlaySplice { + to_remove, + to_insert: Vec::new(), + }) + } + } + + pub fn clear(&mut self) { + if !self.update_tasks.is_empty() || !self.hints.is_empty() { + self.version += 1; + } + self.update_tasks.clear(); + self.hints.clear(); + } + + pub fn hint_by_id(&self, excerpt_id: ExcerptId, hint_id: InlayId) -> Option { + self.hints + .get(&excerpt_id)? + .read() + .hints_by_id + .get(&hint_id) + .cloned() + } + + pub fn hints(&self) -> Vec { + let mut hints = Vec::new(); + for excerpt_hints in self.hints.values() { + let excerpt_hints = excerpt_hints.read(); + hints.extend( + excerpt_hints + .ordered_hints + .iter() + .map(|id| &excerpt_hints.hints_by_id[id]) + .cloned(), + ); + } + hints + } + + pub fn version(&self) -> usize { + self.version + } + + pub fn spawn_hint_resolve( + &self, + buffer_id: u64, + excerpt_id: ExcerptId, + id: InlayId, + cx: &mut ViewContext<'_, '_, Editor>, + ) { + if let Some(excerpt_hints) = self.hints.get(&excerpt_id) { + let mut guard = excerpt_hints.write(); + if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) { + if let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state { + let hint_to_resolve = cached_hint.clone(); + let server_id = *server_id; + cached_hint.resolve_state = ResolveState::Resolving; + drop(guard); + cx.spawn(|editor, mut cx| async move { + let resolved_hint_task = editor.update(&mut cx, |editor, cx| { + editor + .buffer() + .read(cx) + .buffer(buffer_id) + .and_then(|buffer| { + let project = editor.project.as_ref()?; + Some(project.update(cx, |project, cx| { + project.resolve_inlay_hint( + hint_to_resolve, + buffer, + server_id, + cx, + ) + })) + }) + })?; + if let Some(resolved_hint_task) = resolved_hint_task { + let mut resolved_hint = + resolved_hint_task.await.context("hint resolve task")?; + editor.update(&mut cx, |editor, _| { + if let Some(excerpt_hints) = + editor.inlay_hint_cache.hints.get(&excerpt_id) + { + let mut guard = excerpt_hints.write(); + if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) { + if cached_hint.resolve_state == ResolveState::Resolving { + resolved_hint.resolve_state = ResolveState::Resolved; + *cached_hint = resolved_hint; + } + } + } + })?; + } + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + } + } + } +} + +fn spawn_new_update_tasks( + editor: &mut Editor, + reason: &'static str, + excerpts_to_query: HashMap, Global, Range)>, + invalidate: InvalidationStrategy, + update_cache_version: usize, + cx: &mut ViewContext<'_, '_, Editor>, +) { + let visible_hints = Arc::new(editor.visible_inlay_hints(cx)); + for (excerpt_id, (excerpt_buffer, new_task_buffer_version, excerpt_visible_range)) in + excerpts_to_query + { + if excerpt_visible_range.is_empty() { + continue; + } + let buffer = excerpt_buffer.read(cx); + let buffer_id = buffer.remote_id(); + let buffer_snapshot = buffer.snapshot(); + if buffer_snapshot + .version() + .changed_since(&new_task_buffer_version) + { + continue; + } + + let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned(); + if let Some(cached_excerpt_hints) = &cached_excerpt_hints { + let cached_excerpt_hints = cached_excerpt_hints.read(); + let cached_buffer_version = &cached_excerpt_hints.buffer_version; + if cached_excerpt_hints.version > update_cache_version + || cached_buffer_version.changed_since(&new_task_buffer_version) + { + continue; + } + }; + + let (multi_buffer_snapshot, Some(query_ranges)) = + editor.buffer.update(cx, |multi_buffer, cx| { + ( + multi_buffer.snapshot(cx), + determine_query_ranges( + multi_buffer, + excerpt_id, + &excerpt_buffer, + excerpt_visible_range, + cx, + ), + ) + }) + else { + return; + }; + let query = ExcerptQuery { + buffer_id, + excerpt_id, + cache_version: update_cache_version, + invalidate, + reason, + }; + + let new_update_task = |query_ranges| { + new_update_task( + query, + query_ranges, + multi_buffer_snapshot, + buffer_snapshot.clone(), + Arc::clone(&visible_hints), + cached_excerpt_hints, + Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter), + cx, + ) + }; + + match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) { + hash_map::Entry::Occupied(mut o) => { + o.get_mut().update_cached_tasks( + &buffer_snapshot, + query_ranges, + invalidate, + new_update_task, + ); + } + hash_map::Entry::Vacant(v) => { + v.insert(TasksForRanges::new( + query_ranges.clone(), + new_update_task(query_ranges), + )); + } + } + } +} + +#[derive(Debug, Clone)] +struct QueryRanges { + before_visible: Vec>, + visible: Vec>, + after_visible: Vec>, +} + +impl QueryRanges { + fn is_empty(&self) -> bool { + self.before_visible.is_empty() && self.visible.is_empty() && self.after_visible.is_empty() + } +} + +fn determine_query_ranges( + multi_buffer: &mut MultiBuffer, + excerpt_id: ExcerptId, + excerpt_buffer: &ModelHandle, + excerpt_visible_range: Range, + cx: &mut ModelContext<'_, MultiBuffer>, +) -> Option { + let full_excerpt_range = multi_buffer + .excerpts_for_buffer(excerpt_buffer, cx) + .into_iter() + .find(|(id, _)| id == &excerpt_id) + .map(|(_, range)| range.context)?; + let buffer = excerpt_buffer.read(cx); + let snapshot = buffer.snapshot(); + let excerpt_visible_len = excerpt_visible_range.end - excerpt_visible_range.start; + + let visible_range = if excerpt_visible_range.start == excerpt_visible_range.end { + return None; + } else { + vec![ + buffer.anchor_before(snapshot.clip_offset(excerpt_visible_range.start, Bias::Left)) + ..buffer.anchor_after(snapshot.clip_offset(excerpt_visible_range.end, Bias::Right)), + ] + }; + + let full_excerpt_range_end_offset = full_excerpt_range.end.to_offset(&snapshot); + let after_visible_range_start = excerpt_visible_range + .end + .saturating_add(1) + .min(full_excerpt_range_end_offset) + .min(buffer.len()); + let after_visible_range = if after_visible_range_start == full_excerpt_range_end_offset { + Vec::new() + } else { + let after_range_end_offset = after_visible_range_start + .saturating_add(excerpt_visible_len) + .min(full_excerpt_range_end_offset) + .min(buffer.len()); + vec![ + buffer.anchor_before(snapshot.clip_offset(after_visible_range_start, Bias::Left)) + ..buffer.anchor_after(snapshot.clip_offset(after_range_end_offset, Bias::Right)), + ] + }; + + let full_excerpt_range_start_offset = full_excerpt_range.start.to_offset(&snapshot); + let before_visible_range_end = excerpt_visible_range + .start + .saturating_sub(1) + .max(full_excerpt_range_start_offset); + let before_visible_range = if before_visible_range_end == full_excerpt_range_start_offset { + Vec::new() + } else { + let before_range_start_offset = before_visible_range_end + .saturating_sub(excerpt_visible_len) + .max(full_excerpt_range_start_offset); + vec![ + buffer.anchor_before(snapshot.clip_offset(before_range_start_offset, Bias::Left)) + ..buffer.anchor_after(snapshot.clip_offset(before_visible_range_end, Bias::Right)), + ] + }; + + Some(QueryRanges { + before_visible: before_visible_range, + visible: visible_range, + after_visible: after_visible_range, + }) +} + +const MAX_CONCURRENT_LSP_REQUESTS: usize = 5; +const INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS: u64 = 400; + +fn new_update_task( + query: ExcerptQuery, + query_ranges: QueryRanges, + multi_buffer_snapshot: MultiBufferSnapshot, + buffer_snapshot: BufferSnapshot, + visible_hints: Arc>, + cached_excerpt_hints: Option>>, + lsp_request_limiter: Arc, + cx: &mut ViewContext<'_, '_, Editor>, +) -> Task<()> { + cx.spawn(|editor, mut cx| async move { + let closure_cx = cx.clone(); + let fetch_and_update_hints = |invalidate, range| { + fetch_and_update_hints( + editor.clone(), + multi_buffer_snapshot.clone(), + buffer_snapshot.clone(), + Arc::clone(&visible_hints), + cached_excerpt_hints.as_ref().map(Arc::clone), + query, + invalidate, + range, + Arc::clone(&lsp_request_limiter), + closure_cx.clone(), + ) + }; + let visible_range_update_results = future::join_all(query_ranges.visible.into_iter().map( + |visible_range| async move { + ( + visible_range.clone(), + fetch_and_update_hints(query.invalidate.should_invalidate(), visible_range) + .await, + ) + }, + )) + .await; + + let hint_delay = cx.background().timer(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS, + )); + + let mut query_range_failed = |range: &Range, e: anyhow::Error| { + log::error!("inlay hint update task for range {range:?} failed: {e:#}"); + editor + .update(&mut cx, |editor, _| { + if let Some(task_ranges) = editor + .inlay_hint_cache + .update_tasks + .get_mut(&query.excerpt_id) + { + task_ranges.invalidate_range(&buffer_snapshot, &range); + } + }) + .ok() + }; + + for (range, result) in visible_range_update_results { + if let Err(e) = result { + query_range_failed(&range, e); + } + } + + hint_delay.await; + let invisible_range_update_results = future::join_all( + query_ranges + .before_visible + .into_iter() + .chain(query_ranges.after_visible.into_iter()) + .map(|invisible_range| async move { + ( + invisible_range.clone(), + fetch_and_update_hints(false, invisible_range).await, + ) + }), + ) + .await; + for (range, result) in invisible_range_update_results { + if let Err(e) = result { + query_range_failed(&range, e); + } + } + }) +} + +async fn fetch_and_update_hints( + editor: gpui::WeakViewHandle, + multi_buffer_snapshot: MultiBufferSnapshot, + buffer_snapshot: BufferSnapshot, + visible_hints: Arc>, + cached_excerpt_hints: Option>>, + query: ExcerptQuery, + invalidate: bool, + fetch_range: Range, + lsp_request_limiter: Arc, + mut cx: gpui::AsyncAppContext, +) -> anyhow::Result<()> { + let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() { + (None, false) + } else { + match lsp_request_limiter.try_acquire() { + Some(guard) => (Some(guard), false), + None => (Some(lsp_request_limiter.acquire().await), true), + } + }; + let fetch_range_to_log = + fetch_range.start.to_point(&buffer_snapshot)..fetch_range.end.to_point(&buffer_snapshot); + let inlay_hints_fetch_task = editor + .update(&mut cx, |editor, cx| { + if got_throttled { + let query_not_around_visible_range = match editor.excerpt_visible_offsets(None, cx).remove(&query.excerpt_id) { + Some((_, _, current_visible_range)) => { + let visible_offset_length = current_visible_range.len(); + let double_visible_range = current_visible_range + .start + .saturating_sub(visible_offset_length) + ..current_visible_range + .end + .saturating_add(visible_offset_length) + .min(buffer_snapshot.len()); + !double_visible_range + .contains(&fetch_range.start.to_offset(&buffer_snapshot)) + && !double_visible_range + .contains(&fetch_range.end.to_offset(&buffer_snapshot)) + }, + None => true, + }; + if query_not_around_visible_range { + log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping."); + if let Some(task_ranges) = editor + .inlay_hint_cache + .update_tasks + .get_mut(&query.excerpt_id) + { + task_ranges.invalidate_range(&buffer_snapshot, &fetch_range); + } + return None; + } + } + editor + .buffer() + .read(cx) + .buffer(query.buffer_id) + .and_then(|buffer| { + let project = editor.project.as_ref()?; + Some(project.update(cx, |project, cx| { + project.inlay_hints(buffer, fetch_range.clone(), cx) + })) + }) + }) + .ok() + .flatten(); + let new_hints = match inlay_hints_fetch_task { + Some(fetch_task) => { + log::debug!( + "Fetching inlay hints for range {fetch_range_to_log:?}, reason: {query_reason}, invalidate: {invalidate}", + query_reason = query.reason, + ); + log::trace!( + "Currently visible hints: {visible_hints:?}, cached hints present: {}", + cached_excerpt_hints.is_some(), + ); + fetch_task.await.context("inlay hint fetch task")? + } + None => return Ok(()), + }; + drop(lsp_request_guard); + log::debug!( + "Fetched {} hints for range {fetch_range_to_log:?}", + new_hints.len() + ); + log::trace!("Fetched hints: {new_hints:?}"); + + let background_task_buffer_snapshot = buffer_snapshot.clone(); + let backround_fetch_range = fetch_range.clone(); + let new_update = cx + .background() + .spawn(async move { + calculate_hint_updates( + query.excerpt_id, + invalidate, + backround_fetch_range, + new_hints, + &background_task_buffer_snapshot, + cached_excerpt_hints, + &visible_hints, + ) + }) + .await; + if let Some(new_update) = new_update { + log::debug!( + "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}", + new_update.remove_from_visible.len(), + new_update.remove_from_cache.len(), + new_update.add_to_cache.len() + ); + log::trace!("New update: {new_update:?}"); + editor + .update(&mut cx, |editor, cx| { + apply_hint_update( + editor, + new_update, + query, + invalidate, + buffer_snapshot, + multi_buffer_snapshot, + cx, + ); + }) + .ok(); + } + Ok(()) +} + +fn calculate_hint_updates( + excerpt_id: ExcerptId, + invalidate: bool, + fetch_range: Range, + new_excerpt_hints: Vec, + buffer_snapshot: &BufferSnapshot, + cached_excerpt_hints: Option>>, + visible_hints: &[Inlay], +) -> Option { + let mut add_to_cache = Vec::::new(); + let mut excerpt_hints_to_persist = HashMap::default(); + for new_hint in new_excerpt_hints { + if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) { + continue; + } + let missing_from_cache = match &cached_excerpt_hints { + Some(cached_excerpt_hints) => { + let cached_excerpt_hints = cached_excerpt_hints.read(); + match cached_excerpt_hints + .ordered_hints + .binary_search_by(|probe| { + cached_excerpt_hints.hints_by_id[probe] + .position + .cmp(&new_hint.position, buffer_snapshot) + }) { + Ok(ix) => { + let mut missing_from_cache = true; + for id in &cached_excerpt_hints.ordered_hints[ix..] { + let cached_hint = &cached_excerpt_hints.hints_by_id[id]; + if new_hint + .position + .cmp(&cached_hint.position, buffer_snapshot) + .is_gt() + { + break; + } + if cached_hint == &new_hint { + excerpt_hints_to_persist.insert(*id, cached_hint.kind); + missing_from_cache = false; + } + } + missing_from_cache + } + Err(_) => true, + } + } + None => true, + }; + if missing_from_cache { + add_to_cache.push(new_hint); + } + } + + let mut remove_from_visible = Vec::new(); + let mut remove_from_cache = HashSet::default(); + if invalidate { + remove_from_visible.extend( + visible_hints + .iter() + .filter(|hint| hint.position.excerpt_id == excerpt_id) + .map(|inlay_hint| inlay_hint.id) + .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)), + ); + + if let Some(cached_excerpt_hints) = &cached_excerpt_hints { + let cached_excerpt_hints = cached_excerpt_hints.read(); + remove_from_cache.extend( + cached_excerpt_hints + .ordered_hints + .iter() + .filter(|cached_inlay_id| { + !excerpt_hints_to_persist.contains_key(cached_inlay_id) + }) + .copied(), + ); + } + } + + if remove_from_visible.is_empty() && remove_from_cache.is_empty() && add_to_cache.is_empty() { + None + } else { + Some(ExcerptHintsUpdate { + excerpt_id, + remove_from_visible, + remove_from_cache, + add_to_cache, + }) + } +} + +fn contains_position( + range: &Range, + position: language::Anchor, + buffer_snapshot: &BufferSnapshot, +) -> bool { + range.start.cmp(&position, buffer_snapshot).is_le() + && range.end.cmp(&position, buffer_snapshot).is_ge() +} + +fn apply_hint_update( + editor: &mut Editor, + new_update: ExcerptHintsUpdate, + query: ExcerptQuery, + invalidate: bool, + buffer_snapshot: BufferSnapshot, + multi_buffer_snapshot: MultiBufferSnapshot, + cx: &mut ViewContext<'_, '_, Editor>, +) { + let cached_excerpt_hints = editor + .inlay_hint_cache + .hints + .entry(new_update.excerpt_id) + .or_insert_with(|| { + Arc::new(RwLock::new(CachedExcerptHints { + version: query.cache_version, + buffer_version: buffer_snapshot.version().clone(), + buffer_id: query.buffer_id, + ordered_hints: Vec::new(), + hints_by_id: HashMap::default(), + })) + }); + let mut cached_excerpt_hints = cached_excerpt_hints.write(); + match query.cache_version.cmp(&cached_excerpt_hints.version) { + cmp::Ordering::Less => return, + cmp::Ordering::Greater | cmp::Ordering::Equal => { + cached_excerpt_hints.version = query.cache_version; + } + } + + let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty(); + cached_excerpt_hints + .ordered_hints + .retain(|hint_id| !new_update.remove_from_cache.contains(hint_id)); + cached_excerpt_hints + .hints_by_id + .retain(|hint_id, _| !new_update.remove_from_cache.contains(hint_id)); + let mut splice = InlaySplice { + to_remove: new_update.remove_from_visible, + to_insert: Vec::new(), + }; + for new_hint in new_update.add_to_cache { + let insert_position = match cached_excerpt_hints + .ordered_hints + .binary_search_by(|probe| { + cached_excerpt_hints.hints_by_id[probe] + .position + .cmp(&new_hint.position, &buffer_snapshot) + }) { + Ok(i) => { + let mut insert_position = Some(i); + for id in &cached_excerpt_hints.ordered_hints[i..] { + let cached_hint = &cached_excerpt_hints.hints_by_id[id]; + if new_hint + .position + .cmp(&cached_hint.position, &buffer_snapshot) + .is_gt() + { + break; + } + if cached_hint.text() == new_hint.text() { + insert_position = None; + break; + } + } + insert_position + } + Err(i) => Some(i), + }; + + if let Some(insert_position) = insert_position { + let new_inlay_id = post_inc(&mut editor.next_inlay_id); + if editor + .inlay_hint_cache + .allowed_hint_kinds + .contains(&new_hint.kind) + { + let new_hint_position = + multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position); + splice + .to_insert + .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint)); + } + let new_id = InlayId::Hint(new_inlay_id); + cached_excerpt_hints.hints_by_id.insert(new_id, new_hint); + cached_excerpt_hints + .ordered_hints + .insert(insert_position, new_id); + cached_inlays_changed = true; + } + } + cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); + drop(cached_excerpt_hints); + + if invalidate { + let mut outdated_excerpt_caches = HashSet::default(); + for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints { + let excerpt_hints = excerpt_hints.read(); + if excerpt_hints.buffer_id == query.buffer_id + && excerpt_id != &query.excerpt_id + && buffer_snapshot + .version() + .changed_since(&excerpt_hints.buffer_version) + { + outdated_excerpt_caches.insert(*excerpt_id); + splice + .to_remove + .extend(excerpt_hints.ordered_hints.iter().copied()); + } + } + cached_inlays_changed |= !outdated_excerpt_caches.is_empty(); + editor + .inlay_hint_cache + .hints + .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id)); + } + + let InlaySplice { + to_remove, + to_insert, + } = splice; + let displayed_inlays_changed = !to_remove.is_empty() || !to_insert.is_empty(); + if cached_inlays_changed || displayed_inlays_changed { + editor.inlay_hint_cache.version += 1; + } + if displayed_inlays_changed { + editor.splice_inlay_hints(to_remove, to_insert, cx) + } +} + +#[cfg(test)] +pub mod tests { + use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; + + use crate::{ + scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount}, + serde_json::json, + ExcerptRange, + }; + use futures::StreamExt; + use gpui::{executor::Deterministic, TestAppContext, ViewHandle}; + use itertools::Itertools; + use language::{ + language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig, + }; + use lsp::FakeLanguageServer; + use parking_lot::Mutex; + use project::{FakeFs, Project}; + use settings::SettingsStore; + use text::{Point, ToPoint}; + use workspace::Workspace; + + use crate::editor_tests::update_test_language_settings; + + use super::*; + + #[gpui::test] + async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) { + let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), + show_other_hints: allowed_hint_kinds.contains(&None), + }) + }); + + let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; + let lsp_request_count = Arc::new(AtomicU32::new(0)); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path(file_with_hints).unwrap(), + ); + let current_call_id = + Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); + let mut new_hints = Vec::with_capacity(2 * current_call_id as usize); + for _ in 0..2 { + let mut i = current_call_id; + loop { + new_hints.push(lsp::InlayHint { + position: lsp::Position::new(0, i), + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }); + if i == 0 { + break; + } + i -= 1; + } + } + + Ok(Some(new_hints)) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + + let mut edits_made = 1; + editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get its first hints when opening the editor" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.allowed_hint_kinds, allowed_hint_kinds, + "Cache should use editor settings to get the allowed hint kinds" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor update the cache version after every cache/view change" + ); + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input("some change", cx); + edits_made += 1; + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string(), "1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get new hints after an edit" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.allowed_hint_kinds, allowed_hint_kinds, + "Cache should use editor settings to get the allowed hint kinds" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor update the cache version after every cache/view change" + ); + }); + + fake_server + .request::(()) + .await + .expect("inlay refresh request failed"); + edits_made += 1; + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string(), "1".to_string(), "2".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get new hints after hint refresh/ request" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.allowed_hint_kinds, allowed_hint_kinds, + "Cache should use editor settings to get the allowed hint kinds" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor update the cache version after every cache/view change" + ); + }); + } + + #[gpui::test] + async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; + let lsp_request_count = Arc::new(AtomicU32::new(0)); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path(file_with_hints).unwrap(), + ); + let current_call_id = + Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, current_call_id), + label: lsp::InlayHintLabel::String(current_call_id.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + + let mut edits_made = 1; + editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get its first hints when opening the editor" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + edits_made, + "The editor update the cache version after every cache/view change" + ); + }); + + let progress_token = "test_progress_token"; + fake_server + .request::(lsp::WorkDoneProgressCreateParams { + token: lsp::ProgressToken::String(progress_token.to_string()), + }) + .await + .expect("work done progress create request failed"); + cx.foreground().run_until_parked(); + fake_server.notify::(lsp::ProgressParams { + token: lsp::ProgressToken::String(progress_token.to_string()), + value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin( + lsp::WorkDoneProgressBegin::default(), + )), + }); + cx.foreground().run_until_parked(); + + editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should not update hints while the work task is running" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + edits_made, + "Should not update the cache while the work task is running" + ); + }); + + fake_server.notify::(lsp::ProgressParams { + token: lsp::ProgressToken::String(progress_token.to_string()), + value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End( + lsp::WorkDoneProgressEnd::default(), + )), + }); + cx.foreground().run_until_parked(); + + edits_made += 1; + editor.update(cx, |editor, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "New hints should be queried after the work task is done" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + edits_made, + "Cache version should udpate once after the work task is done" + ); + }); + } + + #[gpui::test] + async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out", + "other.md": "Test md file with some text", + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .root(cx); + let worktree_id = workspace.update(cx, |workspace, cx| { + workspace.project().read_with(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }); + + let mut rs_fake_servers = None; + let mut md_fake_servers = None; + for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] { + let mut language = Language::new( + LanguageConfig { + name: name.into(), + path_suffixes: vec![path_suffix.to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + name, + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + match name { + "Rust" => rs_fake_servers = Some(fake_servers), + "Markdown" => md_fake_servers = Some(fake_servers), + _ => unreachable!(), + } + project.update(cx, |project, _| { + project.languages().add(Arc::new(language)); + }); + } + + let _rs_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + cx.foreground().run_until_parked(); + cx.foreground().start_waiting(); + let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap(); + let rs_editor = workspace + .update(cx, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let rs_lsp_request_count = Arc::new(AtomicU32::new(0)); + rs_fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&rs_lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, i), + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + rs_editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get its first hints when opening the editor" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + 1, + "Rust editor update the cache version after every cache/view change" + ); + }); + + cx.foreground().run_until_parked(); + let _md_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/other.md", cx) + }) + .await + .unwrap(); + cx.foreground().run_until_parked(); + cx.foreground().start_waiting(); + let md_fake_server = md_fake_servers.unwrap().next().await.unwrap(); + let md_editor = workspace + .update(cx, |workspace, cx| { + workspace.open_path((worktree_id, "other.md"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let md_lsp_request_count = Arc::new(AtomicU32::new(0)); + md_fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&md_lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/other.md").unwrap(), + ); + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, i), + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + md_editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Markdown editor should have a separate verison, repeating Rust editor rules" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 1); + }); + + rs_editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input("some rs change", cx); + }); + cx.foreground().run_until_parked(); + rs_editor.update(cx, |editor, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Rust inlay cache should change after the edit" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + 2, + "Every time hint cache changes, cache version should be incremented" + ); + }); + md_editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Markdown editor should not be affected by Rust editor changes" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 1); + }); + + md_editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input("some md change", cx); + }); + cx.foreground().run_until_parked(); + md_editor.update(cx, |editor, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Rust editor should not be affected by Markdown editor changes" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 2); + }); + rs_editor.update(cx, |editor, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Markdown editor should also change independently" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 2); + }); + } + + #[gpui::test] + async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) { + let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), + show_other_hints: allowed_hint_kinds.contains(&None), + }) + }); + + let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; + let lsp_request_count = Arc::new(AtomicU32::new(0)); + let another_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&another_lsp_request_count); + async move { + Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path(file_with_hints).unwrap(), + ); + Ok(Some(vec![ + lsp::InlayHint { + position: lsp::Position::new(0, 1), + label: lsp::InlayHintLabel::String("type hint".to_string()), + kind: Some(lsp::InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }, + lsp::InlayHint { + position: lsp::Position::new(0, 2), + label: lsp::InlayHintLabel::String("parameter hint".to_string()), + kind: Some(lsp::InlayHintKind::PARAMETER), + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }, + lsp::InlayHint { + position: lsp::Position::new(0, 3), + label: lsp::InlayHintLabel::String("other hint".to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }, + ])) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + + let mut edits_made = 1; + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 1, + "Should query new hints once" + ); + assert_eq!( + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + cached_hint_labels(editor), + "Should get its first hints when opening the editor" + ); + assert_eq!( + vec!["other hint".to_string(), "type hint".to_string()], + visible_hint_labels(editor, cx) + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.allowed_hint_kinds, allowed_hint_kinds, + "Cache should use editor settings to get the allowed hint kinds" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor update the cache version after every cache/view change" + ); + }); + + fake_server + .request::(()) + .await + .expect("inlay refresh request failed"); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 2, + "Should load new hints twice" + ); + assert_eq!( + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + cached_hint_labels(editor), + "Cached hints should not change due to allowed hint kinds settings update" + ); + assert_eq!( + vec!["other hint".to_string(), "type hint".to_string()], + visible_hint_labels(editor, cx) + ); + assert_eq!( + editor.inlay_hint_cache().version, + edits_made, + "Should not update cache version due to new loaded hints being the same" + ); + }); + + for (new_allowed_hint_kinds, expected_visible_hints) in [ + (HashSet::from_iter([None]), vec!["other hint".to_string()]), + ( + HashSet::from_iter([Some(InlayHintKind::Type)]), + vec!["type hint".to_string()], + ), + ( + HashSet::from_iter([Some(InlayHintKind::Parameter)]), + vec!["parameter hint".to_string()], + ), + ( + HashSet::from_iter([None, Some(InlayHintKind::Type)]), + vec!["other hint".to_string(), "type hint".to_string()], + ), + ( + HashSet::from_iter([None, Some(InlayHintKind::Parameter)]), + vec!["other hint".to_string(), "parameter hint".to_string()], + ), + ( + HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]), + vec!["parameter hint".to_string(), "type hint".to_string()], + ), + ( + HashSet::from_iter([ + None, + Some(InlayHintKind::Type), + Some(InlayHintKind::Parameter), + ]), + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + ), + ] { + edits_made += 1; + update_test_language_settings(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + show_parameter_hints: new_allowed_hint_kinds + .contains(&Some(InlayHintKind::Parameter)), + show_other_hints: new_allowed_hint_kinds.contains(&None), + }) + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 2, + "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}" + ); + assert_eq!( + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + cached_hint_labels(editor), + "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}" + ); + assert_eq!( + expected_visible_hints, + visible_hint_labels(editor, cx), + "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds, + "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor should update the cache version after every cache/view change for hint kinds {new_allowed_hint_kinds:?} due to visible hints change" + ); + }); + } + + edits_made += 1; + let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]); + update_test_language_settings(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: false, + show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + show_parameter_hints: another_allowed_hint_kinds + .contains(&Some(InlayHintKind::Parameter)), + show_other_hints: another_allowed_hint_kinds.contains(&None), + }) + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 2, + "Should not load new hints when hints got disabled" + ); + assert!( + cached_hint_labels(editor).is_empty(), + "Should clear the cache when hints got disabled" + ); + assert!( + visible_hint_labels(editor, cx).is_empty(), + "Should clear visible hints when hints got disabled" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds, + "Should update its allowed hint kinds even when hints got disabled" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor should update the cache version after hints got disabled" + ); + }); + + fake_server + .request::(()) + .await + .expect("inlay refresh request failed"); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 2, + "Should not load new hints when they got disabled" + ); + assert!(cached_hint_labels(editor).is_empty()); + assert!(visible_hint_labels(editor, cx).is_empty()); + assert_eq!( + editor.inlay_hint_cache().version, edits_made, + "The editor should not update the cache version after /refresh query without updates" + ); + }); + + let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]); + edits_made += 1; + update_test_language_settings(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + show_parameter_hints: final_allowed_hint_kinds + .contains(&Some(InlayHintKind::Parameter)), + show_other_hints: final_allowed_hint_kinds.contains(&None), + }) + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 3, + "Should query for new hints when they got reenabled" + ); + assert_eq!( + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + cached_hint_labels(editor), + "Should get its cached hints fully repopulated after the hints got reenabled" + ); + assert_eq!( + vec!["parameter hint".to_string()], + visible_hint_labels(editor, cx), + "Should get its visible hints repopulated and filtered after the h" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds, + "Cache should update editor settings when hints got reenabled" + ); + assert_eq!( + inlay_cache.version, edits_made, + "Cache should update its version after hints got reenabled" + ); + }); + + fake_server + .request::(()) + .await + .expect("inlay refresh request failed"); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 4, + "Should query for new hints again" + ); + assert_eq!( + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + cached_hint_labels(editor), + ); + assert_eq!( + vec!["parameter hint".to_string()], + visible_hint_labels(editor, cx), + ); + assert_eq!(editor.inlay_hint_cache().version, edits_made); + }); + } + + #[gpui::test] + async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; + let fake_server = Arc::new(fake_server); + let lsp_request_count = Arc::new(AtomicU32::new(0)); + let another_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&another_lsp_request_count); + async move { + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1; + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path(file_with_hints).unwrap(), + ); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, i), + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + + let mut expected_changes = Vec::new(); + for change_after_opening in [ + "initial change #1", + "initial change #2", + "initial change #3", + ] { + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input(change_after_opening, cx); + }); + expected_changes.push(change_after_opening); + } + + cx.foreground().run_until_parked(); + + editor.update(cx, |editor, cx| { + let current_text = editor.text(cx); + for change in &expected_changes { + assert!( + current_text.contains(change), + "Should apply all changes made" + ); + } + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 2, + "Should query new hints twice: for editor init and for the last edit that interrupted all others" + ); + let expected_hints = vec!["2".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get hints from the last edit landed only" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, 1, + "Only one update should be registered in the cache after all cancellations" + ); + }); + + let mut edits = Vec::new(); + for async_later_change in [ + "another change #1", + "another change #2", + "another change #3", + ] { + expected_changes.push(async_later_change); + let task_editor = editor.clone(); + let mut task_cx = cx.clone(); + edits.push(cx.foreground().spawn(async move { + task_editor.update(&mut task_cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input(async_later_change, cx); + }); + })); + } + let _ = future::join_all(edits).await; + cx.foreground().run_until_parked(); + + editor.update(cx, |editor, cx| { + let current_text = editor.text(cx); + for change in &expected_changes { + assert!( + current_text.contains(change), + "Should apply all changes made" + ); + } + assert_eq!( + lsp_request_count.load(Ordering::SeqCst), + 3, + "Should query new hints one more time, for the last edit only" + ); + let expected_hints = vec!["3".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get hints from the last edit landed only" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + 2, + "Should update the cache version once more, for the new change" + ); + }); + } + + #[gpui::test(iterations = 10)] + async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/a", + json!({ + "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)), + "other.rs": "// Test file", + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .root(cx); + let worktree_id = workspace.update(cx, |workspace, cx| { + workspace.project().read_with(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }); + + let _buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + cx.foreground().run_until_parked(); + cx.foreground().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + let editor = workspace + .update(cx, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let lsp_request_ranges = Arc::new(Mutex::new(Vec::new())); + let lsp_request_count = Arc::new(AtomicUsize::new(0)); + let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges); + let closure_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_ranges = Arc::clone(&closure_lsp_request_ranges); + let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + + task_lsp_request_ranges.lock().push(params.range); + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1; + Ok(Some(vec![lsp::InlayHint { + position: params.range.end, + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + fn editor_visible_range( + editor: &ViewHandle, + cx: &mut gpui::TestAppContext, + ) -> Range { + let ranges = editor.update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx)); + assert_eq!( + ranges.len(), + 1, + "Single buffer should produce a single excerpt with visible range" + ); + let (_, (excerpt_buffer, _, excerpt_visible_range)) = + ranges.into_iter().next().unwrap(); + excerpt_buffer.update(cx, |buffer, _| { + let snapshot = buffer.snapshot(); + let start = buffer + .anchor_before(excerpt_visible_range.start) + .to_point(&snapshot); + let end = buffer + .anchor_after(excerpt_visible_range.end) + .to_point(&snapshot); + start..end + }) + } + + // in large buffers, requests are made for more than visible range of a buffer. + // invisible parts are queried later, to avoid excessive requests on quick typing. + // wait the timeout needed to get all requests. + cx.foreground().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.foreground().run_until_parked(); + let initial_visible_range = editor_visible_range(&editor, cx); + let lsp_initial_visible_range = lsp::Range::new( + lsp::Position::new( + initial_visible_range.start.row, + initial_visible_range.start.column, + ), + lsp::Position::new( + initial_visible_range.end.row, + initial_visible_range.end.column, + ), + ); + let expected_initial_query_range_end = + lsp::Position::new(initial_visible_range.end.row * 2, 2); + let mut expected_invisible_query_start = lsp_initial_visible_range.end; + expected_invisible_query_start.character += 1; + editor.update(cx, |editor, cx| { + let ranges = lsp_request_ranges.lock().drain(..).collect::>(); + assert_eq!(ranges.len(), 2, + "When scroll is at the edge of a big document, its visible part and the same range further should be queried in order, but got: {ranges:?}"); + let visible_query_range = &ranges[0]; + assert_eq!(visible_query_range.start, lsp_initial_visible_range.start); + assert_eq!(visible_query_range.end, lsp_initial_visible_range.end); + let invisible_query_range = &ranges[1]; + + assert_eq!(invisible_query_range.start, expected_invisible_query_start, "Should initially query visible edge of the document"); + assert_eq!(invisible_query_range.end, expected_initial_query_range_end, "Should initially query visible edge of the document"); + + let requests_count = lsp_request_count.load(Ordering::Acquire); + assert_eq!(requests_count, 2, "Visible + invisible request"); + let expected_hints = vec!["1".to_string(), "2".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should have hints from both LSP requests made for a big file" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx), "Should display only hints from the visible range"); + assert_eq!( + editor.inlay_hint_cache().version, requests_count, + "LSP queries should've bumped the cache version" + ); + }); + + editor.update(cx, |editor, cx| { + editor.scroll_screen(&ScrollAmount::Page(1.0), cx); + editor.scroll_screen(&ScrollAmount::Page(1.0), cx); + }); + cx.foreground().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.foreground().run_until_parked(); + let visible_range_after_scrolls = editor_visible_range(&editor, cx); + let visible_line_count = + editor.update(cx, |editor, _| editor.visible_line_count().unwrap()); + let selection_in_cached_range = editor.update(cx, |editor, cx| { + let ranges = lsp_request_ranges + .lock() + .drain(..) + .sorted_by_key(|r| r.start) + .collect::>(); + assert_eq!( + ranges.len(), + 2, + "Should query 2 ranges after both scrolls, but got: {ranges:?}" + ); + let first_scroll = &ranges[0]; + let second_scroll = &ranges[1]; + assert_eq!( + first_scroll.end, second_scroll.start, + "Should query 2 adjacent ranges after the scrolls, but got: {ranges:?}" + ); + assert_eq!( + first_scroll.start, expected_initial_query_range_end, + "First scroll should start the query right after the end of the original scroll", + ); + assert_eq!( + second_scroll.end, + lsp::Position::new( + visible_range_after_scrolls.end.row + + visible_line_count.ceil() as u32, + 1, + ), + "Second scroll should query one more screen down after the end of the visible range" + ); + + let lsp_requests = lsp_request_count.load(Ordering::Acquire); + assert_eq!(lsp_requests, 4, "Should query for hints after every scroll"); + let expected_hints = vec![ + "1".to_string(), + "2".to_string(), + "3".to_string(), + "4".to_string(), + ]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should have hints from the new LSP response after the edit" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + lsp_requests, + "Should update the cache for every LSP response with hints added" + ); + + let mut selection_in_cached_range = visible_range_after_scrolls.end; + selection_in_cached_range.row -= visible_line_count.ceil() as u32; + selection_in_cached_range + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::center()), cx, |s| { + s.select_ranges([selection_in_cached_range..selection_in_cached_range]) + }); + }); + cx.foreground().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.foreground().run_until_parked(); + editor.update(cx, |_, _| { + let ranges = lsp_request_ranges + .lock() + .drain(..) + .sorted_by_key(|r| r.start) + .collect::>(); + assert!(ranges.is_empty(), "No new ranges or LSP queries should be made after returning to the selection with cached hints"); + assert_eq!(lsp_request_count.load(Ordering::Acquire), 4); + }); + + editor.update(cx, |editor, cx| { + editor.handle_input("++++more text++++", cx); + }); + cx.foreground().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + let mut ranges = lsp_request_ranges.lock().drain(..).collect::>(); + ranges.sort_by_key(|r| r.start); + + assert_eq!(ranges.len(), 3, + "On edit, should scroll to selection and query a range around it: visible + same range above and below. Instead, got query ranges {ranges:?}"); + let above_query_range = &ranges[0]; + let visible_query_range = &ranges[1]; + let below_query_range = &ranges[2]; + assert!(above_query_range.end.character < visible_query_range.start.character || above_query_range.end.line + 1 == visible_query_range.start.line, + "Above range {above_query_range:?} should be before visible range {visible_query_range:?}"); + assert!(visible_query_range.end.character < below_query_range.start.character || visible_query_range.end.line + 1 == below_query_range.start.line, + "Visible range {visible_query_range:?} should be before below range {below_query_range:?}"); + assert!(above_query_range.start.line < selection_in_cached_range.row, + "Hints should be queried with the selected range after the query range start"); + assert!(below_query_range.end.line > selection_in_cached_range.row, + "Hints should be queried with the selected range before the query range end"); + assert!(above_query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32, + "Hints query range should contain one more screen before"); + assert!(below_query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32, + "Hints query range should contain one more screen after"); + + let lsp_requests = lsp_request_count.load(Ordering::Acquire); + assert_eq!(lsp_requests, 7, "There should be a visible range and two ranges above and below it queried"); + let expected_hints = vec!["5".to_string(), "6".to_string(), "7".to_string()]; + assert_eq!(expected_hints, cached_hint_labels(editor), + "Should have hints from the new LSP response after the edit"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, lsp_requests, "Should update the cache for every LSP response with hints added"); + }); + } + + #[gpui::test(iterations = 10)] + async fn test_multiple_excerpts_large_multibuffer( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let language = Arc::new(language); + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/a", + json!({ + "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::>().join("")), + "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::>().join("")), + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| { + project.languages().add(Arc::clone(&language)) + }); + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .root(cx); + let worktree_id = workspace.update(cx, |workspace, cx| { + workspace.project().read_with(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }); + + let buffer_1 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "main.rs"), cx) + }) + .await + .unwrap(); + let buffer_2 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "other.rs"), cx) + }) + .await + .unwrap(); + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + buffer_1.clone(), + [ + ExcerptRange { + context: Point::new(0, 0)..Point::new(2, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(4, 0)..Point::new(11, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(22, 0)..Point::new(33, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(44, 0)..Point::new(55, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(56, 0)..Point::new(66, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(67, 0)..Point::new(77, 0), + primary: None, + }, + ], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ + ExcerptRange { + context: Point::new(0, 1)..Point::new(2, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(4, 1)..Point::new(11, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(22, 1)..Point::new(33, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(44, 1)..Point::new(55, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(56, 1)..Point::new(66, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(67, 1)..Point::new(77, 1), + primary: None, + }, + ], + cx, + ); + multibuffer + }); + + deterministic.run_until_parked(); + cx.foreground().run_until_parked(); + let editor = cx + .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)) + .root(cx); + let editor_edited = Arc::new(AtomicBool::new(false)); + let fake_server = fake_servers.next().await.unwrap(); + let closure_editor_edited = Arc::clone(&editor_edited); + fake_server + .handle_request::(move |params, _| { + let task_editor_edited = Arc::clone(&closure_editor_edited); + async move { + let hint_text = if params.text_document.uri + == lsp::Url::from_file_path("/a/main.rs").unwrap() + { + "main hint" + } else if params.text_document.uri + == lsp::Url::from_file_path("/a/other.rs").unwrap() + { + "other hint" + } else { + panic!("unexpected uri: {:?}", params.text_document.uri); + }; + + // one hint per excerpt + let positions = [ + lsp::Position::new(0, 2), + lsp::Position::new(4, 2), + lsp::Position::new(22, 2), + lsp::Position::new(44, 2), + lsp::Position::new(56, 2), + lsp::Position::new(67, 2), + ]; + let out_of_range_hint = lsp::InlayHint { + position: lsp::Position::new( + params.range.start.line + 99, + params.range.start.character + 99, + ), + label: lsp::InlayHintLabel::String( + "out of excerpt range, should be ignored".to_string(), + ), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }; + + let edited = task_editor_edited.load(Ordering::Acquire); + Ok(Some( + std::iter::once(out_of_range_hint) + .chain(positions.into_iter().enumerate().map(|(i, position)| { + lsp::InlayHint { + position, + label: lsp::InlayHintLabel::String(format!( + "{hint_text}{} #{i}", + if edited { "(edited)" } else { "" }, + )), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + } + })) + .collect(), + )) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + + editor.update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + ]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison"); + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) + }); + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]) + }); + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(50, 0)..Point::new(50, 0)]) + }); + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + "main hint #4".to_string(), + "main hint #5".to_string(), + "other hint #0".to_string(), + "other hint #1".to_string(), + "other hint #2".to_string(), + ]; + assert_eq!(expected_hints, cached_hint_labels(editor), + "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), + "Due to every excerpt having one hint, we update cache per new excerpt scrolled"); + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]) + }); + }); + cx.foreground().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.foreground().run_until_parked(); + let last_scroll_update_version = editor.update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + "main hint #4".to_string(), + "main hint #5".to_string(), + "other hint #0".to_string(), + "other hint #1".to_string(), + "other hint #2".to_string(), + "other hint #3".to_string(), + "other hint #4".to_string(), + "other hint #5".to_string(), + ]; + assert_eq!(expected_hints, cached_hint_labels(editor), + "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len()); + expected_hints.len() + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) + }); + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + "main hint #4".to_string(), + "main hint #5".to_string(), + "other hint #0".to_string(), + "other hint #1".to_string(), + "other hint #2".to_string(), + "other hint #3".to_string(), + "other hint #4".to_string(), + "other hint #5".to_string(), + ]; + assert_eq!(expected_hints, cached_hint_labels(editor), + "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer"); + }); + + editor_edited.store(true, Ordering::Release); + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(56, 0)..Point::new(56, 0)]) + }); + editor.handle_input("++++more text++++", cx); + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint(edited) #0".to_string(), + "main hint(edited) #1".to_string(), + "main hint(edited) #2".to_string(), + "main hint(edited) #3".to_string(), + "main hint(edited) #4".to_string(), + "main hint(edited) #5".to_string(), + "other hint(edited) #0".to_string(), + "other hint(edited) #1".to_string(), + ]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "After multibuffer edit, editor gets scolled back to the last selection; \ +all hints should be invalidated and requeried for all of its visible excerpts" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + + let current_cache_version = editor.inlay_hint_cache().version; + let minimum_expected_version = last_scroll_update_version + expected_hints.len(); + assert!( + current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1, + "Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update" + ); + }); + } + + #[gpui::test] + async fn test_excerpts_removed( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: false, + show_parameter_hints: false, + show_other_hints: false, + }) + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let language = Arc::new(language); + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/a", + json!({ + "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::>().join("")), + "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::>().join("")), + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| { + project.languages().add(Arc::clone(&language)) + }); + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .root(cx); + let worktree_id = workspace.update(cx, |workspace, cx| { + workspace.project().read_with(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }); + + let buffer_1 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "main.rs"), cx) + }) + .await + .unwrap(); + let buffer_2 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "other.rs"), cx) + }) + .await + .unwrap(); + let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); + let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| { + let buffer_1_excerpts = multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(2, 0), + primary: None, + }], + cx, + ); + let buffer_2_excerpts = multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange { + context: Point::new(0, 1)..Point::new(2, 1), + primary: None, + }], + cx, + ); + (buffer_1_excerpts, buffer_2_excerpts) + }); + + assert!(!buffer_1_excerpts.is_empty()); + assert!(!buffer_2_excerpts.is_empty()); + + deterministic.run_until_parked(); + cx.foreground().run_until_parked(); + let editor = cx + .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)) + .root(cx); + let editor_edited = Arc::new(AtomicBool::new(false)); + let fake_server = fake_servers.next().await.unwrap(); + let closure_editor_edited = Arc::clone(&editor_edited); + fake_server + .handle_request::(move |params, _| { + let task_editor_edited = Arc::clone(&closure_editor_edited); + async move { + let hint_text = if params.text_document.uri + == lsp::Url::from_file_path("/a/main.rs").unwrap() + { + "main hint" + } else if params.text_document.uri + == lsp::Url::from_file_path("/a/other.rs").unwrap() + { + "other hint" + } else { + panic!("unexpected uri: {:?}", params.text_document.uri); + }; + + let positions = [ + lsp::Position::new(0, 2), + lsp::Position::new(4, 2), + lsp::Position::new(22, 2), + lsp::Position::new(44, 2), + lsp::Position::new(56, 2), + lsp::Position::new(67, 2), + ]; + let out_of_range_hint = lsp::InlayHint { + position: lsp::Position::new( + params.range.start.line + 99, + params.range.start.character + 99, + ), + label: lsp::InlayHintLabel::String( + "out of excerpt range, should be ignored".to_string(), + ), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }; + + let edited = task_editor_edited.load(Ordering::Acquire); + Ok(Some( + std::iter::once(out_of_range_hint) + .chain(positions.into_iter().enumerate().map(|(i, position)| { + lsp::InlayHint { + position, + label: lsp::InlayHintLabel::String(format!( + "{hint_text}{} #{i}", + if edited { "(edited)" } else { "" }, + )), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + } + })) + .collect(), + )) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + + editor.update(cx, |editor, cx| { + assert_eq!( + vec!["main hint #0".to_string(), "other hint #0".to_string()], + cached_hint_labels(editor), + "Cache should update for both excerpts despite hints display was disabled" + ); + assert!( + visible_hint_labels(editor, cx).is_empty(), + "All hints are disabled and should not be shown despite being present in the cache" + ); + assert_eq!( + editor.inlay_hint_cache().version, + 2, + "Cache should update once per excerpt query" + ); + }); + + editor.update(cx, |editor, cx| { + editor.buffer().update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts(buffer_2_excerpts, cx) + }) + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + vec!["main hint #0".to_string()], + cached_hint_labels(editor), + "For the removed excerpt, should clean corresponding cached hints" + ); + assert!( + visible_hint_labels(editor, cx).is_empty(), + "All hints are disabled and should not be shown despite being present in the cache" + ); + assert_eq!( + editor.inlay_hint_cache().version, + 3, + "Excerpt removal should trigger a cache update" + ); + }); + + update_test_language_settings(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["main hint #0".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Hint display settings change should not change the cache" + ); + assert_eq!( + expected_hints, + visible_hint_labels(editor, cx), + "Settings change should make cached hints visible" + ); + assert_eq!( + editor.inlay_hint_cache().version, + 4, + "Settings change should trigger a cache update" + ); + }); + } + + #[gpui::test] + async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/a", + json!({ + "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "√".repeat(10)).repeat(500)), + "other.rs": "// Test file", + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .root(cx); + let worktree_id = workspace.update(cx, |workspace, cx| { + workspace.project().read_with(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }); + + let _buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + cx.foreground().run_until_parked(); + cx.foreground().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + let editor = workspace + .update(cx, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let lsp_request_count = Arc::new(AtomicU32::new(0)); + let closure_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + let query_start = params.range.start; + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1; + Ok(Some(vec![lsp::InlayHint { + position: query_start, + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(10, 0)..Point::new(10, 0)]) + }) + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!(expected_hints, cached_hint_labels(editor)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 1); + }); + } + + #[gpui::test] + async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: false, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; + + editor.update(cx, |editor, cx| { + editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) + }); + cx.foreground().start_waiting(); + let lsp_request_count = Arc::new(AtomicU32::new(0)); + let closure_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path(file_with_hints).unwrap(), + ); + + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1; + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, i), + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should display inlays after toggle despite them disabled in settings" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + 1, + "First toggle should be cache's first update" + ); + }); + + editor.update(cx, |editor, cx| { + editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + assert!( + cached_hint_labels(editor).is_empty(), + "Should clear hints after 2nd toggle" + ); + assert!(visible_hint_labels(editor, cx).is_empty()); + assert_eq!(editor.inlay_hint_cache().version, 2); + }); + + update_test_language_settings(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["2".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should query LSP hints for the 2nd time after enabling hints in settings" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 3); + }); + + editor.update(cx, |editor, cx| { + editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + assert!( + cached_hint_labels(editor).is_empty(), + "Should clear hints after enabling in settings and a 3rd toggle" + ); + assert!(visible_hint_labels(editor, cx).is_empty()); + assert_eq!(editor.inlay_hint_cache().version, 4); + }); + + editor.update(cx, |editor, cx| { + editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["3".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 5); + }); + } + + pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) { + cx.foreground().forbid_parking(); + + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + theme::init((), cx); + client::init_settings(cx); + language::init(cx); + Project::init_settings(cx); + workspace::init_settings(cx); + crate::init(cx); + }); + + update_test_language_settings(cx, f); + } + + async fn prepare_test_objects( + cx: &mut TestAppContext, + ) -> (&'static str, ViewHandle, FakeLanguageServer) { + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out", + "other.rs": "// Test file", + }), + ) + .await; + + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .root(cx); + let worktree_id = workspace.update(cx, |workspace, cx| { + workspace.project().read_with(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }); + + let _buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + cx.foreground().run_until_parked(); + cx.foreground().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + let editor = workspace + .update(cx, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + editor.update(cx, |editor, cx| { + assert!(cached_hint_labels(editor).is_empty()); + assert!(visible_hint_labels(editor, cx).is_empty()); + assert_eq!(editor.inlay_hint_cache().version, 0); + }); + + ("/a/main.rs", editor, fake_server) + } + + pub fn cached_hint_labels(editor: &Editor) -> Vec { + let mut labels = Vec::new(); + for (_, excerpt_hints) in &editor.inlay_hint_cache().hints { + let excerpt_hints = excerpt_hints.read(); + for id in &excerpt_hints.ordered_hints { + labels.push(excerpt_hints.hints_by_id[id].text()); + } + } + + labels.sort(); + labels + } + + pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec { + let mut hints = editor + .visible_inlay_hints(cx) + .into_iter() + .map(|hint| hint.text.to_string()) + .collect::>(); + hints.sort(); + hints + } +} diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs new file mode 100644 index 0000000000..c87606070e --- /dev/null +++ b/crates/editor2/src/items.rs @@ -0,0 +1,1327 @@ +use crate::{ + display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition, + movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, + Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, +}; +use anyhow::{Context, Result}; +use collections::HashSet; +use futures::future::try_join_all; +use gpui::{ + elements::*, + geometry::vector::{vec2f, Vector2F}, + AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View, ViewContext, + ViewHandle, WeakViewHandle, +}; +use language::{ + proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point, + SelectionGoal, +}; +use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath}; +use rpc::proto::{self, update_view, PeerId}; +use smallvec::SmallVec; +use std::{ + borrow::Cow, + cmp::{self, Ordering}, + fmt::Write, + iter, + ops::Range, + path::{Path, PathBuf}, + sync::Arc, +}; +use text::Selection; +use util::{ + paths::{PathExt, FILE_ROW_COLUMN_DELIMITER}, + ResultExt, TryFutureExt, +}; +use workspace::item::{BreadcrumbText, FollowableItemHandle}; +use workspace::{ + item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem}, + searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, + ItemId, ItemNavHistory, Pane, StatusItemView, ToolbarItemLocation, ViewId, Workspace, + WorkspaceId, +}; + +pub const MAX_TAB_TITLE_LEN: usize = 24; + +impl FollowableItem for Editor { + fn remote_id(&self) -> Option { + self.remote_id + } + + fn from_state_proto( + pane: ViewHandle, + workspace: ViewHandle, + remote_id: ViewId, + state: &mut Option, + cx: &mut AppContext, + ) -> Option>>> { + let project = workspace.read(cx).project().to_owned(); + let Some(proto::view::Variant::Editor(_)) = state else { + return None; + }; + let Some(proto::view::Variant::Editor(state)) = state.take() else { + unreachable!() + }; + + let client = project.read(cx).client(); + let replica_id = project.read(cx).replica_id(); + let buffer_ids = state + .excerpts + .iter() + .map(|excerpt| excerpt.buffer_id) + .collect::>(); + let buffers = project.update(cx, |project, cx| { + buffer_ids + .iter() + .map(|id| project.open_buffer_by_id(*id, cx)) + .collect::>() + }); + + let pane = pane.downgrade(); + Some(cx.spawn(|mut cx| async move { + let mut buffers = futures::future::try_join_all(buffers).await?; + let editor = pane.read_with(&cx, |pane, cx| { + let mut editors = pane.items_of_type::(); + editors.find(|editor| { + let ids_match = editor.remote_id(&client, cx) == Some(remote_id); + let singleton_buffer_matches = state.singleton + && buffers.first() + == editor.read(cx).buffer.read(cx).as_singleton().as_ref(); + ids_match || singleton_buffer_matches + }) + })?; + + let editor = if let Some(editor) = editor { + editor + } else { + pane.update(&mut cx, |_, cx| { + let multibuffer = cx.add_model(|cx| { + let mut multibuffer; + if state.singleton && buffers.len() == 1 { + multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx) + } else { + multibuffer = MultiBuffer::new(replica_id); + let mut excerpts = state.excerpts.into_iter().peekable(); + while let Some(excerpt) = excerpts.peek() { + let buffer_id = excerpt.buffer_id; + let buffer_excerpts = iter::from_fn(|| { + let excerpt = excerpts.peek()?; + (excerpt.buffer_id == buffer_id) + .then(|| excerpts.next().unwrap()) + }); + let buffer = + buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id); + if let Some(buffer) = buffer { + multibuffer.push_excerpts( + buffer.clone(), + buffer_excerpts.filter_map(deserialize_excerpt_range), + cx, + ); + } + } + }; + + if let Some(title) = &state.title { + multibuffer = multibuffer.with_title(title.clone()) + } + + multibuffer + }); + + cx.add_view(|cx| { + let mut editor = + Editor::for_multibuffer(multibuffer, Some(project.clone()), cx); + editor.remote_id = Some(remote_id); + editor + }) + })? + }; + + update_editor_from_message( + editor.downgrade(), + project, + proto::update_view::Editor { + selections: state.selections, + pending_selection: state.pending_selection, + scroll_top_anchor: state.scroll_top_anchor, + scroll_x: state.scroll_x, + scroll_y: state.scroll_y, + ..Default::default() + }, + &mut cx, + ) + .await?; + + Ok(editor) + })) + } + + fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext) { + self.leader_peer_id = leader_peer_id; + if self.leader_peer_id.is_some() { + self.buffer.update(cx, |buffer, cx| { + buffer.remove_active_selections(cx); + }); + } else { + self.buffer.update(cx, |buffer, cx| { + if self.focused { + buffer.set_active_selections( + &self.selections.disjoint_anchors(), + self.selections.line_mode, + self.cursor_shape, + cx, + ); + } + }); + } + cx.notify(); + } + + fn to_state_proto(&self, cx: &AppContext) -> Option { + let buffer = self.buffer.read(cx); + let scroll_anchor = self.scroll_manager.anchor(); + let excerpts = buffer + .read(cx) + .excerpts() + .map(|(id, buffer, range)| proto::Excerpt { + id: id.to_proto(), + buffer_id: buffer.remote_id(), + context_start: Some(serialize_text_anchor(&range.context.start)), + context_end: Some(serialize_text_anchor(&range.context.end)), + primary_start: range + .primary + .as_ref() + .map(|range| serialize_text_anchor(&range.start)), + primary_end: range + .primary + .as_ref() + .map(|range| serialize_text_anchor(&range.end)), + }) + .collect(); + + Some(proto::view::Variant::Editor(proto::view::Editor { + singleton: buffer.is_singleton(), + title: (!buffer.is_singleton()).then(|| buffer.title(cx).into()), + excerpts, + scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.anchor)), + scroll_x: scroll_anchor.offset.x(), + scroll_y: scroll_anchor.offset.y(), + selections: self + .selections + .disjoint_anchors() + .iter() + .map(serialize_selection) + .collect(), + pending_selection: self + .selections + .pending_anchor() + .as_ref() + .map(serialize_selection), + })) + } + + fn add_event_to_update_proto( + &self, + event: &Self::Event, + update: &mut Option, + cx: &AppContext, + ) -> bool { + let update = + update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default())); + + match update { + proto::update_view::Variant::Editor(update) => match event { + Event::ExcerptsAdded { + buffer, + predecessor, + excerpts, + } => { + let buffer_id = buffer.read(cx).remote_id(); + let mut excerpts = excerpts.iter(); + if let Some((id, range)) = excerpts.next() { + update.inserted_excerpts.push(proto::ExcerptInsertion { + previous_excerpt_id: Some(predecessor.to_proto()), + excerpt: serialize_excerpt(buffer_id, id, range), + }); + update.inserted_excerpts.extend(excerpts.map(|(id, range)| { + proto::ExcerptInsertion { + previous_excerpt_id: None, + excerpt: serialize_excerpt(buffer_id, id, range), + } + })) + } + true + } + Event::ExcerptsRemoved { ids } => { + update + .deleted_excerpts + .extend(ids.iter().map(ExcerptId::to_proto)); + true + } + Event::ScrollPositionChanged { .. } => { + let scroll_anchor = self.scroll_manager.anchor(); + update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.anchor)); + update.scroll_x = scroll_anchor.offset.x(); + update.scroll_y = scroll_anchor.offset.y(); + true + } + Event::SelectionsChanged { .. } => { + update.selections = self + .selections + .disjoint_anchors() + .iter() + .map(serialize_selection) + .collect(); + update.pending_selection = self + .selections + .pending_anchor() + .as_ref() + .map(serialize_selection); + true + } + _ => false, + }, + } + } + + fn apply_update_proto( + &mut self, + project: &ModelHandle, + message: update_view::Variant, + cx: &mut ViewContext, + ) -> Task> { + let update_view::Variant::Editor(message) = message; + let project = project.clone(); + cx.spawn(|this, mut cx| async move { + update_editor_from_message(this, project, message, &mut cx).await + }) + } + + fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool { + match event { + Event::Edited => true, + Event::SelectionsChanged { local } => *local, + Event::ScrollPositionChanged { local, .. } => *local, + _ => false, + } + } + + fn is_project_item(&self, _cx: &AppContext) -> bool { + true + } +} + +async fn update_editor_from_message( + this: WeakViewHandle, + project: ModelHandle, + message: proto::update_view::Editor, + cx: &mut AsyncAppContext, +) -> Result<()> { + // Open all of the buffers of which excerpts were added to the editor. + let inserted_excerpt_buffer_ids = message + .inserted_excerpts + .iter() + .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id)) + .collect::>(); + let inserted_excerpt_buffers = project.update(cx, |project, cx| { + inserted_excerpt_buffer_ids + .into_iter() + .map(|id| project.open_buffer_by_id(id, cx)) + .collect::>() + }); + let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?; + + // Update the editor's excerpts. + this.update(cx, |editor, cx| { + editor.buffer.update(cx, |multibuffer, cx| { + let mut removed_excerpt_ids = message + .deleted_excerpts + .into_iter() + .map(ExcerptId::from_proto) + .collect::>(); + removed_excerpt_ids.sort_by({ + let multibuffer = multibuffer.read(cx); + move |a, b| a.cmp(&b, &multibuffer) + }); + + let mut insertions = message.inserted_excerpts.into_iter().peekable(); + while let Some(insertion) = insertions.next() { + let Some(excerpt) = insertion.excerpt else { + continue; + }; + let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { + continue; + }; + let buffer_id = excerpt.buffer_id; + let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { + continue; + }; + + let adjacent_excerpts = iter::from_fn(|| { + let insertion = insertions.peek()?; + if insertion.previous_excerpt_id.is_none() + && insertion.excerpt.as_ref()?.buffer_id == buffer_id + { + insertions.next()?.excerpt + } else { + None + } + }); + + multibuffer.insert_excerpts_with_ids_after( + ExcerptId::from_proto(previous_excerpt_id), + buffer, + [excerpt] + .into_iter() + .chain(adjacent_excerpts) + .filter_map(|excerpt| { + Some(( + ExcerptId::from_proto(excerpt.id), + deserialize_excerpt_range(excerpt)?, + )) + }), + cx, + ); + } + + multibuffer.remove_excerpts(removed_excerpt_ids, cx); + }); + })?; + + // Deserialize the editor state. + let (selections, pending_selection, scroll_top_anchor) = this.update(cx, |editor, cx| { + let buffer = editor.buffer.read(cx).read(cx); + let selections = message + .selections + .into_iter() + .filter_map(|selection| deserialize_selection(&buffer, selection)) + .collect::>(); + let pending_selection = message + .pending_selection + .and_then(|selection| deserialize_selection(&buffer, selection)); + let scroll_top_anchor = message + .scroll_top_anchor + .and_then(|anchor| deserialize_anchor(&buffer, anchor)); + anyhow::Ok((selections, pending_selection, scroll_top_anchor)) + })??; + + // Wait until the buffer has received all of the operations referenced by + // the editor's new state. + this.update(cx, |editor, cx| { + editor.buffer.update(cx, |buffer, cx| { + buffer.wait_for_anchors( + selections + .iter() + .chain(pending_selection.as_ref()) + .flat_map(|selection| [selection.start, selection.end]) + .chain(scroll_top_anchor), + cx, + ) + }) + })? + .await?; + + // Update the editor's state. + this.update(cx, |editor, cx| { + if !selections.is_empty() || pending_selection.is_some() { + editor.set_selections_from_remote(selections, pending_selection, cx); + editor.request_autoscroll_remotely(Autoscroll::newest(), cx); + } else if let Some(scroll_top_anchor) = scroll_top_anchor { + editor.set_scroll_anchor_remote( + ScrollAnchor { + anchor: scroll_top_anchor, + offset: vec2f(message.scroll_x, message.scroll_y), + }, + cx, + ); + } + })?; + Ok(()) +} + +fn serialize_excerpt( + buffer_id: u64, + id: &ExcerptId, + range: &ExcerptRange, +) -> Option { + Some(proto::Excerpt { + id: id.to_proto(), + buffer_id, + context_start: Some(serialize_text_anchor(&range.context.start)), + context_end: Some(serialize_text_anchor(&range.context.end)), + primary_start: range + .primary + .as_ref() + .map(|r| serialize_text_anchor(&r.start)), + primary_end: range + .primary + .as_ref() + .map(|r| serialize_text_anchor(&r.end)), + }) +} + +fn serialize_selection(selection: &Selection) -> proto::Selection { + proto::Selection { + id: selection.id as u64, + start: Some(serialize_anchor(&selection.start)), + end: Some(serialize_anchor(&selection.end)), + reversed: selection.reversed, + } +} + +fn serialize_anchor(anchor: &Anchor) -> proto::EditorAnchor { + proto::EditorAnchor { + excerpt_id: anchor.excerpt_id.to_proto(), + anchor: Some(serialize_text_anchor(&anchor.text_anchor)), + } +} + +fn deserialize_excerpt_range(excerpt: proto::Excerpt) -> Option> { + let context = { + let start = language::proto::deserialize_anchor(excerpt.context_start?)?; + let end = language::proto::deserialize_anchor(excerpt.context_end?)?; + start..end + }; + let primary = excerpt + .primary_start + .zip(excerpt.primary_end) + .and_then(|(start, end)| { + let start = language::proto::deserialize_anchor(start)?; + let end = language::proto::deserialize_anchor(end)?; + Some(start..end) + }); + Some(ExcerptRange { context, primary }) +} + +fn deserialize_selection( + buffer: &MultiBufferSnapshot, + selection: proto::Selection, +) -> Option> { + Some(Selection { + id: selection.id as usize, + start: deserialize_anchor(buffer, selection.start?)?, + end: deserialize_anchor(buffer, selection.end?)?, + reversed: selection.reversed, + goal: SelectionGoal::None, + }) +} + +fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor) -> Option { + let excerpt_id = ExcerptId::from_proto(anchor.excerpt_id); + Some(Anchor { + excerpt_id, + text_anchor: language::proto::deserialize_anchor(anchor.anchor?)?, + buffer_id: buffer.buffer_id_for_excerpt(excerpt_id), + }) +} + +impl Item for Editor { + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { + if let Ok(data) = data.downcast::() { + let newest_selection = self.selections.newest::(cx); + let buffer = self.buffer.read(cx).read(cx); + let offset = if buffer.can_resolve(&data.cursor_anchor) { + data.cursor_anchor.to_point(&buffer) + } else { + buffer.clip_point(data.cursor_position, Bias::Left) + }; + + let mut scroll_anchor = data.scroll_anchor; + if !buffer.can_resolve(&scroll_anchor.anchor) { + scroll_anchor.anchor = buffer.anchor_before( + buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left), + ); + } + + drop(buffer); + + if newest_selection.head() == offset { + false + } else { + let nav_history = self.nav_history.take(); + self.set_scroll_anchor(scroll_anchor, cx); + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([offset..offset]) + }); + self.nav_history = nav_history; + true + } + } else { + false + } + } + + fn tab_tooltip_text(&self, cx: &AppContext) -> Option> { + let file_path = self + .buffer() + .read(cx) + .as_singleton()? + .read(cx) + .file() + .and_then(|f| f.as_local())? + .abs_path(cx); + + let file_path = file_path.compact().to_string_lossy().to_string(); + + Some(file_path.into()) + } + + fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option> { + match path_for_buffer(&self.buffer, detail, true, cx)? { + Cow::Borrowed(path) => Some(path.to_string_lossy()), + Cow::Owned(path) => Some(path.to_string_lossy().to_string().into()), + } + } + + fn tab_content( + &self, + detail: Option, + style: &theme::Tab, + cx: &AppContext, + ) -> AnyElement { + Flex::row() + .with_child(Label::new(self.title(cx).to_string(), style.label.clone()).into_any()) + .with_children(detail.and_then(|detail| { + let path = path_for_buffer(&self.buffer, detail, false, cx)?; + let description = path.to_string_lossy(); + Some( + Label::new( + util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN), + style.description.text.clone(), + ) + .contained() + .with_style(style.description.container) + .aligned(), + ) + })) + .align_children_center() + .into_any() + } + + fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) { + self.buffer + .read(cx) + .for_each_buffer(|buffer| f(buffer.id(), buffer.read(cx))); + } + + fn is_singleton(&self, cx: &AppContext) -> bool { + self.buffer.read(cx).is_singleton() + } + + fn clone_on_split(&self, _workspace_id: WorkspaceId, cx: &mut ViewContext) -> Option + where + Self: Sized, + { + Some(self.clone(cx)) + } + + fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { + self.nav_history = Some(history); + } + + fn deactivated(&mut self, cx: &mut ViewContext) { + let selection = self.selections.newest_anchor(); + self.push_to_nav_history(selection.head(), None, cx); + } + + fn workspace_deactivated(&mut self, cx: &mut ViewContext) { + hide_link_definition(self, cx); + self.link_go_to_definition_state.last_trigger_point = None; + } + + fn is_dirty(&self, cx: &AppContext) -> bool { + self.buffer().read(cx).read(cx).is_dirty() + } + + fn has_conflict(&self, cx: &AppContext) -> bool { + self.buffer().read(cx).read(cx).has_conflict() + } + + fn can_save(&self, cx: &AppContext) -> bool { + let buffer = &self.buffer().read(cx); + if let Some(buffer) = buffer.as_singleton() { + buffer.read(cx).project_path(cx).is_some() + } else { + true + } + } + + fn save( + &mut self, + project: ModelHandle, + cx: &mut ViewContext, + ) -> Task> { + self.report_editor_event("save", None, cx); + let format = self.perform_format(project.clone(), FormatTrigger::Save, cx); + let buffers = self.buffer().clone().read(cx).all_buffers(); + cx.spawn(|_, mut cx| async move { + format.await?; + + if buffers.len() == 1 { + project + .update(&mut cx, |project, cx| project.save_buffers(buffers, cx)) + .await?; + } else { + // For multi-buffers, only save those ones that contain changes. For clean buffers + // we simulate saving by calling `Buffer::did_save`, so that language servers or + // other downstream listeners of save events get notified. + let (dirty_buffers, clean_buffers) = buffers.into_iter().partition(|buffer| { + buffer.read_with(&cx, |buffer, _| buffer.is_dirty() || buffer.has_conflict()) + }); + + project + .update(&mut cx, |project, cx| { + project.save_buffers(dirty_buffers, cx) + }) + .await?; + for buffer in clean_buffers { + buffer.update(&mut cx, |buffer, cx| { + let version = buffer.saved_version().clone(); + let fingerprint = buffer.saved_version_fingerprint(); + let mtime = buffer.saved_mtime(); + buffer.did_save(version, fingerprint, mtime, cx); + }); + } + } + + Ok(()) + }) + } + + fn save_as( + &mut self, + project: ModelHandle, + abs_path: PathBuf, + cx: &mut ViewContext, + ) -> Task> { + let buffer = self + .buffer() + .read(cx) + .as_singleton() + .expect("cannot call save_as on an excerpt list"); + + let file_extension = abs_path + .extension() + .map(|a| a.to_string_lossy().to_string()); + self.report_editor_event("save", file_extension, cx); + + project.update(cx, |project, cx| { + project.save_buffer_as(buffer, abs_path, cx) + }) + } + + fn reload( + &mut self, + project: ModelHandle, + cx: &mut ViewContext, + ) -> Task> { + let buffer = self.buffer().clone(); + let buffers = self.buffer.read(cx).all_buffers(); + let reload_buffers = + project.update(cx, |project, cx| project.reload_buffers(buffers, true, cx)); + cx.spawn(|this, mut cx| async move { + let transaction = reload_buffers.log_err().await; + this.update(&mut cx, |editor, cx| { + editor.request_autoscroll(Autoscroll::fit(), cx) + })?; + buffer.update(&mut cx, |buffer, cx| { + if let Some(transaction) = transaction { + if !buffer.is_singleton() { + buffer.push_transaction(&transaction.0, cx); + } + } + }); + Ok(()) + }) + } + + fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { + let mut result = SmallVec::new(); + match event { + Event::Closed => result.push(ItemEvent::CloseItem), + Event::Saved | Event::TitleChanged => { + result.push(ItemEvent::UpdateTab); + result.push(ItemEvent::UpdateBreadcrumbs); + } + Event::Reparsed => { + result.push(ItemEvent::UpdateBreadcrumbs); + } + Event::SelectionsChanged { local } if *local => { + result.push(ItemEvent::UpdateBreadcrumbs); + } + Event::DirtyChanged => { + result.push(ItemEvent::UpdateTab); + } + Event::BufferEdited => { + result.push(ItemEvent::Edit); + result.push(ItemEvent::UpdateBreadcrumbs); + } + _ => {} + } + result + } + + fn as_searchable(&self, handle: &ViewHandle) -> Option> { + Some(Box::new(handle.clone())) + } + + fn pixel_position_of_cursor(&self, _: &AppContext) -> Option { + self.pixel_position_of_newest_cursor + } + + fn breadcrumb_location(&self) -> ToolbarItemLocation { + ToolbarItemLocation::PrimaryLeft { flex: None } + } + + fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option> { + let cursor = self.selections.newest_anchor().head(); + let multibuffer = &self.buffer().read(cx); + let (buffer_id, symbols) = + multibuffer.symbols_containing(cursor, Some(&theme.editor.syntax), cx)?; + let buffer = multibuffer.buffer(buffer_id)?; + + let buffer = buffer.read(cx); + let filename = buffer + .snapshot() + .resolve_file_path( + cx, + self.project + .as_ref() + .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) + .unwrap_or_default(), + ) + .map(|path| path.to_string_lossy().to_string()) + .unwrap_or_else(|| "untitled".to_string()); + + let mut breadcrumbs = vec![BreadcrumbText { + text: filename, + highlights: None, + }]; + breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText { + text: symbol.text, + highlights: Some(symbol.highlight_ranges), + })); + Some(breadcrumbs) + } + + fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { + let workspace_id = workspace.database_id(); + let item_id = cx.view_id(); + self.workspace = Some((workspace.weak_handle(), workspace.database_id())); + + fn serialize( + buffer: ModelHandle, + workspace_id: WorkspaceId, + item_id: ItemId, + cx: &mut AppContext, + ) { + if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) { + let path = file.abs_path(cx); + + cx.background() + .spawn(async move { + DB.save_path(item_id, workspace_id, path.clone()) + .await + .log_err() + }) + .detach(); + } + } + + if let Some(buffer) = self.buffer().read(cx).as_singleton() { + serialize(buffer.clone(), workspace_id, item_id, cx); + + cx.subscribe(&buffer, |this, buffer, event, cx| { + if let Some((_, workspace_id)) = this.workspace.as_ref() { + if let language::Event::FileHandleChanged = event { + serialize(buffer, *workspace_id, cx.view_id(), cx); + } + } + }) + .detach(); + } + } + + fn serialized_item_kind() -> Option<&'static str> { + Some("Editor") + } + + fn deserialize( + project: ModelHandle, + _workspace: WeakViewHandle, + workspace_id: workspace::WorkspaceId, + item_id: ItemId, + cx: &mut ViewContext, + ) -> Task>> { + let project_item: Result<_> = project.update(cx, |project, cx| { + // Look up the path with this key associated, create a self with that path + let path = DB + .get_path(item_id, workspace_id)? + .context("No path stored for this editor")?; + + let (worktree, path) = project + .find_local_worktree(&path, cx) + .with_context(|| format!("No worktree for path: {path:?}"))?; + let project_path = ProjectPath { + worktree_id: worktree.read(cx).id(), + path: path.into(), + }; + + Ok(project.open_path(project_path, cx)) + }); + + project_item + .map(|project_item| { + cx.spawn(|pane, mut cx| async move { + let (_, project_item) = project_item.await?; + let buffer = project_item + .downcast::() + .context("Project item at stored path was not a buffer")?; + Ok(pane.update(&mut cx, |_, cx| { + cx.add_view(|cx| { + let mut editor = Editor::for_buffer(buffer, Some(project), cx); + editor.read_scroll_position_from_db(item_id, workspace_id, cx); + editor + }) + })?) + }) + }) + .unwrap_or_else(|error| Task::ready(Err(error))) + } +} + +impl ProjectItem for Editor { + type Item = Buffer; + + fn for_project_item( + project: ModelHandle, + buffer: ModelHandle, + cx: &mut ViewContext, + ) -> Self { + Self::for_buffer(buffer, Some(project), cx) + } +} + +pub(crate) enum BufferSearchHighlights {} +impl SearchableItem for Editor { + type Match = Range; + + fn to_search_event( + &mut self, + event: &Self::Event, + _: &mut ViewContext, + ) -> Option { + match event { + Event::BufferEdited => Some(SearchEvent::MatchesInvalidated), + Event::SelectionsChanged { .. } => { + if self.selections.disjoint_anchors().len() == 1 { + Some(SearchEvent::ActiveMatchChanged) + } else { + None + } + } + _ => None, + } + } + + fn clear_matches(&mut self, cx: &mut ViewContext) { + self.clear_background_highlights::(cx); + } + + fn update_matches(&mut self, matches: Vec>, cx: &mut ViewContext) { + self.highlight_background::( + matches, + |theme| theme.search.match_background, + cx, + ); + } + + fn query_suggestion(&mut self, cx: &mut ViewContext) -> String { + let display_map = self.snapshot(cx).display_snapshot; + let selection = self.selections.newest::(cx); + if selection.start == selection.end { + let point = selection.start.to_display_point(&display_map); + let range = surrounding_word(&display_map, point); + let range = range.start.to_offset(&display_map, Bias::Left) + ..range.end.to_offset(&display_map, Bias::Right); + let text: String = display_map.buffer_snapshot.text_for_range(range).collect(); + if text.trim().is_empty() { + String::new() + } else { + text + } + } else { + display_map + .buffer_snapshot + .text_for_range(selection.start..selection.end) + .collect() + } + } + + fn activate_match( + &mut self, + index: usize, + matches: Vec>, + cx: &mut ViewContext, + ) { + self.unfold_ranges([matches[index].clone()], false, true, cx); + let range = self.range_for_match(&matches[index]); + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([range]); + }) + } + + fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext) { + self.unfold_ranges(matches.clone(), false, false, cx); + let mut ranges = Vec::new(); + for m in &matches { + ranges.push(self.range_for_match(&m)) + } + self.change_selections(None, cx, |s| s.select_ranges(ranges)); + } + fn replace( + &mut self, + identifier: &Self::Match, + query: &SearchQuery, + cx: &mut ViewContext, + ) { + let text = self.buffer.read(cx); + let text = text.snapshot(cx); + let text = text.text_for_range(identifier.clone()).collect::>(); + let text: Cow<_> = if text.len() == 1 { + text.first().cloned().unwrap().into() + } else { + let joined_chunks = text.join(""); + joined_chunks.into() + }; + + if let Some(replacement) = query.replacement_for(&text) { + self.transact(cx, |this, cx| { + this.edit([(identifier.clone(), Arc::from(&*replacement))], cx); + }); + } + } + fn match_index_for_direction( + &mut self, + matches: &Vec>, + current_index: usize, + direction: Direction, + count: usize, + cx: &mut ViewContext, + ) -> usize { + let buffer = self.buffer().read(cx).snapshot(cx); + let current_index_position = if self.selections.disjoint_anchors().len() == 1 { + self.selections.newest_anchor().head() + } else { + matches[current_index].start + }; + + let mut count = count % matches.len(); + if count == 0 { + return current_index; + } + match direction { + Direction::Next => { + if matches[current_index] + .start + .cmp(¤t_index_position, &buffer) + .is_gt() + { + count = count - 1 + } + + (current_index + count) % matches.len() + } + Direction::Prev => { + if matches[current_index] + .end + .cmp(¤t_index_position, &buffer) + .is_lt() + { + count = count - 1; + } + + if current_index >= count { + current_index - count + } else { + matches.len() - (count - current_index) + } + } + } + } + + fn find_matches( + &mut self, + query: Arc, + cx: &mut ViewContext, + ) -> Task>> { + let buffer = self.buffer().read(cx).snapshot(cx); + cx.background().spawn(async move { + let mut ranges = Vec::new(); + if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() { + ranges.extend( + query + .search(excerpt_buffer, None) + .await + .into_iter() + .map(|range| { + buffer.anchor_after(range.start)..buffer.anchor_before(range.end) + }), + ); + } else { + for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) { + let excerpt_range = excerpt.range.context.to_offset(&excerpt.buffer); + ranges.extend( + query + .search(&excerpt.buffer, Some(excerpt_range.clone())) + .await + .into_iter() + .map(|range| { + let start = excerpt + .buffer + .anchor_after(excerpt_range.start + range.start); + let end = excerpt + .buffer + .anchor_before(excerpt_range.start + range.end); + buffer.anchor_in_excerpt(excerpt.id.clone(), start) + ..buffer.anchor_in_excerpt(excerpt.id.clone(), end) + }), + ); + } + } + ranges + }) + } + + fn active_match_index( + &mut self, + matches: Vec>, + cx: &mut ViewContext, + ) -> Option { + active_match_index( + &matches, + &self.selections.newest_anchor().head(), + &self.buffer().read(cx).snapshot(cx), + ) + } +} + +pub fn active_match_index( + ranges: &[Range], + cursor: &Anchor, + buffer: &MultiBufferSnapshot, +) -> Option { + if ranges.is_empty() { + None + } else { + match ranges.binary_search_by(|probe| { + if probe.end.cmp(cursor, &*buffer).is_lt() { + Ordering::Less + } else if probe.start.cmp(cursor, &*buffer).is_gt() { + Ordering::Greater + } else { + Ordering::Equal + } + }) { + Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)), + } + } +} + +pub struct CursorPosition { + position: Option, + selected_count: usize, + _observe_active_editor: Option, +} + +impl Default for CursorPosition { + fn default() -> Self { + Self::new() + } +} + +impl CursorPosition { + pub fn new() -> Self { + Self { + position: None, + selected_count: 0, + _observe_active_editor: None, + } + } + + fn update_position(&mut self, editor: ViewHandle, cx: &mut ViewContext) { + let editor = editor.read(cx); + let buffer = editor.buffer().read(cx).snapshot(cx); + + self.selected_count = 0; + let mut last_selection: Option> = None; + for selection in editor.selections.all::(cx) { + self.selected_count += selection.end - selection.start; + if last_selection + .as_ref() + .map_or(true, |last_selection| selection.id > last_selection.id) + { + last_selection = Some(selection); + } + } + self.position = last_selection.map(|s| s.head().to_point(&buffer)); + + cx.notify(); + } +} + +impl Entity for CursorPosition { + type Event = (); +} + +impl View for CursorPosition { + fn ui_name() -> &'static str { + "CursorPosition" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + if let Some(position) = self.position { + let theme = &theme::current(cx).workspace.status_bar; + let mut text = format!( + "{}{FILE_ROW_COLUMN_DELIMITER}{}", + position.row + 1, + position.column + 1 + ); + if self.selected_count > 0 { + write!(text, " ({} selected)", self.selected_count).unwrap(); + } + Label::new(text, theme.cursor_position.clone()).into_any() + } else { + Empty::new().into_any() + } + } +} + +impl StatusItemView for CursorPosition { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) { + if let Some(editor) = active_pane_item.and_then(|item| item.act_as::(cx)) { + self._observe_active_editor = Some(cx.observe(&editor, Self::update_position)); + self.update_position(editor, cx); + } else { + self.position = None; + self._observe_active_editor = None; + } + + cx.notify(); + } +} + +fn path_for_buffer<'a>( + buffer: &ModelHandle, + height: usize, + include_filename: bool, + cx: &'a AppContext, +) -> Option> { + let file = buffer.read(cx).as_singleton()?.read(cx).file()?; + path_for_file(file.as_ref(), height, include_filename, cx) +} + +fn path_for_file<'a>( + file: &'a dyn language::File, + mut height: usize, + include_filename: bool, + cx: &'a AppContext, +) -> Option> { + // Ensure we always render at least the filename. + height += 1; + + let mut prefix = file.path().as_ref(); + while height > 0 { + if let Some(parent) = prefix.parent() { + prefix = parent; + height -= 1; + } else { + break; + } + } + + // Here we could have just always used `full_path`, but that is very + // allocation-heavy and so we try to use a `Cow` if we haven't + // traversed all the way up to the worktree's root. + if height > 0 { + let full_path = file.full_path(cx); + if include_filename { + Some(full_path.into()) + } else { + Some(full_path.parent()?.to_path_buf().into()) + } + } else { + let mut path = file.path().strip_prefix(prefix).ok()?; + if !include_filename { + path = path.parent()?; + } + Some(path.into()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use gpui::AppContext; + use std::{ + path::{Path, PathBuf}, + sync::Arc, + time::SystemTime, + }; + + #[gpui::test] + fn test_path_for_file(cx: &mut AppContext) { + let file = TestFile { + path: Path::new("").into(), + full_path: PathBuf::from(""), + }; + assert_eq!(path_for_file(&file, 0, false, cx), None); + } + + struct TestFile { + path: Arc, + full_path: PathBuf, + } + + impl language::File for TestFile { + fn path(&self) -> &Arc { + &self.path + } + + fn full_path(&self, _: &gpui::AppContext) -> PathBuf { + self.full_path.clone() + } + + fn as_local(&self) -> Option<&dyn language::LocalFile> { + unimplemented!() + } + + fn mtime(&self) -> SystemTime { + unimplemented!() + } + + fn file_name<'a>(&'a self, _: &'a gpui::AppContext) -> &'a std::ffi::OsStr { + unimplemented!() + } + + fn worktree_id(&self) -> usize { + 0 + } + + fn is_deleted(&self) -> bool { + unimplemented!() + } + + fn as_any(&self) -> &dyn std::any::Any { + unimplemented!() + } + + fn to_proto(&self) -> rpc::proto::File { + unimplemented!() + } + } +} diff --git a/crates/editor2/src/link_go_to_definition.rs b/crates/editor2/src/link_go_to_definition.rs new file mode 100644 index 0000000000..7da0b88622 --- /dev/null +++ b/crates/editor2/src/link_go_to_definition.rs @@ -0,0 +1,1269 @@ +use crate::{ + display_map::DisplaySnapshot, + element::PointForPosition, + hover_popover::{self, InlayHover}, + Anchor, DisplayPoint, Editor, EditorSnapshot, InlayId, SelectPhase, +}; +use gpui::{Task, ViewContext}; +use language::{Bias, ToOffset}; +use lsp::LanguageServerId; +use project::{ + HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink, + ResolveState, +}; +use std::ops::Range; +use util::TryFutureExt; + +#[derive(Debug, Default)] +pub struct LinkGoToDefinitionState { + pub last_trigger_point: Option, + pub symbol_range: Option, + pub kind: Option, + pub definitions: Vec, + pub task: Option>>, +} + +#[derive(Debug, Eq, PartialEq, Clone)] +pub enum RangeInEditor { + Text(Range), + Inlay(InlayHighlight), +} + +impl RangeInEditor { + pub fn as_text_range(&self) -> Option> { + match self { + Self::Text(range) => Some(range.clone()), + Self::Inlay(_) => None, + } + } + + fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool { + match (self, trigger_point) { + (Self::Text(range), TriggerPoint::Text(point)) => { + let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le(); + point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge() + } + (Self::Inlay(highlight), TriggerPoint::InlayHint(point, _, _)) => { + highlight.inlay == point.inlay + && highlight.range.contains(&point.range.start) + && highlight.range.contains(&point.range.end) + } + (Self::Inlay(_), TriggerPoint::Text(_)) + | (Self::Text(_), TriggerPoint::InlayHint(_, _, _)) => false, + } + } +} + +#[derive(Debug)] +pub enum GoToDefinitionTrigger { + Text(DisplayPoint), + InlayHint(InlayHighlight, lsp::Location, LanguageServerId), +} + +#[derive(Debug, Clone)] +pub enum GoToDefinitionLink { + Text(LocationLink), + InlayHint(lsp::Location, LanguageServerId), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InlayHighlight { + pub inlay: InlayId, + pub inlay_position: Anchor, + pub range: Range, +} + +#[derive(Debug, Clone)] +pub enum TriggerPoint { + Text(Anchor), + InlayHint(InlayHighlight, lsp::Location, LanguageServerId), +} + +impl TriggerPoint { + pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind { + match self { + TriggerPoint::Text(_) => { + if shift { + LinkDefinitionKind::Type + } else { + LinkDefinitionKind::Symbol + } + } + TriggerPoint::InlayHint(_, _, _) => LinkDefinitionKind::Type, + } + } + + fn anchor(&self) -> &Anchor { + match self { + TriggerPoint::Text(anchor) => anchor, + TriggerPoint::InlayHint(inlay_range, _, _) => &inlay_range.inlay_position, + } + } +} + +pub fn update_go_to_definition_link( + editor: &mut Editor, + origin: Option, + cmd_held: bool, + shift_held: bool, + cx: &mut ViewContext, +) { + let pending_nonempty_selection = editor.has_pending_nonempty_selection(); + + // Store new mouse point as an anchor + let snapshot = editor.snapshot(cx); + let trigger_point = match origin { + Some(GoToDefinitionTrigger::Text(p)) => { + Some(TriggerPoint::Text(snapshot.buffer_snapshot.anchor_before( + p.to_offset(&snapshot.display_snapshot, Bias::Left), + ))) + } + Some(GoToDefinitionTrigger::InlayHint(p, lsp_location, language_server_id)) => { + Some(TriggerPoint::InlayHint(p, lsp_location, language_server_id)) + } + None => None, + }; + + // If the new point is the same as the previously stored one, return early + if let (Some(a), Some(b)) = ( + &trigger_point, + &editor.link_go_to_definition_state.last_trigger_point, + ) { + match (a, b) { + (TriggerPoint::Text(anchor_a), TriggerPoint::Text(anchor_b)) => { + if anchor_a.cmp(anchor_b, &snapshot.buffer_snapshot).is_eq() { + return; + } + } + (TriggerPoint::InlayHint(range_a, _, _), TriggerPoint::InlayHint(range_b, _, _)) => { + if range_a == range_b { + return; + } + } + _ => {} + } + } + + editor.link_go_to_definition_state.last_trigger_point = trigger_point.clone(); + + if pending_nonempty_selection { + hide_link_definition(editor, cx); + return; + } + + if cmd_held { + if let Some(trigger_point) = trigger_point { + let kind = trigger_point.definition_kind(shift_held); + show_link_definition(kind, editor, trigger_point, snapshot, cx); + return; + } + } + + hide_link_definition(editor, cx); +} + +pub fn update_inlay_link_and_hover_points( + snapshot: &DisplaySnapshot, + point_for_position: PointForPosition, + editor: &mut Editor, + cmd_held: bool, + shift_held: bool, + cx: &mut ViewContext<'_, '_, Editor>, +) { + let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 { + Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left)) + } else { + None + }; + let mut go_to_definition_updated = false; + let mut hover_updated = false; + if let Some(hovered_offset) = hovered_offset { + let buffer_snapshot = editor.buffer().read(cx).snapshot(cx); + let previous_valid_anchor = buffer_snapshot.anchor_at( + point_for_position.previous_valid.to_point(snapshot), + Bias::Left, + ); + let next_valid_anchor = buffer_snapshot.anchor_at( + point_for_position.next_valid.to_point(snapshot), + Bias::Right, + ); + if let Some(hovered_hint) = editor + .visible_inlay_hints(cx) + .into_iter() + .skip_while(|hint| { + hint.position + .cmp(&previous_valid_anchor, &buffer_snapshot) + .is_lt() + }) + .take_while(|hint| { + hint.position + .cmp(&next_valid_anchor, &buffer_snapshot) + .is_le() + }) + .max_by_key(|hint| hint.id) + { + let inlay_hint_cache = editor.inlay_hint_cache(); + let excerpt_id = previous_valid_anchor.excerpt_id; + if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) { + match cached_hint.resolve_state { + ResolveState::CanResolve(_, _) => { + if let Some(buffer_id) = previous_valid_anchor.buffer_id { + inlay_hint_cache.spawn_hint_resolve( + buffer_id, + excerpt_id, + hovered_hint.id, + cx, + ); + } + } + ResolveState::Resolved => { + let mut extra_shift_left = 0; + let mut extra_shift_right = 0; + if cached_hint.padding_left { + extra_shift_left += 1; + extra_shift_right += 1; + } + if cached_hint.padding_right { + extra_shift_right += 1; + } + match cached_hint.label { + project::InlayHintLabel::String(_) => { + if let Some(tooltip) = cached_hint.tooltip { + hover_popover::hover_at_inlay( + editor, + InlayHover { + excerpt: excerpt_id, + tooltip: match tooltip { + InlayHintTooltip::String(text) => HoverBlock { + text, + kind: HoverBlockKind::PlainText, + }, + InlayHintTooltip::MarkupContent(content) => { + HoverBlock { + text: content.value, + kind: content.kind, + } + } + }, + range: InlayHighlight { + inlay: hovered_hint.id, + inlay_position: hovered_hint.position, + range: extra_shift_left + ..hovered_hint.text.len() + extra_shift_right, + }, + }, + cx, + ); + hover_updated = true; + } + } + project::InlayHintLabel::LabelParts(label_parts) => { + let hint_start = + snapshot.anchor_to_inlay_offset(hovered_hint.position); + if let Some((hovered_hint_part, part_range)) = + hover_popover::find_hovered_hint_part( + label_parts, + hint_start, + hovered_offset, + ) + { + let highlight_start = + (part_range.start - hint_start).0 + extra_shift_left; + let highlight_end = + (part_range.end - hint_start).0 + extra_shift_right; + let highlight = InlayHighlight { + inlay: hovered_hint.id, + inlay_position: hovered_hint.position, + range: highlight_start..highlight_end, + }; + if let Some(tooltip) = hovered_hint_part.tooltip { + hover_popover::hover_at_inlay( + editor, + InlayHover { + excerpt: excerpt_id, + tooltip: match tooltip { + InlayHintLabelPartTooltip::String(text) => { + HoverBlock { + text, + kind: HoverBlockKind::PlainText, + } + } + InlayHintLabelPartTooltip::MarkupContent( + content, + ) => HoverBlock { + text: content.value, + kind: content.kind, + }, + }, + range: highlight.clone(), + }, + cx, + ); + hover_updated = true; + } + if let Some((language_server_id, location)) = + hovered_hint_part.location + { + go_to_definition_updated = true; + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::InlayHint( + highlight, + location, + language_server_id, + )), + cmd_held, + shift_held, + cx, + ); + } + } + } + }; + } + ResolveState::Resolving => {} + } + } + } + } + + if !go_to_definition_updated { + update_go_to_definition_link(editor, None, cmd_held, shift_held, cx); + } + if !hover_updated { + hover_popover::hover_at(editor, None, cx); + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum LinkDefinitionKind { + Symbol, + Type, +} + +pub fn show_link_definition( + definition_kind: LinkDefinitionKind, + editor: &mut Editor, + trigger_point: TriggerPoint, + snapshot: EditorSnapshot, + cx: &mut ViewContext, +) { + let same_kind = editor.link_go_to_definition_state.kind == Some(definition_kind); + if !same_kind { + hide_link_definition(editor, cx); + } + + if editor.pending_rename.is_some() { + return; + } + + let trigger_anchor = trigger_point.anchor(); + let (buffer, buffer_position) = if let Some(output) = editor + .buffer + .read(cx) + .text_anchor_for_position(trigger_anchor.clone(), cx) + { + output + } else { + return; + }; + + let excerpt_id = if let Some((excerpt_id, _, _)) = editor + .buffer() + .read(cx) + .excerpt_containing(trigger_anchor.clone(), cx) + { + excerpt_id + } else { + return; + }; + + let project = if let Some(project) = editor.project.clone() { + project + } else { + return; + }; + + // Don't request again if the location is within the symbol region of a previous request with the same kind + if let Some(symbol_range) = &editor.link_go_to_definition_state.symbol_range { + if same_kind && symbol_range.point_within_range(&trigger_point, &snapshot) { + return; + } + } + + let task = cx.spawn(|this, mut cx| { + async move { + let result = match &trigger_point { + TriggerPoint::Text(_) => { + // query the LSP for definition info + cx.update(|cx| { + project.update(cx, |project, cx| match definition_kind { + LinkDefinitionKind::Symbol => { + project.definition(&buffer, buffer_position, cx) + } + + LinkDefinitionKind::Type => { + project.type_definition(&buffer, buffer_position, cx) + } + }) + }) + .await + .ok() + .map(|definition_result| { + ( + definition_result.iter().find_map(|link| { + link.origin.as_ref().map(|origin| { + let start = snapshot + .buffer_snapshot + .anchor_in_excerpt(excerpt_id.clone(), origin.range.start); + let end = snapshot + .buffer_snapshot + .anchor_in_excerpt(excerpt_id.clone(), origin.range.end); + RangeInEditor::Text(start..end) + }) + }), + definition_result + .into_iter() + .map(GoToDefinitionLink::Text) + .collect(), + ) + }) + } + TriggerPoint::InlayHint(highlight, lsp_location, server_id) => Some(( + Some(RangeInEditor::Inlay(highlight.clone())), + vec![GoToDefinitionLink::InlayHint( + lsp_location.clone(), + *server_id, + )], + )), + }; + + this.update(&mut cx, |this, cx| { + // Clear any existing highlights + this.clear_highlights::(cx); + this.link_go_to_definition_state.kind = Some(definition_kind); + this.link_go_to_definition_state.symbol_range = result + .as_ref() + .and_then(|(symbol_range, _)| symbol_range.clone()); + + if let Some((symbol_range, definitions)) = result { + this.link_go_to_definition_state.definitions = definitions.clone(); + + let buffer_snapshot = buffer.read(cx).snapshot(); + + // Only show highlight if there exists a definition to jump to that doesn't contain + // the current location. + let any_definition_does_not_contain_current_location = + definitions.iter().any(|definition| { + match &definition { + GoToDefinitionLink::Text(link) => { + if link.target.buffer == buffer { + let range = &link.target.range; + // Expand range by one character as lsp definition ranges include positions adjacent + // but not contained by the symbol range + let start = buffer_snapshot.clip_offset( + range + .start + .to_offset(&buffer_snapshot) + .saturating_sub(1), + Bias::Left, + ); + let end = buffer_snapshot.clip_offset( + range.end.to_offset(&buffer_snapshot) + 1, + Bias::Right, + ); + let offset = buffer_position.to_offset(&buffer_snapshot); + !(start <= offset && end >= offset) + } else { + true + } + } + GoToDefinitionLink::InlayHint(_, _) => true, + } + }); + + if any_definition_does_not_contain_current_location { + // Highlight symbol using theme link definition highlight style + let style = theme::current(cx).editor.link_definition; + let highlight_range = + symbol_range.unwrap_or_else(|| match &trigger_point { + TriggerPoint::Text(trigger_anchor) => { + let snapshot = &snapshot.buffer_snapshot; + // If no symbol range returned from language server, use the surrounding word. + let (offset_range, _) = + snapshot.surrounding_word(*trigger_anchor); + RangeInEditor::Text( + snapshot.anchor_before(offset_range.start) + ..snapshot.anchor_after(offset_range.end), + ) + } + TriggerPoint::InlayHint(highlight, _, _) => { + RangeInEditor::Inlay(highlight.clone()) + } + }); + + match highlight_range { + RangeInEditor::Text(text_range) => this + .highlight_text::( + vec![text_range], + style, + cx, + ), + RangeInEditor::Inlay(highlight) => this + .highlight_inlays::( + vec![highlight], + style, + cx, + ), + } + } else { + hide_link_definition(this, cx); + } + } + })?; + + Ok::<_, anyhow::Error>(()) + } + .log_err() + }); + + editor.link_go_to_definition_state.task = Some(task); +} + +pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext) { + if editor.link_go_to_definition_state.symbol_range.is_some() + || !editor.link_go_to_definition_state.definitions.is_empty() + { + editor.link_go_to_definition_state.symbol_range.take(); + editor.link_go_to_definition_state.definitions.clear(); + cx.notify(); + } + + editor.link_go_to_definition_state.task = None; + + editor.clear_highlights::(cx); +} + +pub fn go_to_fetched_definition( + editor: &mut Editor, + point: PointForPosition, + split: bool, + cx: &mut ViewContext, +) { + go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, editor, point, split, cx); +} + +pub fn go_to_fetched_type_definition( + editor: &mut Editor, + point: PointForPosition, + split: bool, + cx: &mut ViewContext, +) { + go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, editor, point, split, cx); +} + +fn go_to_fetched_definition_of_kind( + kind: LinkDefinitionKind, + editor: &mut Editor, + point: PointForPosition, + split: bool, + cx: &mut ViewContext, +) { + let cached_definitions = editor.link_go_to_definition_state.definitions.clone(); + hide_link_definition(editor, cx); + let cached_definitions_kind = editor.link_go_to_definition_state.kind; + + let is_correct_kind = cached_definitions_kind == Some(kind); + if !cached_definitions.is_empty() && is_correct_kind { + if !editor.focused { + cx.focus_self(); + } + + editor.navigate_to_definitions(cached_definitions, split, cx); + } else { + editor.select( + SelectPhase::Begin { + position: point.next_valid, + add: false, + click_count: 1, + }, + cx, + ); + + if point.as_valid().is_some() { + match kind { + LinkDefinitionKind::Symbol => editor.go_to_definition(&Default::default(), cx), + LinkDefinitionKind::Type => editor.go_to_type_definition(&Default::default(), cx), + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + display_map::ToDisplayPoint, + editor_tests::init_test, + inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, + test::editor_lsp_test_context::EditorLspTestContext, + }; + use futures::StreamExt; + use gpui::{ + platform::{self, Modifiers, ModifiersChangedEvent}, + View, + }; + use indoc::indoc; + use language::language_settings::InlayHintSettings; + use lsp::request::{GotoDefinition, GotoTypeDefinition}; + use util::assert_set_eq; + + #[gpui::test] + async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + struct A; + let vˇariable = A; + "}); + + // Basic hold cmd+shift, expect highlight in region if response contains type definition + let hover_point = cx.display_point(indoc! {" + struct A; + let vˇariable = A; + "}); + let symbol_range = cx.lsp_range(indoc! {" + struct A; + let «variable» = A; + "}); + let target_range = cx.lsp_range(indoc! {" + struct «A»; + let variable = A; + "}); + + let mut requests = + cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![ + lsp::LocationLink { + origin_selection_range: Some(symbol_range), + target_uri: url.clone(), + target_range, + target_selection_range: target_range, + }, + ]))) + }); + + // Press cmd+shift to trigger highlight + cx.update_editor(|editor, cx| { + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(hover_point)), + true, + true, + cx, + ); + }); + requests.next().await; + cx.foreground().run_until_parked(); + cx.assert_editor_text_highlights::(indoc! {" + struct A; + let «variable» = A; + "}); + + // Unpress shift causes highlight to go away (normal goto-definition is not valid here) + cx.update_editor(|editor, cx| { + editor.modifiers_changed( + &platform::ModifiersChangedEvent { + modifiers: Modifiers { + cmd: true, + ..Default::default() + }, + ..Default::default() + }, + cx, + ); + }); + // Assert no link highlights + cx.assert_editor_text_highlights::(indoc! {" + struct A; + let variable = A; + "}); + + // Cmd+shift click without existing definition requests and jumps + let hover_point = cx.display_point(indoc! {" + struct A; + let vˇariable = A; + "}); + let target_range = cx.lsp_range(indoc! {" + struct «A»; + let variable = A; + "}); + + let mut requests = + cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![ + lsp::LocationLink { + origin_selection_range: None, + target_uri: url, + target_range, + target_selection_range: target_range, + }, + ]))) + }); + + cx.update_editor(|editor, cx| { + go_to_fetched_type_definition(editor, PointForPosition::valid(hover_point), false, cx); + }); + requests.next().await; + cx.foreground().run_until_parked(); + + cx.assert_editor_state(indoc! {" + struct «Aˇ»; + let variable = A; + "}); + } + + #[gpui::test] + async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + fn ˇtest() { do_work(); } + fn do_work() { test(); } + "}); + + // Basic hold cmd, expect highlight in region if response contains definition + let hover_point = cx.display_point(indoc! {" + fn test() { do_wˇork(); } + fn do_work() { test(); } + "}); + let symbol_range = cx.lsp_range(indoc! {" + fn test() { «do_work»(); } + fn do_work() { test(); } + "}); + let target_range = cx.lsp_range(indoc! {" + fn test() { do_work(); } + fn «do_work»() { test(); } + "}); + + let mut requests = cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ + lsp::LocationLink { + origin_selection_range: Some(symbol_range), + target_uri: url.clone(), + target_range, + target_selection_range: target_range, + }, + ]))) + }); + + cx.update_editor(|editor, cx| { + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(hover_point)), + true, + false, + cx, + ); + }); + requests.next().await; + cx.foreground().run_until_parked(); + cx.assert_editor_text_highlights::(indoc! {" + fn test() { «do_work»(); } + fn do_work() { test(); } + "}); + + // Unpress cmd causes highlight to go away + cx.update_editor(|editor, cx| { + editor.modifiers_changed(&Default::default(), cx); + }); + + // Assert no link highlights + cx.assert_editor_text_highlights::(indoc! {" + fn test() { do_work(); } + fn do_work() { test(); } + "}); + + // Response without source range still highlights word + cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_trigger_point = None); + let mut requests = cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ + lsp::LocationLink { + // No origin range + origin_selection_range: None, + target_uri: url.clone(), + target_range, + target_selection_range: target_range, + }, + ]))) + }); + cx.update_editor(|editor, cx| { + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(hover_point)), + true, + false, + cx, + ); + }); + requests.next().await; + cx.foreground().run_until_parked(); + + cx.assert_editor_text_highlights::(indoc! {" + fn test() { «do_work»(); } + fn do_work() { test(); } + "}); + + // Moving mouse to location with no response dismisses highlight + let hover_point = cx.display_point(indoc! {" + fˇn test() { do_work(); } + fn do_work() { test(); } + "}); + let mut requests = cx + .lsp + .handle_request::(move |_, _| async move { + // No definitions returned + Ok(Some(lsp::GotoDefinitionResponse::Link(vec![]))) + }); + cx.update_editor(|editor, cx| { + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(hover_point)), + true, + false, + cx, + ); + }); + requests.next().await; + cx.foreground().run_until_parked(); + + // Assert no link highlights + cx.assert_editor_text_highlights::(indoc! {" + fn test() { do_work(); } + fn do_work() { test(); } + "}); + + // Move mouse without cmd and then pressing cmd triggers highlight + let hover_point = cx.display_point(indoc! {" + fn test() { do_work(); } + fn do_work() { teˇst(); } + "}); + cx.update_editor(|editor, cx| { + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(hover_point)), + false, + false, + cx, + ); + }); + cx.foreground().run_until_parked(); + + // Assert no link highlights + cx.assert_editor_text_highlights::(indoc! {" + fn test() { do_work(); } + fn do_work() { test(); } + "}); + + let symbol_range = cx.lsp_range(indoc! {" + fn test() { do_work(); } + fn do_work() { «test»(); } + "}); + let target_range = cx.lsp_range(indoc! {" + fn «test»() { do_work(); } + fn do_work() { test(); } + "}); + + let mut requests = cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ + lsp::LocationLink { + origin_selection_range: Some(symbol_range), + target_uri: url, + target_range, + target_selection_range: target_range, + }, + ]))) + }); + cx.update_editor(|editor, cx| { + editor.modifiers_changed( + &ModifiersChangedEvent { + modifiers: Modifiers { + cmd: true, + ..Default::default() + }, + }, + cx, + ); + }); + requests.next().await; + cx.foreground().run_until_parked(); + + cx.assert_editor_text_highlights::(indoc! {" + fn test() { do_work(); } + fn do_work() { «test»(); } + "}); + + // Deactivating the window dismisses the highlight + cx.update_workspace(|workspace, cx| { + workspace.on_window_activation_changed(false, cx); + }); + cx.assert_editor_text_highlights::(indoc! {" + fn test() { do_work(); } + fn do_work() { test(); } + "}); + + // Moving the mouse restores the highlights. + cx.update_editor(|editor, cx| { + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(hover_point)), + true, + false, + cx, + ); + }); + cx.foreground().run_until_parked(); + cx.assert_editor_text_highlights::(indoc! {" + fn test() { do_work(); } + fn do_work() { «test»(); } + "}); + + // Moving again within the same symbol range doesn't re-request + let hover_point = cx.display_point(indoc! {" + fn test() { do_work(); } + fn do_work() { tesˇt(); } + "}); + cx.update_editor(|editor, cx| { + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(hover_point)), + true, + false, + cx, + ); + }); + cx.foreground().run_until_parked(); + cx.assert_editor_text_highlights::(indoc! {" + fn test() { do_work(); } + fn do_work() { «test»(); } + "}); + + // Cmd click with existing definition doesn't re-request and dismisses highlight + cx.update_editor(|editor, cx| { + go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx); + }); + // Assert selection moved to to definition + cx.lsp + .handle_request::(move |_, _| async move { + // Empty definition response to make sure we aren't hitting the lsp and using + // the cached location instead + Ok(Some(lsp::GotoDefinitionResponse::Link(vec![]))) + }); + cx.foreground().run_until_parked(); + cx.assert_editor_state(indoc! {" + fn «testˇ»() { do_work(); } + fn do_work() { test(); } + "}); + + // Assert no link highlights after jump + cx.assert_editor_text_highlights::(indoc! {" + fn test() { do_work(); } + fn do_work() { test(); } + "}); + + // Cmd click without existing definition requests and jumps + let hover_point = cx.display_point(indoc! {" + fn test() { do_wˇork(); } + fn do_work() { test(); } + "}); + let target_range = cx.lsp_range(indoc! {" + fn test() { do_work(); } + fn «do_work»() { test(); } + "}); + + let mut requests = cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ + lsp::LocationLink { + origin_selection_range: None, + target_uri: url, + target_range, + target_selection_range: target_range, + }, + ]))) + }); + cx.update_editor(|editor, cx| { + go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx); + }); + requests.next().await; + cx.foreground().run_until_parked(); + cx.assert_editor_state(indoc! {" + fn test() { do_work(); } + fn «do_workˇ»() { test(); } + "}); + + // 1. We have a pending selection, mouse point is over a symbol that we have a response for, hitting cmd and nothing happens + // 2. Selection is completed, hovering + let hover_point = cx.display_point(indoc! {" + fn test() { do_wˇork(); } + fn do_work() { test(); } + "}); + let target_range = cx.lsp_range(indoc! {" + fn test() { do_work(); } + fn «do_work»() { test(); } + "}); + let mut requests = cx.handle_request::(move |url, _, _| async move { + Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ + lsp::LocationLink { + origin_selection_range: None, + target_uri: url, + target_range, + target_selection_range: target_range, + }, + ]))) + }); + + // create a pending selection + let selection_range = cx.ranges(indoc! {" + fn «test() { do_w»ork(); } + fn do_work() { test(); } + "})[0] + .clone(); + cx.update_editor(|editor, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + let anchor_range = snapshot.anchor_before(selection_range.start) + ..snapshot.anchor_after(selection_range.end); + editor.change_selections(Some(crate::Autoscroll::fit()), cx, |s| { + s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character) + }); + }); + cx.update_editor(|editor, cx| { + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(hover_point)), + true, + false, + cx, + ); + }); + cx.foreground().run_until_parked(); + assert!(requests.try_next().is_err()); + cx.assert_editor_text_highlights::(indoc! {" + fn test() { do_work(); } + fn do_work() { test(); } + "}); + cx.foreground().run_until_parked(); + } + + #[gpui::test] + async fn test_link_go_to_inlay(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + cx, + ) + .await; + cx.set_state(indoc! {" + struct TestStruct; + + fn main() { + let variableˇ = TestStruct; + } + "}); + let hint_start_offset = cx.ranges(indoc! {" + struct TestStruct; + + fn main() { + let variableˇ = TestStruct; + } + "})[0] + .start; + let hint_position = cx.to_lsp(hint_start_offset); + let target_range = cx.lsp_range(indoc! {" + struct «TestStruct»; + + fn main() { + let variable = TestStruct; + } + "}); + + let expected_uri = cx.buffer_lsp_url.clone(); + let hint_label = ": TestStruct"; + cx.lsp + .handle_request::(move |params, _| { + let expected_uri = expected_uri.clone(); + async move { + assert_eq!(params.text_document.uri, expected_uri); + Ok(Some(vec![lsp::InlayHint { + position: hint_position, + label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { + value: hint_label.to_string(), + location: Some(lsp::Location { + uri: params.text_document.uri, + range: target_range, + }), + ..Default::default() + }]), + kind: Some(lsp::InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: Some(false), + padding_right: Some(false), + data: None, + }])) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let expected_layers = vec![hint_label.to_string()]; + assert_eq!(expected_layers, cached_hint_labels(editor)); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + }); + + let inlay_range = cx + .ranges(indoc! {" + struct TestStruct; + + fn main() { + let variable« »= TestStruct; + } + "}) + .get(0) + .cloned() + .unwrap(); + let hint_hover_position = cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let previous_valid = inlay_range.start.to_display_point(&snapshot); + let next_valid = inlay_range.end.to_display_point(&snapshot); + assert_eq!(previous_valid.row(), next_valid.row()); + assert!(previous_valid.column() < next_valid.column()); + let exact_unclipped = DisplayPoint::new( + previous_valid.row(), + previous_valid.column() + (hint_label.len() / 2) as u32, + ); + PointForPosition { + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end: 0, + } + }); + // Press cmd to trigger highlight + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + hint_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let actual_highlights = snapshot + .inlay_highlights::() + .into_iter() + .flat_map(|highlights| highlights.values().map(|(_, highlight)| highlight)) + .collect::>(); + + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + let expected_highlight = InlayHighlight { + inlay: InlayId::Hint(0), + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), + range: 0..hint_label.len(), + }; + assert_set_eq!(actual_highlights, vec![&expected_highlight]); + }); + + // Unpress cmd causes highlight to go away + cx.update_editor(|editor, cx| { + editor.modifiers_changed( + &platform::ModifiersChangedEvent { + modifiers: Modifiers { + cmd: false, + ..Default::default() + }, + ..Default::default() + }, + cx, + ); + }); + // Assert no link highlights + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let actual_ranges = snapshot + .text_highlight_ranges::() + .map(|ranges| ranges.as_ref().clone().1) + .unwrap_or_default(); + + assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}"); + }); + + // Cmd+click without existing definition requests and jumps + cx.update_editor(|editor, cx| { + editor.modifiers_changed( + &platform::ModifiersChangedEvent { + modifiers: Modifiers { + cmd: true, + ..Default::default() + }, + ..Default::default() + }, + cx, + ); + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + hint_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + go_to_fetched_type_definition(editor, hint_hover_position, false, cx); + }); + cx.foreground().run_until_parked(); + cx.assert_editor_state(indoc! {" + struct «TestStructˇ»; + + fn main() { + let variable = TestStruct; + } + "}); + } +} diff --git a/crates/editor2/src/mouse_context_menu.rs b/crates/editor2/src/mouse_context_menu.rs new file mode 100644 index 0000000000..8dfdcdff53 --- /dev/null +++ b/crates/editor2/src/mouse_context_menu.rs @@ -0,0 +1,96 @@ +use crate::{ + DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition, + Rename, RevealInFinder, SelectMode, ToggleCodeActions, +}; +use context_menu::ContextMenuItem; +use gpui::{elements::AnchorCorner, geometry::vector::Vector2F, ViewContext}; + +pub fn deploy_context_menu( + editor: &mut Editor, + position: Vector2F, + point: DisplayPoint, + cx: &mut ViewContext, +) { + if !editor.focused { + cx.focus_self(); + } + + // Don't show context menu for inline editors + if editor.mode() != EditorMode::Full { + return; + } + + // Don't show the context menu if there isn't a project associated with this editor + if editor.project.is_none() { + return; + } + + // Move the cursor to the clicked location so that dispatched actions make sense + editor.change_selections(None, cx, |s| { + s.clear_disjoint(); + s.set_pending_display_range(point..point, SelectMode::Character); + }); + + editor.mouse_context_menu.update(cx, |menu, cx| { + menu.show( + position, + AnchorCorner::TopLeft, + vec![ + ContextMenuItem::action("Rename Symbol", Rename), + ContextMenuItem::action("Go to Definition", GoToDefinition), + ContextMenuItem::action("Go to Type Definition", GoToTypeDefinition), + ContextMenuItem::action("Find All References", FindAllReferences), + ContextMenuItem::action( + "Code Actions", + ToggleCodeActions { + deployed_from_indicator: false, + }, + ), + ContextMenuItem::Separator, + ContextMenuItem::action("Reveal in Finder", RevealInFinder), + ], + cx, + ); + }); + cx.notify(); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext}; + use indoc::indoc; + + #[gpui::test] + async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + fn teˇst() { + do_work(); + } + "}); + let point = cx.display_point(indoc! {" + fn test() { + do_wˇork(); + } + "}); + cx.update_editor(|editor, cx| deploy_context_menu(editor, Default::default(), point, cx)); + + cx.assert_editor_state(indoc! {" + fn test() { + do_wˇork(); + } + "}); + cx.editor(|editor, app| assert!(editor.mouse_context_menu.read(app).visible())); + } +} diff --git a/crates/editor2/src/movement.rs b/crates/editor2/src/movement.rs new file mode 100644 index 0000000000..332eb3c1c5 --- /dev/null +++ b/crates/editor2/src/movement.rs @@ -0,0 +1,927 @@ +use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; +use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint}; +use gpui::{FontCache, TextLayoutCache}; +use language::Point; +use std::{ops::Range, sync::Arc}; + +#[derive(Debug, PartialEq)] +pub enum FindRange { + SingleLine, + MultiLine, +} + +/// TextLayoutDetails encompasses everything we need to move vertically +/// taking into account variable width characters. +pub struct TextLayoutDetails { + pub font_cache: Arc, + pub text_layout_cache: Arc, + pub editor_style: EditorStyle, +} + +pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { + if point.column() > 0 { + *point.column_mut() -= 1; + } else if point.row() > 0 { + *point.row_mut() -= 1; + *point.column_mut() = map.line_len(point.row()); + } + map.clip_point(point, Bias::Left) +} + +pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { + if point.column() > 0 { + *point.column_mut() -= 1; + } + map.clip_point(point, Bias::Left) +} + +pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { + let max_column = map.line_len(point.row()); + if point.column() < max_column { + *point.column_mut() += 1; + } else if point.row() < map.max_point().row() { + *point.row_mut() += 1; + *point.column_mut() = 0; + } + map.clip_point(point, Bias::Right) +} + +pub fn saturating_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { + *point.column_mut() += 1; + map.clip_point(point, Bias::Right) +} + +pub fn up( + map: &DisplaySnapshot, + start: DisplayPoint, + goal: SelectionGoal, + preserve_column_at_start: bool, + text_layout_details: &TextLayoutDetails, +) -> (DisplayPoint, SelectionGoal) { + up_by_rows( + map, + start, + 1, + goal, + preserve_column_at_start, + text_layout_details, + ) +} + +pub fn down( + map: &DisplaySnapshot, + start: DisplayPoint, + goal: SelectionGoal, + preserve_column_at_end: bool, + text_layout_details: &TextLayoutDetails, +) -> (DisplayPoint, SelectionGoal) { + down_by_rows( + map, + start, + 1, + goal, + preserve_column_at_end, + text_layout_details, + ) +} + +pub fn up_by_rows( + map: &DisplaySnapshot, + start: DisplayPoint, + row_count: u32, + goal: SelectionGoal, + preserve_column_at_start: bool, + text_layout_details: &TextLayoutDetails, +) -> (DisplayPoint, SelectionGoal) { + let mut goal_x = match goal { + SelectionGoal::HorizontalPosition(x) => x, + SelectionGoal::WrappedHorizontalPosition((_, x)) => x, + SelectionGoal::HorizontalRange { end, .. } => end, + _ => map.x_for_point(start, text_layout_details), + }; + + let prev_row = start.row().saturating_sub(row_count); + let mut point = map.clip_point( + DisplayPoint::new(prev_row, map.line_len(prev_row)), + Bias::Left, + ); + if point.row() < start.row() { + *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details) + } else if preserve_column_at_start { + return (start, goal); + } else { + point = DisplayPoint::new(0, 0); + goal_x = 0.0; + } + + let mut clipped_point = map.clip_point(point, Bias::Left); + if clipped_point.row() < point.row() { + clipped_point = map.clip_point(point, Bias::Right); + } + (clipped_point, SelectionGoal::HorizontalPosition(goal_x)) +} + +pub fn down_by_rows( + map: &DisplaySnapshot, + start: DisplayPoint, + row_count: u32, + goal: SelectionGoal, + preserve_column_at_end: bool, + text_layout_details: &TextLayoutDetails, +) -> (DisplayPoint, SelectionGoal) { + let mut goal_x = match goal { + SelectionGoal::HorizontalPosition(x) => x, + SelectionGoal::WrappedHorizontalPosition((_, x)) => x, + SelectionGoal::HorizontalRange { end, .. } => end, + _ => map.x_for_point(start, text_layout_details), + }; + + let new_row = start.row() + row_count; + let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right); + if point.row() > start.row() { + *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details) + } else if preserve_column_at_end { + return (start, goal); + } else { + point = map.max_point(); + goal_x = map.x_for_point(point, text_layout_details) + } + + let mut clipped_point = map.clip_point(point, Bias::Right); + if clipped_point.row() > point.row() { + clipped_point = map.clip_point(point, Bias::Left); + } + (clipped_point, SelectionGoal::HorizontalPosition(goal_x)) +} + +pub fn line_beginning( + map: &DisplaySnapshot, + display_point: DisplayPoint, + stop_at_soft_boundaries: bool, +) -> DisplayPoint { + let point = display_point.to_point(map); + let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right); + let line_start = map.prev_line_boundary(point).1; + + if stop_at_soft_boundaries && display_point != soft_line_start { + soft_line_start + } else { + line_start + } +} + +pub fn indented_line_beginning( + map: &DisplaySnapshot, + display_point: DisplayPoint, + stop_at_soft_boundaries: bool, +) -> DisplayPoint { + let point = display_point.to_point(map); + let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right); + let indent_start = Point::new( + point.row, + map.buffer_snapshot.indent_size_for_line(point.row).len, + ) + .to_display_point(map); + let line_start = map.prev_line_boundary(point).1; + + if stop_at_soft_boundaries && soft_line_start > indent_start && display_point != soft_line_start + { + soft_line_start + } else if stop_at_soft_boundaries && display_point != indent_start { + indent_start + } else { + line_start + } +} + +pub fn line_end( + map: &DisplaySnapshot, + display_point: DisplayPoint, + stop_at_soft_boundaries: bool, +) -> DisplayPoint { + let soft_line_end = map.clip_point( + DisplayPoint::new(display_point.row(), map.line_len(display_point.row())), + Bias::Left, + ); + if stop_at_soft_boundaries && display_point != soft_line_end { + soft_line_end + } else { + map.next_line_boundary(display_point.to_point(map)).1 + } +} + +pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + let raw_point = point.to_point(map); + let scope = map.buffer_snapshot.language_scope_at(raw_point); + + find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| { + (char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace()) + || left == '\n' + }) +} + +pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + let raw_point = point.to_point(map); + let scope = map.buffer_snapshot.language_scope_at(raw_point); + + find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| { + let is_word_start = + char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace(); + let is_subword_start = + left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase(); + is_word_start || is_subword_start || left == '\n' + }) +} + +pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + let raw_point = point.to_point(map); + let scope = map.buffer_snapshot.language_scope_at(raw_point); + + find_boundary(map, point, FindRange::MultiLine, |left, right| { + (char_kind(&scope, left) != char_kind(&scope, right) && !left.is_whitespace()) + || right == '\n' + }) +} + +pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + let raw_point = point.to_point(map); + let scope = map.buffer_snapshot.language_scope_at(raw_point); + + find_boundary(map, point, FindRange::MultiLine, |left, right| { + let is_word_end = + (char_kind(&scope, left) != char_kind(&scope, right)) && !left.is_whitespace(); + let is_subword_end = + left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase(); + is_word_end || is_subword_end || right == '\n' + }) +} + +pub fn start_of_paragraph( + map: &DisplaySnapshot, + display_point: DisplayPoint, + mut count: usize, +) -> DisplayPoint { + let point = display_point.to_point(map); + if point.row == 0 { + return DisplayPoint::zero(); + } + + let mut found_non_blank_line = false; + for row in (0..point.row + 1).rev() { + let blank = map.buffer_snapshot.is_line_blank(row); + if found_non_blank_line && blank { + if count <= 1 { + return Point::new(row, 0).to_display_point(map); + } + count -= 1; + found_non_blank_line = false; + } + + found_non_blank_line |= !blank; + } + + DisplayPoint::zero() +} + +pub fn end_of_paragraph( + map: &DisplaySnapshot, + display_point: DisplayPoint, + mut count: usize, +) -> DisplayPoint { + let point = display_point.to_point(map); + if point.row == map.max_buffer_row() { + return map.max_point(); + } + + let mut found_non_blank_line = false; + for row in point.row..map.max_buffer_row() + 1 { + let blank = map.buffer_snapshot.is_line_blank(row); + if found_non_blank_line && blank { + if count <= 1 { + return Point::new(row, 0).to_display_point(map); + } + count -= 1; + found_non_blank_line = false; + } + + found_non_blank_line |= !blank; + } + + map.max_point() +} + +/// Scans for a boundary preceding the given start point `from` until a boundary is found, +/// indicated by the given predicate returning true. +/// The predicate is called with the character to the left and right of the candidate boundary location. +/// If FindRange::SingleLine is specified and no boundary is found before the start of the current line, the start of the current line will be returned. +pub fn find_preceding_boundary( + map: &DisplaySnapshot, + from: DisplayPoint, + find_range: FindRange, + mut is_boundary: impl FnMut(char, char) -> bool, +) -> DisplayPoint { + let mut prev_ch = None; + let mut offset = from.to_point(map).to_offset(&map.buffer_snapshot); + + for ch in map.buffer_snapshot.reversed_chars_at(offset) { + if find_range == FindRange::SingleLine && ch == '\n' { + break; + } + if let Some(prev_ch) = prev_ch { + if is_boundary(ch, prev_ch) { + break; + } + } + + offset -= ch.len_utf8(); + prev_ch = Some(ch); + } + + map.clip_point(offset.to_display_point(map), Bias::Left) +} + +/// Scans for a boundary following the given start point until a boundary is found, indicated by the +/// given predicate returning true. The predicate is called with the character to the left and right +/// of the candidate boundary location, and will be called with `\n` characters indicating the start +/// or end of a line. +pub fn find_boundary( + map: &DisplaySnapshot, + from: DisplayPoint, + find_range: FindRange, + mut is_boundary: impl FnMut(char, char) -> bool, +) -> DisplayPoint { + let mut offset = from.to_offset(&map, Bias::Right); + let mut prev_ch = None; + + for ch in map.buffer_snapshot.chars_at(offset) { + if find_range == FindRange::SingleLine && ch == '\n' { + break; + } + if let Some(prev_ch) = prev_ch { + if is_boundary(prev_ch, ch) { + break; + } + } + + offset += ch.len_utf8(); + prev_ch = Some(ch); + } + map.clip_point(offset.to_display_point(map), Bias::Right) +} + +pub fn chars_after( + map: &DisplaySnapshot, + mut offset: usize, +) -> impl Iterator)> + '_ { + map.buffer_snapshot.chars_at(offset).map(move |ch| { + let before = offset; + offset = offset + ch.len_utf8(); + (ch, before..offset) + }) +} + +pub fn chars_before( + map: &DisplaySnapshot, + mut offset: usize, +) -> impl Iterator)> + '_ { + map.buffer_snapshot + .reversed_chars_at(offset) + .map(move |ch| { + let after = offset; + offset = offset - ch.len_utf8(); + (ch, offset..after) + }) +} + +pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool { + let raw_point = point.to_point(map); + let scope = map.buffer_snapshot.language_scope_at(raw_point); + let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left); + let text = &map.buffer_snapshot; + let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(&scope, c)); + let prev_char_kind = text + .reversed_chars_at(ix) + .next() + .map(|c| char_kind(&scope, c)); + prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word)) +} + +pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range { + let position = map + .clip_point(position, Bias::Left) + .to_offset(map, Bias::Left); + let (range, _) = map.buffer_snapshot.surrounding_word(position); + let start = range + .start + .to_point(&map.buffer_snapshot) + .to_display_point(map); + let end = range + .end + .to_point(&map.buffer_snapshot) + .to_display_point(map); + start..end +} + +pub fn split_display_range_by_lines( + map: &DisplaySnapshot, + range: Range, +) -> Vec> { + let mut result = Vec::new(); + + let mut start = range.start; + // Loop over all the covered rows until the one containing the range end + for row in range.start.row()..range.end.row() { + let row_end_column = map.line_len(row); + let end = map.clip_point(DisplayPoint::new(row, row_end_column), Bias::Left); + if start != end { + result.push(start..end); + } + start = map.clip_point(DisplayPoint::new(row + 1, 0), Bias::Left); + } + + // Add the final range from the start of the last end to the original range end. + result.push(start..range.end); + + result +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + display_map::Inlay, + test::{editor_test_context::EditorTestContext, marked_display_snapshot}, + Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer, + }; + use project::Project; + use settings::SettingsStore; + use util::post_inc; + + #[gpui::test] + fn test_previous_word_start(cx: &mut gpui::AppContext) { + init_test(cx); + + fn assert(marked_text: &str, cx: &mut gpui::AppContext) { + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); + assert_eq!( + previous_word_start(&snapshot, display_points[1]), + display_points[0] + ); + } + + assert("\nˇ ˇlorem", cx); + assert("ˇ\nˇ lorem", cx); + assert(" ˇloremˇ", cx); + assert("ˇ ˇlorem", cx); + assert(" ˇlorˇem", cx); + assert("\nlorem\nˇ ˇipsum", cx); + assert("\n\nˇ\nˇ", cx); + assert(" ˇlorem ˇipsum", cx); + assert("loremˇ-ˇipsum", cx); + assert("loremˇ-#$@ˇipsum", cx); + assert("ˇlorem_ˇipsum", cx); + assert(" ˇdefγˇ", cx); + assert(" ˇbcΔˇ", cx); + assert(" abˇ——ˇcd", cx); + } + + #[gpui::test] + fn test_previous_subword_start(cx: &mut gpui::AppContext) { + init_test(cx); + + fn assert(marked_text: &str, cx: &mut gpui::AppContext) { + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); + assert_eq!( + previous_subword_start(&snapshot, display_points[1]), + display_points[0] + ); + } + + // Subword boundaries are respected + assert("lorem_ˇipˇsum", cx); + assert("lorem_ˇipsumˇ", cx); + assert("ˇlorem_ˇipsum", cx); + assert("lorem_ˇipsum_ˇdolor", cx); + assert("loremˇIpˇsum", cx); + assert("loremˇIpsumˇ", cx); + + // Word boundaries are still respected + assert("\nˇ ˇlorem", cx); + assert(" ˇloremˇ", cx); + assert(" ˇlorˇem", cx); + assert("\nlorem\nˇ ˇipsum", cx); + assert("\n\nˇ\nˇ", cx); + assert(" ˇlorem ˇipsum", cx); + assert("loremˇ-ˇipsum", cx); + assert("loremˇ-#$@ˇipsum", cx); + assert(" ˇdefγˇ", cx); + assert(" bcˇΔˇ", cx); + assert(" ˇbcδˇ", cx); + assert(" abˇ——ˇcd", cx); + } + + #[gpui::test] + fn test_find_preceding_boundary(cx: &mut gpui::AppContext) { + init_test(cx); + + fn assert( + marked_text: &str, + cx: &mut gpui::AppContext, + is_boundary: impl FnMut(char, char) -> bool, + ) { + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); + assert_eq!( + find_preceding_boundary( + &snapshot, + display_points[1], + FindRange::MultiLine, + is_boundary + ), + display_points[0] + ); + } + + assert("abcˇdef\ngh\nijˇk", cx, |left, right| { + left == 'c' && right == 'd' + }); + assert("abcdef\nˇgh\nijˇk", cx, |left, right| { + left == '\n' && right == 'g' + }); + let mut line_count = 0; + assert("abcdef\nˇgh\nijˇk", cx, |left, _| { + if left == '\n' { + line_count += 1; + line_count == 2 + } else { + false + } + }); + } + + #[gpui::test] + fn test_find_preceding_boundary_with_inlays(cx: &mut gpui::AppContext) { + init_test(cx); + + let input_text = "abcdefghijklmnopqrstuvwxys"; + let family_id = cx + .font_cache() + .load_family(&["Helvetica"], &Default::default()) + .unwrap(); + let font_id = cx + .font_cache() + .select_font(family_id, &Default::default()) + .unwrap(); + let font_size = 14.0; + let buffer = MultiBuffer::build_simple(input_text, cx); + let buffer_snapshot = buffer.read(cx).snapshot(cx); + let display_map = + cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx)); + + // add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary + let mut id = 0; + let inlays = (0..buffer_snapshot.len()) + .map(|offset| { + [ + Inlay { + id: InlayId::Suggestion(post_inc(&mut id)), + position: buffer_snapshot.anchor_at(offset, Bias::Left), + text: format!("test").into(), + }, + Inlay { + id: InlayId::Suggestion(post_inc(&mut id)), + position: buffer_snapshot.anchor_at(offset, Bias::Right), + text: format!("test").into(), + }, + Inlay { + id: InlayId::Hint(post_inc(&mut id)), + position: buffer_snapshot.anchor_at(offset, Bias::Left), + text: format!("test").into(), + }, + Inlay { + id: InlayId::Hint(post_inc(&mut id)), + position: buffer_snapshot.anchor_at(offset, Bias::Right), + text: format!("test").into(), + }, + ] + }) + .flatten() + .collect(); + let snapshot = display_map.update(cx, |map, cx| { + map.splice_inlays(Vec::new(), inlays, cx); + map.snapshot(cx) + }); + + assert_eq!( + find_preceding_boundary( + &snapshot, + buffer_snapshot.len().to_display_point(&snapshot), + FindRange::MultiLine, + |left, _| left == 'e', + ), + snapshot + .buffer_snapshot + .offset_to_point(5) + .to_display_point(&snapshot), + "Should not stop at inlays when looking for boundaries" + ); + } + + #[gpui::test] + fn test_next_word_end(cx: &mut gpui::AppContext) { + init_test(cx); + + fn assert(marked_text: &str, cx: &mut gpui::AppContext) { + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); + assert_eq!( + next_word_end(&snapshot, display_points[0]), + display_points[1] + ); + } + + assert("\nˇ loremˇ", cx); + assert(" ˇloremˇ", cx); + assert(" lorˇemˇ", cx); + assert(" loremˇ ˇ\nipsum\n", cx); + assert("\nˇ\nˇ\n\n", cx); + assert("loremˇ ipsumˇ ", cx); + assert("loremˇ-ˇipsum", cx); + assert("loremˇ#$@-ˇipsum", cx); + assert("loremˇ_ipsumˇ", cx); + assert(" ˇbcΔˇ", cx); + assert(" abˇ——ˇcd", cx); + } + + #[gpui::test] + fn test_next_subword_end(cx: &mut gpui::AppContext) { + init_test(cx); + + fn assert(marked_text: &str, cx: &mut gpui::AppContext) { + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); + assert_eq!( + next_subword_end(&snapshot, display_points[0]), + display_points[1] + ); + } + + // Subword boundaries are respected + assert("loˇremˇ_ipsum", cx); + assert("ˇloremˇ_ipsum", cx); + assert("loremˇ_ipsumˇ", cx); + assert("loremˇ_ipsumˇ_dolor", cx); + assert("loˇremˇIpsum", cx); + assert("loremˇIpsumˇDolor", cx); + + // Word boundaries are still respected + assert("\nˇ loremˇ", cx); + assert(" ˇloremˇ", cx); + assert(" lorˇemˇ", cx); + assert(" loremˇ ˇ\nipsum\n", cx); + assert("\nˇ\nˇ\n\n", cx); + assert("loremˇ ipsumˇ ", cx); + assert("loremˇ-ˇipsum", cx); + assert("loremˇ#$@-ˇipsum", cx); + assert("loremˇ_ipsumˇ", cx); + assert(" ˇbcˇΔ", cx); + assert(" abˇ——ˇcd", cx); + } + + #[gpui::test] + fn test_find_boundary(cx: &mut gpui::AppContext) { + init_test(cx); + + fn assert( + marked_text: &str, + cx: &mut gpui::AppContext, + is_boundary: impl FnMut(char, char) -> bool, + ) { + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); + assert_eq!( + find_boundary( + &snapshot, + display_points[0], + FindRange::MultiLine, + is_boundary + ), + display_points[1] + ); + } + + assert("abcˇdef\ngh\nijˇk", cx, |left, right| { + left == 'j' && right == 'k' + }); + assert("abˇcdef\ngh\nˇijk", cx, |left, right| { + left == '\n' && right == 'i' + }); + let mut line_count = 0; + assert("abcˇdef\ngh\nˇijk", cx, |left, _| { + if left == '\n' { + line_count += 1; + line_count == 2 + } else { + false + } + }); + } + + #[gpui::test] + fn test_surrounding_word(cx: &mut gpui::AppContext) { + init_test(cx); + + fn assert(marked_text: &str, cx: &mut gpui::AppContext) { + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); + assert_eq!( + surrounding_word(&snapshot, display_points[1]), + display_points[0]..display_points[2], + "{}", + marked_text.to_string() + ); + } + + assert("ˇˇloremˇ ipsum", cx); + assert("ˇloˇremˇ ipsum", cx); + assert("ˇloremˇˇ ipsum", cx); + assert("loremˇ ˇ ˇipsum", cx); + assert("lorem\nˇˇˇ\nipsum", cx); + assert("lorem\nˇˇipsumˇ", cx); + assert("loremˇ,ˇˇ ipsum", cx); + assert("ˇloremˇˇ, ipsum", cx); + } + + #[gpui::test] + async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) { + cx.update(|cx| { + init_test(cx); + }); + + let mut cx = EditorTestContext::new(cx).await; + let editor = cx.editor.clone(); + let window = cx.window.clone(); + cx.update_window(window, |cx| { + let text_layout_details = + editor.read_with(cx, |editor, cx| editor.text_layout_details(cx)); + + let family_id = cx + .font_cache() + .load_family(&["Helvetica"], &Default::default()) + .unwrap(); + let font_id = cx + .font_cache() + .select_font(family_id, &Default::default()) + .unwrap(); + + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn")); + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + buffer.clone(), + [ + ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 4), + primary: None, + }, + ExcerptRange { + context: Point::new(2, 0)..Point::new(3, 2), + primary: None, + }, + ], + cx, + ); + multibuffer + }); + let display_map = + cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx)); + let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); + + assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn"); + + let col_2_x = snapshot.x_for_point(DisplayPoint::new(2, 2), &text_layout_details); + + // Can't move up into the first excerpt's header + assert_eq!( + up( + &snapshot, + DisplayPoint::new(2, 2), + SelectionGoal::HorizontalPosition(col_2_x), + false, + &text_layout_details + ), + ( + DisplayPoint::new(2, 0), + SelectionGoal::HorizontalPosition(0.0) + ), + ); + assert_eq!( + up( + &snapshot, + DisplayPoint::new(2, 0), + SelectionGoal::None, + false, + &text_layout_details + ), + ( + DisplayPoint::new(2, 0), + SelectionGoal::HorizontalPosition(0.0) + ), + ); + + let col_4_x = snapshot.x_for_point(DisplayPoint::new(3, 4), &text_layout_details); + + // Move up and down within first excerpt + assert_eq!( + up( + &snapshot, + DisplayPoint::new(3, 4), + SelectionGoal::HorizontalPosition(col_4_x), + false, + &text_layout_details + ), + ( + DisplayPoint::new(2, 3), + SelectionGoal::HorizontalPosition(col_4_x) + ), + ); + assert_eq!( + down( + &snapshot, + DisplayPoint::new(2, 3), + SelectionGoal::HorizontalPosition(col_4_x), + false, + &text_layout_details + ), + ( + DisplayPoint::new(3, 4), + SelectionGoal::HorizontalPosition(col_4_x) + ), + ); + + let col_5_x = snapshot.x_for_point(DisplayPoint::new(6, 5), &text_layout_details); + + // Move up and down across second excerpt's header + assert_eq!( + up( + &snapshot, + DisplayPoint::new(6, 5), + SelectionGoal::HorizontalPosition(col_5_x), + false, + &text_layout_details + ), + ( + DisplayPoint::new(3, 4), + SelectionGoal::HorizontalPosition(col_5_x) + ), + ); + assert_eq!( + down( + &snapshot, + DisplayPoint::new(3, 4), + SelectionGoal::HorizontalPosition(col_5_x), + false, + &text_layout_details + ), + ( + DisplayPoint::new(6, 5), + SelectionGoal::HorizontalPosition(col_5_x) + ), + ); + + let max_point_x = snapshot.x_for_point(DisplayPoint::new(7, 2), &text_layout_details); + + // Can't move down off the end + assert_eq!( + down( + &snapshot, + DisplayPoint::new(7, 0), + SelectionGoal::HorizontalPosition(0.0), + false, + &text_layout_details + ), + ( + DisplayPoint::new(7, 2), + SelectionGoal::HorizontalPosition(max_point_x) + ), + ); + assert_eq!( + down( + &snapshot, + DisplayPoint::new(7, 2), + SelectionGoal::HorizontalPosition(max_point_x), + false, + &text_layout_details + ), + ( + DisplayPoint::new(7, 2), + SelectionGoal::HorizontalPosition(max_point_x) + ), + ); + }); + } + + fn init_test(cx: &mut gpui::AppContext) { + cx.set_global(SettingsStore::test(cx)); + theme::init((), cx); + language::init(cx); + crate::init(cx); + Project::init_settings(cx); + } +} diff --git a/crates/editor2/src/persistence.rs b/crates/editor2/src/persistence.rs new file mode 100644 index 0000000000..6e37735c13 --- /dev/null +++ b/crates/editor2/src/persistence.rs @@ -0,0 +1,83 @@ +use std::path::PathBuf; + +use db::sqlez_macros::sql; +use db::{define_connection, query}; + +use workspace::{ItemId, WorkspaceDb, WorkspaceId}; + +define_connection!( + // Current schema shape using pseudo-rust syntax: + // editors( + // item_id: usize, + // workspace_id: usize, + // path: PathBuf, + // scroll_top_row: usize, + // scroll_vertical_offset: f32, + // scroll_horizontal_offset: f32, + // ) + pub static ref DB: EditorDb = + &[sql! ( + CREATE TABLE editors( + item_id INTEGER NOT NULL, + workspace_id INTEGER NOT NULL, + path BLOB NOT NULL, + PRIMARY KEY(item_id, workspace_id), + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE + ) STRICT; + ), + sql! ( + ALTER TABLE editors ADD COLUMN scroll_top_row INTEGER NOT NULL DEFAULT 0; + ALTER TABLE editors ADD COLUMN scroll_horizontal_offset REAL NOT NULL DEFAULT 0; + ALTER TABLE editors ADD COLUMN scroll_vertical_offset REAL NOT NULL DEFAULT 0; + )]; +); + +impl EditorDb { + query! { + pub fn get_path(item_id: ItemId, workspace_id: WorkspaceId) -> Result> { + SELECT path FROM editors + WHERE item_id = ? AND workspace_id = ? + } + } + + query! { + pub async fn save_path(item_id: ItemId, workspace_id: WorkspaceId, path: PathBuf) -> Result<()> { + INSERT INTO editors + (item_id, workspace_id, path) + VALUES + (?1, ?2, ?3) + ON CONFLICT DO UPDATE SET + item_id = ?1, + workspace_id = ?2, + path = ?3 + } + } + + // Returns the scroll top row, and offset + query! { + pub fn get_scroll_position(item_id: ItemId, workspace_id: WorkspaceId) -> Result> { + SELECT scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset + FROM editors + WHERE item_id = ? AND workspace_id = ? + } + } + + query! { + pub async fn save_scroll_position( + item_id: ItemId, + workspace_id: WorkspaceId, + top_row: u32, + vertical_offset: f32, + horizontal_offset: f32 + ) -> Result<()> { + UPDATE OR IGNORE editors + SET + scroll_top_row = ?3, + scroll_horizontal_offset = ?4, + scroll_vertical_offset = ?5 + WHERE item_id = ?1 AND workspace_id = ?2 + } + } +} diff --git a/crates/editor2/src/scroll.rs b/crates/editor2/src/scroll.rs new file mode 100644 index 0000000000..8233f92a1a --- /dev/null +++ b/crates/editor2/src/scroll.rs @@ -0,0 +1,436 @@ +pub mod actions; +pub mod autoscroll; +pub mod scroll_amount; + +use std::{ + cmp::Ordering, + time::{Duration, Instant}, +}; + +use gpui::{ + geometry::vector::{vec2f, Vector2F}, + AppContext, Axis, Task, ViewContext, +}; +use language::{Bias, Point}; +use util::ResultExt; +use workspace::WorkspaceId; + +use crate::{ + display_map::{DisplaySnapshot, ToDisplayPoint}, + hover_popover::hide_hover, + persistence::DB, + Anchor, DisplayPoint, Editor, EditorMode, Event, InlayHintRefreshReason, MultiBufferSnapshot, + ToPoint, +}; + +use self::{ + autoscroll::{Autoscroll, AutoscrollStrategy}, + scroll_amount::ScrollAmount, +}; + +pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28); +pub const VERTICAL_SCROLL_MARGIN: f32 = 3.; +const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); + +#[derive(Default)] +pub struct ScrollbarAutoHide(pub bool); + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ScrollAnchor { + pub offset: Vector2F, + pub anchor: Anchor, +} + +impl ScrollAnchor { + fn new() -> Self { + Self { + offset: Vector2F::zero(), + anchor: Anchor::min(), + } + } + + pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F { + let mut scroll_position = self.offset; + if self.anchor != Anchor::min() { + let scroll_top = self.anchor.to_display_point(snapshot).row() as f32; + scroll_position.set_y(scroll_top + scroll_position.y()); + } else { + scroll_position.set_y(0.); + } + scroll_position + } + + pub fn top_row(&self, buffer: &MultiBufferSnapshot) -> u32 { + self.anchor.to_point(buffer).row + } +} + +#[derive(Clone, Copy, Debug)] +pub struct OngoingScroll { + last_event: Instant, + axis: Option, +} + +impl OngoingScroll { + fn new() -> Self { + Self { + last_event: Instant::now() - SCROLL_EVENT_SEPARATION, + axis: None, + } + } + + pub fn filter(&self, delta: &mut Vector2F) -> Option { + const UNLOCK_PERCENT: f32 = 1.9; + const UNLOCK_LOWER_BOUND: f32 = 6.; + let mut axis = self.axis; + + let x = delta.x().abs(); + let y = delta.y().abs(); + let duration = Instant::now().duration_since(self.last_event); + if duration > SCROLL_EVENT_SEPARATION { + //New ongoing scroll will start, determine axis + axis = if x <= y { + Some(Axis::Vertical) + } else { + Some(Axis::Horizontal) + }; + } else if x.max(y) >= UNLOCK_LOWER_BOUND { + //Check if the current ongoing will need to unlock + match axis { + Some(Axis::Vertical) => { + if x > y && x >= y * UNLOCK_PERCENT { + axis = None; + } + } + + Some(Axis::Horizontal) => { + if y > x && y >= x * UNLOCK_PERCENT { + axis = None; + } + } + + None => {} + } + } + + match axis { + Some(Axis::Vertical) => *delta = vec2f(0., delta.y()), + Some(Axis::Horizontal) => *delta = vec2f(delta.x(), 0.), + None => {} + } + + axis + } +} + +pub struct ScrollManager { + vertical_scroll_margin: f32, + anchor: ScrollAnchor, + ongoing: OngoingScroll, + autoscroll_request: Option<(Autoscroll, bool)>, + last_autoscroll: Option<(Vector2F, f32, f32, AutoscrollStrategy)>, + show_scrollbars: bool, + hide_scrollbar_task: Option>, + visible_line_count: Option, +} + +impl ScrollManager { + pub fn new() -> Self { + ScrollManager { + vertical_scroll_margin: VERTICAL_SCROLL_MARGIN, + anchor: ScrollAnchor::new(), + ongoing: OngoingScroll::new(), + autoscroll_request: None, + show_scrollbars: true, + hide_scrollbar_task: None, + last_autoscroll: None, + visible_line_count: None, + } + } + + pub fn clone_state(&mut self, other: &Self) { + self.anchor = other.anchor; + self.ongoing = other.ongoing; + } + + pub fn anchor(&self) -> ScrollAnchor { + self.anchor + } + + pub fn ongoing_scroll(&self) -> OngoingScroll { + self.ongoing + } + + pub fn update_ongoing_scroll(&mut self, axis: Option) { + self.ongoing.last_event = Instant::now(); + self.ongoing.axis = axis; + } + + pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F { + self.anchor.scroll_position(snapshot) + } + + fn set_scroll_position( + &mut self, + scroll_position: Vector2F, + map: &DisplaySnapshot, + local: bool, + autoscroll: bool, + workspace_id: Option, + cx: &mut ViewContext, + ) { + let (new_anchor, top_row) = if scroll_position.y() <= 0. { + ( + ScrollAnchor { + anchor: Anchor::min(), + offset: scroll_position.max(vec2f(0., 0.)), + }, + 0, + ) + } else { + let scroll_top_buffer_point = + DisplayPoint::new(scroll_position.y() as u32, 0).to_point(&map); + let top_anchor = map + .buffer_snapshot + .anchor_at(scroll_top_buffer_point, Bias::Right); + + ( + ScrollAnchor { + anchor: top_anchor, + offset: vec2f( + scroll_position.x(), + scroll_position.y() - top_anchor.to_display_point(&map).row() as f32, + ), + }, + scroll_top_buffer_point.row, + ) + }; + + self.set_anchor(new_anchor, top_row, local, autoscroll, workspace_id, cx); + } + + fn set_anchor( + &mut self, + anchor: ScrollAnchor, + top_row: u32, + local: bool, + autoscroll: bool, + workspace_id: Option, + cx: &mut ViewContext, + ) { + self.anchor = anchor; + cx.emit(Event::ScrollPositionChanged { local, autoscroll }); + self.show_scrollbar(cx); + self.autoscroll_request.take(); + if let Some(workspace_id) = workspace_id { + let item_id = cx.view_id(); + + cx.background() + .spawn(async move { + DB.save_scroll_position( + item_id, + workspace_id, + top_row, + anchor.offset.x(), + anchor.offset.y(), + ) + .await + .log_err() + }) + .detach() + } + cx.notify(); + } + + pub fn show_scrollbar(&mut self, cx: &mut ViewContext) { + if !self.show_scrollbars { + self.show_scrollbars = true; + cx.notify(); + } + + if cx.default_global::().0 { + self.hide_scrollbar_task = Some(cx.spawn(|editor, mut cx| async move { + cx.background().timer(SCROLLBAR_SHOW_INTERVAL).await; + editor + .update(&mut cx, |editor, cx| { + editor.scroll_manager.show_scrollbars = false; + cx.notify(); + }) + .log_err(); + })); + } else { + self.hide_scrollbar_task = None; + } + } + + pub fn scrollbars_visible(&self) -> bool { + self.show_scrollbars + } + + pub fn has_autoscroll_request(&self) -> bool { + self.autoscroll_request.is_some() + } + + pub fn clamp_scroll_left(&mut self, max: f32) -> bool { + if max < self.anchor.offset.x() { + self.anchor.offset.set_x(max); + true + } else { + false + } + } +} + +impl Editor { + pub fn vertical_scroll_margin(&mut self) -> usize { + self.scroll_manager.vertical_scroll_margin as usize + } + + pub fn set_vertical_scroll_margin(&mut self, margin_rows: usize, cx: &mut ViewContext) { + self.scroll_manager.vertical_scroll_margin = margin_rows as f32; + cx.notify(); + } + + pub fn visible_line_count(&self) -> Option { + self.scroll_manager.visible_line_count + } + + pub(crate) fn set_visible_line_count(&mut self, lines: f32, cx: &mut ViewContext) { + let opened_first_time = self.scroll_manager.visible_line_count.is_none(); + self.scroll_manager.visible_line_count = Some(lines); + if opened_first_time { + cx.spawn(|editor, mut cx| async move { + editor + .update(&mut cx, |editor, cx| { + editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx) + }) + .ok() + }) + .detach() + } + } + + pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext) { + self.set_scroll_position_internal(scroll_position, true, false, cx); + } + + pub(crate) fn set_scroll_position_internal( + &mut self, + scroll_position: Vector2F, + local: bool, + autoscroll: bool, + cx: &mut ViewContext, + ) { + let map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + + hide_hover(self, cx); + let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1); + self.scroll_manager.set_scroll_position( + scroll_position, + &map, + local, + autoscroll, + workspace_id, + cx, + ); + + self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); + } + + pub fn scroll_position(&self, cx: &mut ViewContext) -> Vector2F { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + self.scroll_manager.anchor.scroll_position(&display_map) + } + + pub fn set_scroll_anchor(&mut self, scroll_anchor: ScrollAnchor, cx: &mut ViewContext) { + hide_hover(self, cx); + let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1); + let top_row = scroll_anchor + .anchor + .to_point(&self.buffer().read(cx).snapshot(cx)) + .row; + self.scroll_manager + .set_anchor(scroll_anchor, top_row, true, false, workspace_id, cx); + } + + pub(crate) fn set_scroll_anchor_remote( + &mut self, + scroll_anchor: ScrollAnchor, + cx: &mut ViewContext, + ) { + hide_hover(self, cx); + let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1); + let top_row = scroll_anchor + .anchor + .to_point(&self.buffer().read(cx).snapshot(cx)) + .row; + self.scroll_manager + .set_anchor(scroll_anchor, top_row, false, false, workspace_id, cx); + } + + pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext) { + if matches!(self.mode, EditorMode::SingleLine) { + cx.propagate_action(); + return; + } + + if self.take_rename(true, cx).is_some() { + return; + } + + let cur_position = self.scroll_position(cx); + let new_pos = cur_position + vec2f(0., amount.lines(self)); + self.set_scroll_position(new_pos, cx); + } + + /// Returns an ordering. The newest selection is: + /// Ordering::Equal => on screen + /// Ordering::Less => above the screen + /// Ordering::Greater => below the screen + pub fn newest_selection_on_screen(&self, cx: &mut AppContext) -> Ordering { + let snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let newest_head = self + .selections + .newest_anchor() + .head() + .to_display_point(&snapshot); + let screen_top = self + .scroll_manager + .anchor + .anchor + .to_display_point(&snapshot); + + if screen_top > newest_head { + return Ordering::Less; + } + + if let Some(visible_lines) = self.visible_line_count() { + if newest_head.row() < screen_top.row() + visible_lines as u32 { + return Ordering::Equal; + } + } + + Ordering::Greater + } + + pub fn read_scroll_position_from_db( + &mut self, + item_id: usize, + workspace_id: WorkspaceId, + cx: &mut ViewContext, + ) { + let scroll_position = DB.get_scroll_position(item_id, workspace_id); + if let Ok(Some((top_row, x, y))) = scroll_position { + let top_anchor = self + .buffer() + .read(cx) + .snapshot(cx) + .anchor_at(Point::new(top_row as u32, 0), Bias::Left); + let scroll_anchor = ScrollAnchor { + offset: Vector2F::new(x, y), + anchor: top_anchor, + }; + self.set_scroll_anchor(scroll_anchor, cx); + } + } +} diff --git a/crates/editor2/src/scroll/actions.rs b/crates/editor2/src/scroll/actions.rs new file mode 100644 index 0000000000..82c2e10589 --- /dev/null +++ b/crates/editor2/src/scroll/actions.rs @@ -0,0 +1,152 @@ +use gpui::{actions, geometry::vector::Vector2F, AppContext, Axis, ViewContext}; +use language::Bias; + +use crate::{Editor, EditorMode}; + +use super::{autoscroll::Autoscroll, scroll_amount::ScrollAmount, ScrollAnchor}; + +actions!( + editor, + [ + LineDown, + LineUp, + HalfPageDown, + HalfPageUp, + PageDown, + PageUp, + NextScreen, + ScrollCursorTop, + ScrollCursorCenter, + ScrollCursorBottom, + ] +); + +pub fn init(cx: &mut AppContext) { + cx.add_action(Editor::next_screen); + cx.add_action(Editor::scroll_cursor_top); + cx.add_action(Editor::scroll_cursor_center); + cx.add_action(Editor::scroll_cursor_bottom); + cx.add_action(|this: &mut Editor, _: &LineDown, cx| { + this.scroll_screen(&ScrollAmount::Line(1.), cx) + }); + cx.add_action(|this: &mut Editor, _: &LineUp, cx| { + this.scroll_screen(&ScrollAmount::Line(-1.), cx) + }); + cx.add_action(|this: &mut Editor, _: &HalfPageDown, cx| { + this.scroll_screen(&ScrollAmount::Page(0.5), cx) + }); + cx.add_action(|this: &mut Editor, _: &HalfPageUp, cx| { + this.scroll_screen(&ScrollAmount::Page(-0.5), cx) + }); + cx.add_action(|this: &mut Editor, _: &PageDown, cx| { + this.scroll_screen(&ScrollAmount::Page(1.), cx) + }); + cx.add_action(|this: &mut Editor, _: &PageUp, cx| { + this.scroll_screen(&ScrollAmount::Page(-1.), cx) + }); +} + +impl Editor { + pub fn next_screen(&mut self, _: &NextScreen, cx: &mut ViewContext) -> Option<()> { + if self.take_rename(true, cx).is_some() { + return None; + } + + if self.mouse_context_menu.read(cx).visible() { + return None; + } + + if matches!(self.mode, EditorMode::SingleLine) { + cx.propagate_action(); + return None; + } + self.request_autoscroll(Autoscroll::Next, cx); + Some(()) + } + + pub fn scroll( + &mut self, + scroll_position: Vector2F, + axis: Option, + cx: &mut ViewContext, + ) { + self.scroll_manager.update_ongoing_scroll(axis); + self.set_scroll_position(scroll_position, cx); + } + + fn scroll_cursor_top(editor: &mut Editor, _: &ScrollCursorTop, cx: &mut ViewContext) { + let snapshot = editor.snapshot(cx).display_snapshot; + let scroll_margin_rows = editor.vertical_scroll_margin() as u32; + + let mut new_screen_top = editor.selections.newest_display(cx).head(); + *new_screen_top.row_mut() = new_screen_top.row().saturating_sub(scroll_margin_rows); + *new_screen_top.column_mut() = 0; + let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left); + let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top); + + editor.set_scroll_anchor( + ScrollAnchor { + anchor: new_anchor, + offset: Default::default(), + }, + cx, + ) + } + + fn scroll_cursor_center( + editor: &mut Editor, + _: &ScrollCursorCenter, + cx: &mut ViewContext, + ) { + let snapshot = editor.snapshot(cx).display_snapshot; + let visible_rows = if let Some(visible_rows) = editor.visible_line_count() { + visible_rows as u32 + } else { + return; + }; + + let mut new_screen_top = editor.selections.newest_display(cx).head(); + *new_screen_top.row_mut() = new_screen_top.row().saturating_sub(visible_rows / 2); + *new_screen_top.column_mut() = 0; + let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left); + let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top); + + editor.set_scroll_anchor( + ScrollAnchor { + anchor: new_anchor, + offset: Default::default(), + }, + cx, + ) + } + + fn scroll_cursor_bottom( + editor: &mut Editor, + _: &ScrollCursorBottom, + cx: &mut ViewContext, + ) { + let snapshot = editor.snapshot(cx).display_snapshot; + let scroll_margin_rows = editor.vertical_scroll_margin() as u32; + let visible_rows = if let Some(visible_rows) = editor.visible_line_count() { + visible_rows as u32 + } else { + return; + }; + + let mut new_screen_top = editor.selections.newest_display(cx).head(); + *new_screen_top.row_mut() = new_screen_top + .row() + .saturating_sub(visible_rows.saturating_sub(scroll_margin_rows)); + *new_screen_top.column_mut() = 0; + let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left); + let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top); + + editor.set_scroll_anchor( + ScrollAnchor { + anchor: new_anchor, + offset: Default::default(), + }, + cx, + ) + } +} diff --git a/crates/editor2/src/scroll/autoscroll.rs b/crates/editor2/src/scroll/autoscroll.rs new file mode 100644 index 0000000000..ffada50179 --- /dev/null +++ b/crates/editor2/src/scroll/autoscroll.rs @@ -0,0 +1,258 @@ +use std::cmp; + +use gpui::ViewContext; +use language::Point; + +use crate::{display_map::ToDisplayPoint, Editor, EditorMode, LineWithInvisibles}; + +#[derive(PartialEq, Eq)] +pub enum Autoscroll { + Next, + Strategy(AutoscrollStrategy), +} + +impl Autoscroll { + pub fn fit() -> Self { + Self::Strategy(AutoscrollStrategy::Fit) + } + + pub fn newest() -> Self { + Self::Strategy(AutoscrollStrategy::Newest) + } + + pub fn center() -> Self { + Self::Strategy(AutoscrollStrategy::Center) + } +} + +#[derive(PartialEq, Eq, Default)] +pub enum AutoscrollStrategy { + Fit, + Newest, + #[default] + Center, + Top, + Bottom, +} + +impl AutoscrollStrategy { + fn next(&self) -> Self { + match self { + AutoscrollStrategy::Center => AutoscrollStrategy::Top, + AutoscrollStrategy::Top => AutoscrollStrategy::Bottom, + _ => AutoscrollStrategy::Center, + } + } +} + +impl Editor { + pub fn autoscroll_vertically( + &mut self, + viewport_height: f32, + line_height: f32, + cx: &mut ViewContext, + ) -> bool { + let visible_lines = viewport_height / line_height; + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut scroll_position = self.scroll_manager.scroll_position(&display_map); + let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) { + (display_map.max_point().row() as f32 - visible_lines + 1.).max(0.) + } else { + display_map.max_point().row() as f32 + }; + if scroll_position.y() > max_scroll_top { + scroll_position.set_y(max_scroll_top); + self.set_scroll_position(scroll_position, cx); + } + + let Some((autoscroll, local)) = self.scroll_manager.autoscroll_request.take() else { + return false; + }; + + let mut target_top; + let mut target_bottom; + if let Some(highlighted_rows) = &self.highlighted_rows { + target_top = highlighted_rows.start as f32; + target_bottom = target_top + 1.; + } else { + let selections = self.selections.all::(cx); + target_top = selections + .first() + .unwrap() + .head() + .to_display_point(&display_map) + .row() as f32; + target_bottom = selections + .last() + .unwrap() + .head() + .to_display_point(&display_map) + .row() as f32 + + 1.0; + + // If the selections can't all fit on screen, scroll to the newest. + if autoscroll == Autoscroll::newest() + || autoscroll == Autoscroll::fit() && target_bottom - target_top > visible_lines + { + let newest_selection_top = selections + .iter() + .max_by_key(|s| s.id) + .unwrap() + .head() + .to_display_point(&display_map) + .row() as f32; + target_top = newest_selection_top; + target_bottom = newest_selection_top + 1.; + } + } + + let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) { + 0. + } else { + ((visible_lines - (target_bottom - target_top)) / 2.0).floor() + }; + + let strategy = match autoscroll { + Autoscroll::Strategy(strategy) => strategy, + Autoscroll::Next => { + let last_autoscroll = &self.scroll_manager.last_autoscroll; + if let Some(last_autoscroll) = last_autoscroll { + if self.scroll_manager.anchor.offset == last_autoscroll.0 + && target_top == last_autoscroll.1 + && target_bottom == last_autoscroll.2 + { + last_autoscroll.3.next() + } else { + AutoscrollStrategy::default() + } + } else { + AutoscrollStrategy::default() + } + } + }; + + match strategy { + AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => { + let margin = margin.min(self.scroll_manager.vertical_scroll_margin); + let target_top = (target_top - margin).max(0.0); + let target_bottom = target_bottom + margin; + let start_row = scroll_position.y(); + let end_row = start_row + visible_lines; + + let needs_scroll_up = target_top < start_row; + let needs_scroll_down = target_bottom >= end_row; + + if needs_scroll_up && !needs_scroll_down { + scroll_position.set_y(target_top); + self.set_scroll_position_internal(scroll_position, local, true, cx); + } + if !needs_scroll_up && needs_scroll_down { + scroll_position.set_y(target_bottom - visible_lines); + self.set_scroll_position_internal(scroll_position, local, true, cx); + } + } + AutoscrollStrategy::Center => { + scroll_position.set_y((target_top - margin).max(0.0)); + self.set_scroll_position_internal(scroll_position, local, true, cx); + } + AutoscrollStrategy::Top => { + scroll_position.set_y((target_top).max(0.0)); + self.set_scroll_position_internal(scroll_position, local, true, cx); + } + AutoscrollStrategy::Bottom => { + scroll_position.set_y((target_bottom - visible_lines).max(0.0)); + self.set_scroll_position_internal(scroll_position, local, true, cx); + } + } + + self.scroll_manager.last_autoscroll = Some(( + self.scroll_manager.anchor.offset, + target_top, + target_bottom, + strategy, + )); + + true + } + + pub fn autoscroll_horizontally( + &mut self, + start_row: u32, + viewport_width: f32, + scroll_width: f32, + max_glyph_width: f32, + layouts: &[LineWithInvisibles], + cx: &mut ViewContext, + ) -> bool { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let selections = self.selections.all::(cx); + + let mut target_left; + let mut target_right; + + if self.highlighted_rows.is_some() { + target_left = 0.0_f32; + target_right = 0.0_f32; + } else { + target_left = std::f32::INFINITY; + target_right = 0.0_f32; + for selection in selections { + let head = selection.head().to_display_point(&display_map); + if head.row() >= start_row && head.row() < start_row + layouts.len() as u32 { + let start_column = head.column().saturating_sub(3); + let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3); + target_left = target_left.min( + layouts[(head.row() - start_row) as usize] + .line + .x_for_index(start_column as usize), + ); + target_right = target_right.max( + layouts[(head.row() - start_row) as usize] + .line + .x_for_index(end_column as usize) + + max_glyph_width, + ); + } + } + } + + target_right = target_right.min(scroll_width); + + if target_right - target_left > viewport_width { + return false; + } + + let scroll_left = self.scroll_manager.anchor.offset.x() * max_glyph_width; + let scroll_right = scroll_left + viewport_width; + + if target_left < scroll_left { + self.scroll_manager + .anchor + .offset + .set_x(target_left / max_glyph_width); + true + } else if target_right > scroll_right { + self.scroll_manager + .anchor + .offset + .set_x((target_right - viewport_width) / max_glyph_width); + true + } else { + false + } + } + + pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext) { + self.scroll_manager.autoscroll_request = Some((autoscroll, true)); + cx.notify(); + } + + pub(crate) fn request_autoscroll_remotely( + &mut self, + autoscroll: Autoscroll, + cx: &mut ViewContext, + ) { + self.scroll_manager.autoscroll_request = Some((autoscroll, false)); + cx.notify(); + } +} diff --git a/crates/editor2/src/scroll/scroll_amount.rs b/crates/editor2/src/scroll/scroll_amount.rs new file mode 100644 index 0000000000..2cb22d1516 --- /dev/null +++ b/crates/editor2/src/scroll/scroll_amount.rs @@ -0,0 +1,28 @@ +use crate::Editor; +use serde::Deserialize; + +#[derive(Clone, PartialEq, Deserialize)] +pub enum ScrollAmount { + // Scroll N lines (positive is towards the end of the document) + Line(f32), + // Scroll N pages (positive is towards the end of the document) + Page(f32), +} + +impl ScrollAmount { + pub fn lines(&self, editor: &mut Editor) -> f32 { + match self { + Self::Line(count) => *count, + Self::Page(count) => editor + .visible_line_count() + .map(|mut l| { + // for full pages subtract one to leave an anchor line + if count.abs() == 1.0 { + l -= 1.0 + } + (l * count).trunc() + }) + .unwrap_or(0.), + } + } +} diff --git a/crates/editor2/src/selections_collection.rs b/crates/editor2/src/selections_collection.rs new file mode 100644 index 0000000000..4b2dc855c3 --- /dev/null +++ b/crates/editor2/src/selections_collection.rs @@ -0,0 +1,886 @@ +use std::{ + cell::Ref, + iter, mem, + ops::{Deref, DerefMut, Range, Sub}, + sync::Arc, +}; + +use collections::HashMap; +use gpui::{AppContext, ModelHandle}; +use itertools::Itertools; +use language::{Bias, Point, Selection, SelectionGoal, TextDimension, ToPoint}; +use util::post_inc; + +use crate::{ + display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint}, + movement::TextLayoutDetails, + Anchor, DisplayPoint, ExcerptId, MultiBuffer, MultiBufferSnapshot, SelectMode, ToOffset, +}; + +#[derive(Debug, Clone)] +pub struct PendingSelection { + pub selection: Selection, + pub mode: SelectMode, +} + +#[derive(Debug, Clone)] +pub struct SelectionsCollection { + display_map: ModelHandle, + buffer: ModelHandle, + pub next_selection_id: usize, + pub line_mode: bool, + disjoint: Arc<[Selection]>, + pending: Option, +} + +impl SelectionsCollection { + pub fn new(display_map: ModelHandle, buffer: ModelHandle) -> Self { + Self { + display_map, + buffer, + next_selection_id: 1, + line_mode: false, + disjoint: Arc::from([]), + pending: Some(PendingSelection { + selection: Selection { + id: 0, + start: Anchor::min(), + end: Anchor::min(), + reversed: false, + goal: SelectionGoal::None, + }, + mode: SelectMode::Character, + }), + } + } + + pub fn display_map(&self, cx: &mut AppContext) -> DisplaySnapshot { + self.display_map.update(cx, |map, cx| map.snapshot(cx)) + } + + fn buffer<'a>(&self, cx: &'a AppContext) -> Ref<'a, MultiBufferSnapshot> { + self.buffer.read(cx).read(cx) + } + + pub fn clone_state(&mut self, other: &SelectionsCollection) { + self.next_selection_id = other.next_selection_id; + self.line_mode = other.line_mode; + self.disjoint = other.disjoint.clone(); + self.pending = other.pending.clone(); + } + + pub fn count(&self) -> usize { + let mut count = self.disjoint.len(); + if self.pending.is_some() { + count += 1; + } + count + } + + /// The non-pending, non-overlapping selections. There could still be a pending + /// selection that overlaps these if the mouse is being dragged, etc. Returned as + /// selections over Anchors. + pub fn disjoint_anchors(&self) -> Arc<[Selection]> { + self.disjoint.clone() + } + + pub fn pending_anchor(&self) -> Option> { + self.pending + .as_ref() + .map(|pending| pending.selection.clone()) + } + + pub fn pending>( + &self, + cx: &AppContext, + ) -> Option> { + self.pending_anchor() + .as_ref() + .map(|pending| pending.map(|p| p.summary::(&self.buffer(cx)))) + } + + pub fn pending_mode(&self) -> Option { + self.pending.as_ref().map(|pending| pending.mode.clone()) + } + + pub fn all<'a, D>(&self, cx: &AppContext) -> Vec> + where + D: 'a + TextDimension + Ord + Sub + std::fmt::Debug, + { + let disjoint_anchors = &self.disjoint; + let mut disjoint = + resolve_multiple::(disjoint_anchors.iter(), &self.buffer(cx)).peekable(); + + let mut pending_opt = self.pending::(cx); + + iter::from_fn(move || { + if let Some(pending) = pending_opt.as_mut() { + while let Some(next_selection) = disjoint.peek() { + if pending.start <= next_selection.end && pending.end >= next_selection.start { + let next_selection = disjoint.next().unwrap(); + if next_selection.start < pending.start { + pending.start = next_selection.start; + } + if next_selection.end > pending.end { + pending.end = next_selection.end; + } + } else if next_selection.end < pending.start { + return disjoint.next(); + } else { + break; + } + } + + pending_opt.take() + } else { + disjoint.next() + } + }) + .collect() + } + + /// Returns all of the selections, adjusted to take into account the selection line_mode + pub fn all_adjusted(&self, cx: &mut AppContext) -> Vec> { + let mut selections = self.all::(cx); + if self.line_mode { + let map = self.display_map(cx); + for selection in &mut selections { + let new_range = map.expand_to_line(selection.range()); + selection.start = new_range.start; + selection.end = new_range.end; + } + } + selections + } + + pub fn all_adjusted_display( + &self, + cx: &mut AppContext, + ) -> (DisplaySnapshot, Vec>) { + if self.line_mode { + let selections = self.all::(cx); + let map = self.display_map(cx); + let result = selections + .into_iter() + .map(|mut selection| { + let new_range = map.expand_to_line(selection.range()); + selection.start = new_range.start; + selection.end = new_range.end; + selection.map(|point| point.to_display_point(&map)) + }) + .collect(); + (map, result) + } else { + self.all_display(cx) + } + } + + pub fn disjoint_in_range<'a, D>( + &self, + range: Range, + cx: &AppContext, + ) -> Vec> + where + D: 'a + TextDimension + Ord + Sub + std::fmt::Debug, + { + let buffer = self.buffer(cx); + let start_ix = match self + .disjoint + .binary_search_by(|probe| probe.end.cmp(&range.start, &buffer)) + { + Ok(ix) | Err(ix) => ix, + }; + let end_ix = match self + .disjoint + .binary_search_by(|probe| probe.start.cmp(&range.end, &buffer)) + { + Ok(ix) => ix + 1, + Err(ix) => ix, + }; + resolve_multiple(&self.disjoint[start_ix..end_ix], &buffer).collect() + } + + pub fn all_display( + &self, + cx: &mut AppContext, + ) -> (DisplaySnapshot, Vec>) { + let display_map = self.display_map(cx); + let selections = self + .all::(cx) + .into_iter() + .map(|selection| selection.map(|point| point.to_display_point(&display_map))) + .collect(); + (display_map, selections) + } + + pub fn newest_anchor(&self) -> &Selection { + self.pending + .as_ref() + .map(|s| &s.selection) + .or_else(|| self.disjoint.iter().max_by_key(|s| s.id)) + .unwrap() + } + + pub fn newest>( + &self, + cx: &AppContext, + ) -> Selection { + resolve(self.newest_anchor(), &self.buffer(cx)) + } + + pub fn newest_display(&self, cx: &mut AppContext) -> Selection { + let display_map = self.display_map(cx); + let selection = self + .newest_anchor() + .map(|point| point.to_display_point(&display_map)); + selection + } + + pub fn oldest_anchor(&self) -> &Selection { + self.disjoint + .iter() + .min_by_key(|s| s.id) + .or_else(|| self.pending.as_ref().map(|p| &p.selection)) + .unwrap() + } + + pub fn oldest>( + &self, + cx: &AppContext, + ) -> Selection { + resolve(self.oldest_anchor(), &self.buffer(cx)) + } + + pub fn first_anchor(&self) -> Selection { + self.disjoint[0].clone() + } + + pub fn first>( + &self, + cx: &AppContext, + ) -> Selection { + self.all(cx).first().unwrap().clone() + } + + pub fn last>( + &self, + cx: &AppContext, + ) -> Selection { + self.all(cx).last().unwrap().clone() + } + + #[cfg(any(test, feature = "test-support"))] + pub fn ranges + std::fmt::Debug>( + &self, + cx: &AppContext, + ) -> Vec> { + self.all::(cx) + .iter() + .map(|s| { + if s.reversed { + s.end.clone()..s.start.clone() + } else { + s.start.clone()..s.end.clone() + } + }) + .collect() + } + + #[cfg(any(test, feature = "test-support"))] + pub fn display_ranges(&self, cx: &mut AppContext) -> Vec> { + let display_map = self.display_map(cx); + self.disjoint_anchors() + .iter() + .chain(self.pending_anchor().as_ref()) + .map(|s| { + if s.reversed { + s.end.to_display_point(&display_map)..s.start.to_display_point(&display_map) + } else { + s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map) + } + }) + .collect() + } + + pub fn build_columnar_selection( + &mut self, + display_map: &DisplaySnapshot, + row: u32, + positions: &Range, + reversed: bool, + text_layout_details: &TextLayoutDetails, + ) -> Option> { + let is_empty = positions.start == positions.end; + let line_len = display_map.line_len(row); + + let layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details); + + let start_col = layed_out_line.closest_index_for_x(positions.start) as u32; + if start_col < line_len || (is_empty && positions.start == layed_out_line.width()) { + let start = DisplayPoint::new(row, start_col); + let end_col = layed_out_line.closest_index_for_x(positions.end) as u32; + let end = DisplayPoint::new(row, end_col); + + Some(Selection { + id: post_inc(&mut self.next_selection_id), + start: start.to_point(display_map), + end: end.to_point(display_map), + reversed, + goal: SelectionGoal::HorizontalRange { + start: positions.start, + end: positions.end, + }, + }) + } else { + None + } + } + + pub(crate) fn change_with( + &mut self, + cx: &mut AppContext, + change: impl FnOnce(&mut MutableSelectionsCollection) -> R, + ) -> (bool, R) { + let mut mutable_collection = MutableSelectionsCollection { + collection: self, + selections_changed: false, + cx, + }; + + let result = change(&mut mutable_collection); + assert!( + !mutable_collection.disjoint.is_empty() || mutable_collection.pending.is_some(), + "There must be at least one selection" + ); + (mutable_collection.selections_changed, result) + } +} + +pub struct MutableSelectionsCollection<'a> { + collection: &'a mut SelectionsCollection, + selections_changed: bool, + cx: &'a mut AppContext, +} + +impl<'a> MutableSelectionsCollection<'a> { + pub fn display_map(&mut self) -> DisplaySnapshot { + self.collection.display_map(self.cx) + } + + fn buffer(&self) -> Ref { + self.collection.buffer(self.cx) + } + + pub fn clear_disjoint(&mut self) { + self.collection.disjoint = Arc::from([]); + } + + pub fn delete(&mut self, selection_id: usize) { + let mut changed = false; + self.collection.disjoint = self + .disjoint + .iter() + .filter(|selection| { + let found = selection.id == selection_id; + changed |= found; + !found + }) + .cloned() + .collect(); + + self.selections_changed |= changed; + } + + pub fn clear_pending(&mut self) { + if self.collection.pending.is_some() { + self.collection.pending = None; + self.selections_changed = true; + } + } + + pub fn set_pending_anchor_range(&mut self, range: Range, mode: SelectMode) { + self.collection.pending = Some(PendingSelection { + selection: Selection { + id: post_inc(&mut self.collection.next_selection_id), + start: range.start, + end: range.end, + reversed: false, + goal: SelectionGoal::None, + }, + mode, + }); + self.selections_changed = true; + } + + pub fn set_pending_display_range(&mut self, range: Range, mode: SelectMode) { + let (start, end, reversed) = { + let display_map = self.display_map(); + let buffer = self.buffer(); + let mut start = range.start; + let mut end = range.end; + let reversed = if start > end { + mem::swap(&mut start, &mut end); + true + } else { + false + }; + + let end_bias = if end > start { Bias::Left } else { Bias::Right }; + ( + buffer.anchor_before(start.to_point(&display_map)), + buffer.anchor_at(end.to_point(&display_map), end_bias), + reversed, + ) + }; + + let new_pending = PendingSelection { + selection: Selection { + id: post_inc(&mut self.collection.next_selection_id), + start, + end, + reversed, + goal: SelectionGoal::None, + }, + mode, + }; + + self.collection.pending = Some(new_pending); + self.selections_changed = true; + } + + pub fn set_pending(&mut self, selection: Selection, mode: SelectMode) { + self.collection.pending = Some(PendingSelection { selection, mode }); + self.selections_changed = true; + } + + pub fn try_cancel(&mut self) -> bool { + if let Some(pending) = self.collection.pending.take() { + if self.disjoint.is_empty() { + self.collection.disjoint = Arc::from([pending.selection]); + } + self.selections_changed = true; + return true; + } + + let mut oldest = self.oldest_anchor().clone(); + if self.count() > 1 { + self.collection.disjoint = Arc::from([oldest]); + self.selections_changed = true; + return true; + } + + if !oldest.start.cmp(&oldest.end, &self.buffer()).is_eq() { + let head = oldest.head(); + oldest.start = head.clone(); + oldest.end = head; + self.collection.disjoint = Arc::from([oldest]); + self.selections_changed = true; + return true; + } + + false + } + + pub fn insert_range(&mut self, range: Range) + where + T: 'a + ToOffset + ToPoint + TextDimension + Ord + Sub + std::marker::Copy, + { + let mut selections = self.all(self.cx); + let mut start = range.start.to_offset(&self.buffer()); + let mut end = range.end.to_offset(&self.buffer()); + let reversed = if start > end { + mem::swap(&mut start, &mut end); + true + } else { + false + }; + selections.push(Selection { + id: post_inc(&mut self.collection.next_selection_id), + start, + end, + reversed, + goal: SelectionGoal::None, + }); + self.select(selections); + } + + pub fn select(&mut self, mut selections: Vec>) + where + T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug, + { + let buffer = self.buffer.read(self.cx).snapshot(self.cx); + selections.sort_unstable_by_key(|s| s.start); + // Merge overlapping selections. + let mut i = 1; + while i < selections.len() { + if selections[i - 1].end >= selections[i].start { + let removed = selections.remove(i); + if removed.start < selections[i - 1].start { + selections[i - 1].start = removed.start; + } + if removed.end > selections[i - 1].end { + selections[i - 1].end = removed.end; + } + } else { + i += 1; + } + } + + self.collection.disjoint = Arc::from_iter(selections.into_iter().map(|selection| { + let end_bias = if selection.end > selection.start { + Bias::Left + } else { + Bias::Right + }; + Selection { + id: selection.id, + start: buffer.anchor_after(selection.start), + end: buffer.anchor_at(selection.end, end_bias), + reversed: selection.reversed, + goal: selection.goal, + } + })); + + self.collection.pending = None; + self.selections_changed = true; + } + + pub fn select_anchors(&mut self, selections: Vec>) { + let buffer = self.buffer.read(self.cx).snapshot(self.cx); + let resolved_selections = + resolve_multiple::(&selections, &buffer).collect::>(); + self.select(resolved_selections); + } + + pub fn select_ranges(&mut self, ranges: I) + where + I: IntoIterator>, + T: ToOffset, + { + let buffer = self.buffer.read(self.cx).snapshot(self.cx); + let ranges = ranges + .into_iter() + .map(|range| range.start.to_offset(&buffer)..range.end.to_offset(&buffer)); + self.select_offset_ranges(ranges); + } + + fn select_offset_ranges(&mut self, ranges: I) + where + I: IntoIterator>, + { + let selections = ranges + .into_iter() + .map(|range| { + let mut start = range.start; + let mut end = range.end; + let reversed = if start > end { + mem::swap(&mut start, &mut end); + true + } else { + false + }; + Selection { + id: post_inc(&mut self.collection.next_selection_id), + start, + end, + reversed, + goal: SelectionGoal::None, + } + }) + .collect::>(); + + self.select(selections) + } + + pub fn select_anchor_ranges>>(&mut self, ranges: I) { + let buffer = self.buffer.read(self.cx).snapshot(self.cx); + let selections = ranges + .into_iter() + .map(|range| { + let mut start = range.start; + let mut end = range.end; + let reversed = if start.cmp(&end, &buffer).is_gt() { + mem::swap(&mut start, &mut end); + true + } else { + false + }; + Selection { + id: post_inc(&mut self.collection.next_selection_id), + start, + end, + reversed, + goal: SelectionGoal::None, + } + }) + .collect::>(); + + self.select_anchors(selections) + } + + pub fn new_selection_id(&mut self) -> usize { + post_inc(&mut self.next_selection_id) + } + + pub fn select_display_ranges(&mut self, ranges: T) + where + T: IntoIterator>, + { + let display_map = self.display_map(); + let selections = ranges + .into_iter() + .map(|range| { + let mut start = range.start; + let mut end = range.end; + let reversed = if start > end { + mem::swap(&mut start, &mut end); + true + } else { + false + }; + Selection { + id: post_inc(&mut self.collection.next_selection_id), + start: start.to_point(&display_map), + end: end.to_point(&display_map), + reversed, + goal: SelectionGoal::None, + } + }) + .collect(); + self.select(selections); + } + + pub fn move_with( + &mut self, + mut move_selection: impl FnMut(&DisplaySnapshot, &mut Selection), + ) { + let mut changed = false; + let display_map = self.display_map(); + let selections = self + .all::(self.cx) + .into_iter() + .map(|selection| { + let mut moved_selection = + selection.map(|point| point.to_display_point(&display_map)); + move_selection(&display_map, &mut moved_selection); + let moved_selection = + moved_selection.map(|display_point| display_point.to_point(&display_map)); + if selection != moved_selection { + changed = true; + } + moved_selection + }) + .collect(); + + if changed { + self.select(selections) + } + } + + pub fn move_offsets_with( + &mut self, + mut move_selection: impl FnMut(&MultiBufferSnapshot, &mut Selection), + ) { + let mut changed = false; + let snapshot = self.buffer().clone(); + let selections = self + .all::(self.cx) + .into_iter() + .map(|selection| { + let mut moved_selection = selection.clone(); + move_selection(&snapshot, &mut moved_selection); + if selection != moved_selection { + changed = true; + } + moved_selection + }) + .collect(); + drop(snapshot); + + if changed { + self.select(selections) + } + } + + pub fn move_heads_with( + &mut self, + mut update_head: impl FnMut( + &DisplaySnapshot, + DisplayPoint, + SelectionGoal, + ) -> (DisplayPoint, SelectionGoal), + ) { + self.move_with(|map, selection| { + let (new_head, new_goal) = update_head(map, selection.head(), selection.goal); + selection.set_head(new_head, new_goal); + }); + } + + pub fn move_cursors_with( + &mut self, + mut update_cursor_position: impl FnMut( + &DisplaySnapshot, + DisplayPoint, + SelectionGoal, + ) -> (DisplayPoint, SelectionGoal), + ) { + self.move_with(|map, selection| { + let (cursor, new_goal) = update_cursor_position(map, selection.head(), selection.goal); + selection.collapse_to(cursor, new_goal) + }); + } + + pub fn maybe_move_cursors_with( + &mut self, + mut update_cursor_position: impl FnMut( + &DisplaySnapshot, + DisplayPoint, + SelectionGoal, + ) -> Option<(DisplayPoint, SelectionGoal)>, + ) { + self.move_cursors_with(|map, point, goal| { + update_cursor_position(map, point, goal).unwrap_or((point, goal)) + }) + } + + pub fn replace_cursors_with( + &mut self, + mut find_replacement_cursors: impl FnMut(&DisplaySnapshot) -> Vec, + ) { + let display_map = self.display_map(); + let new_selections = find_replacement_cursors(&display_map) + .into_iter() + .map(|cursor| { + let cursor_point = cursor.to_point(&display_map); + Selection { + id: post_inc(&mut self.collection.next_selection_id), + start: cursor_point, + end: cursor_point, + reversed: false, + goal: SelectionGoal::None, + } + }) + .collect(); + self.select(new_selections); + } + + /// Compute new ranges for any selections that were located in excerpts that have + /// since been removed. + /// + /// Returns a `HashMap` indicating which selections whose former head position + /// was no longer present. The keys of the map are selection ids. The values are + /// the id of the new excerpt where the head of the selection has been moved. + pub fn refresh(&mut self) -> HashMap { + let mut pending = self.collection.pending.take(); + let mut selections_with_lost_position = HashMap::default(); + + let anchors_with_status = { + let buffer = self.buffer(); + let disjoint_anchors = self + .disjoint + .iter() + .flat_map(|selection| [&selection.start, &selection.end]); + buffer.refresh_anchors(disjoint_anchors) + }; + let adjusted_disjoint: Vec<_> = anchors_with_status + .chunks(2) + .map(|selection_anchors| { + let (anchor_ix, start, kept_start) = selection_anchors[0].clone(); + let (_, end, kept_end) = selection_anchors[1].clone(); + let selection = &self.disjoint[anchor_ix / 2]; + let kept_head = if selection.reversed { + kept_start + } else { + kept_end + }; + if !kept_head { + selections_with_lost_position.insert(selection.id, selection.head().excerpt_id); + } + + Selection { + id: selection.id, + start, + end, + reversed: selection.reversed, + goal: selection.goal, + } + }) + .collect(); + + if !adjusted_disjoint.is_empty() { + let resolved_selections = + resolve_multiple(adjusted_disjoint.iter(), &self.buffer()).collect(); + self.select::(resolved_selections); + } + + if let Some(pending) = pending.as_mut() { + let buffer = self.buffer(); + let anchors = + buffer.refresh_anchors([&pending.selection.start, &pending.selection.end]); + let (_, start, kept_start) = anchors[0].clone(); + let (_, end, kept_end) = anchors[1].clone(); + let kept_head = if pending.selection.reversed { + kept_start + } else { + kept_end + }; + if !kept_head { + selections_with_lost_position + .insert(pending.selection.id, pending.selection.head().excerpt_id); + } + + pending.selection.start = start; + pending.selection.end = end; + } + self.collection.pending = pending; + self.selections_changed = true; + + selections_with_lost_position + } +} + +impl<'a> Deref for MutableSelectionsCollection<'a> { + type Target = SelectionsCollection; + fn deref(&self) -> &Self::Target { + self.collection + } +} + +impl<'a> DerefMut for MutableSelectionsCollection<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.collection + } +} + +// Panics if passed selections are not in order +pub fn resolve_multiple<'a, D, I>( + selections: I, + snapshot: &MultiBufferSnapshot, +) -> impl 'a + Iterator> +where + D: TextDimension + Ord + Sub + std::fmt::Debug, + I: 'a + IntoIterator>, +{ + let (to_summarize, selections) = selections.into_iter().tee(); + let mut summaries = snapshot + .summaries_for_anchors::( + to_summarize + .flat_map(|s| [&s.start, &s.end]) + .collect::>(), + ) + .into_iter(); + selections.map(move |s| Selection { + id: s.id, + start: summaries.next().unwrap(), + end: summaries.next().unwrap(), + reversed: s.reversed, + goal: s.goal, + }) +} + +fn resolve>( + selection: &Selection, + buffer: &MultiBufferSnapshot, +) -> Selection { + selection.map(|p| p.summary::(buffer)) +} diff --git a/crates/editor2/src/test.rs b/crates/editor2/src/test.rs new file mode 100644 index 0000000000..08cc533d62 --- /dev/null +++ b/crates/editor2/src/test.rs @@ -0,0 +1,83 @@ +pub mod editor_lsp_test_context; +pub mod editor_test_context; + +use crate::{ + display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint}, + DisplayPoint, Editor, EditorMode, MultiBuffer, +}; + +use gpui::{ModelHandle, ViewContext}; + +use project::Project; +use util::test::{marked_text_offsets, marked_text_ranges}; + +#[cfg(test)] +#[ctor::ctor] +fn init_logger() { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } +} + +// Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one. +pub fn marked_display_snapshot( + text: &str, + cx: &mut gpui::AppContext, +) -> (DisplaySnapshot, Vec) { + let (unmarked_text, markers) = marked_text_offsets(text); + + let family_id = cx + .font_cache() + .load_family(&["Helvetica"], &Default::default()) + .unwrap(); + let font_id = cx + .font_cache() + .select_font(family_id, &Default::default()) + .unwrap(); + let font_size = 14.0; + + let buffer = MultiBuffer::build_simple(&unmarked_text, cx); + let display_map = + cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx)); + let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); + let markers = markers + .into_iter() + .map(|offset| offset.to_display_point(&snapshot)) + .collect(); + + (snapshot, markers) +} + +pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext) { + let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true); + assert_eq!(editor.text(cx), unmarked_text); + editor.change_selections(None, cx, |s| s.select_ranges(text_ranges)); +} + +pub fn assert_text_with_selections( + editor: &mut Editor, + marked_text: &str, + cx: &mut ViewContext, +) { + let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true); + assert_eq!(editor.text(cx), unmarked_text); + assert_eq!(editor.selections.ranges(cx), text_ranges); +} + +// RA thinks this is dead code even though it is used in a whole lot of tests +#[allow(dead_code)] +#[cfg(any(test, feature = "test-support"))] +pub(crate) fn build_editor( + buffer: ModelHandle, + cx: &mut ViewContext, +) -> Editor { + Editor::new(EditorMode::Full, buffer, None, None, cx) +} + +pub(crate) fn build_editor_with_project( + project: ModelHandle, + buffer: ModelHandle, + cx: &mut ViewContext, +) -> Editor { + Editor::new(EditorMode::Full, buffer, Some(project), None, cx) +} diff --git a/crates/editor2/src/test/editor_lsp_test_context.rs b/crates/editor2/src/test/editor_lsp_test_context.rs new file mode 100644 index 0000000000..3e2f38a0b6 --- /dev/null +++ b/crates/editor2/src/test/editor_lsp_test_context.rs @@ -0,0 +1,297 @@ +use std::{ + borrow::Cow, + ops::{Deref, DerefMut, Range}, + sync::Arc, +}; + +use anyhow::Result; + +use crate::{Editor, ToPoint}; +use collections::HashSet; +use futures::Future; +use gpui::{json, ViewContext, ViewHandle}; +use indoc::indoc; +use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries}; +use lsp::{notification, request}; +use multi_buffer::ToPointUtf16; +use project::Project; +use smol::stream::StreamExt; +use workspace::{AppState, Workspace, WorkspaceHandle}; + +use super::editor_test_context::EditorTestContext; + +pub struct EditorLspTestContext<'a> { + pub cx: EditorTestContext<'a>, + pub lsp: lsp::FakeLanguageServer, + pub workspace: ViewHandle, + pub buffer_lsp_url: lsp::Url, +} + +impl<'a> EditorLspTestContext<'a> { + pub async fn new( + mut language: Language, + capabilities: lsp::ServerCapabilities, + cx: &'a mut gpui::TestAppContext, + ) -> EditorLspTestContext<'a> { + use json::json; + + let app_state = cx.update(AppState::test); + + cx.update(|cx| { + language::init(cx); + crate::init(cx); + workspace::init(app_state.clone(), cx); + Project::init_settings(cx); + }); + + let file_name = format!( + "file.{}", + language + .path_suffixes() + .first() + .expect("language must have a path suffix for EditorLspTestContext") + ); + + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities, + ..Default::default() + })) + .await; + + let project = Project::test(app_state.fs.clone(), [], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + + app_state + .fs + .as_fake() + .insert_tree("/root", json!({ "dir": { file_name.clone(): "" }})) + .await; + + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); + project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/root", true, cx) + }) + .await + .unwrap(); + cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) + .await; + + let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); + let item = workspace + .update(cx, |workspace, cx| { + workspace.open_path(file, None, true, cx) + }) + .await + .expect("Could not open test file"); + + let editor = cx.update(|cx| { + item.act_as::(cx) + .expect("Opened test file wasn't an editor") + }); + editor.update(cx, |_, cx| cx.focus_self()); + + let lsp = fake_servers.next().await.unwrap(); + + Self { + cx: EditorTestContext { + cx, + window: window.into(), + editor, + }, + lsp, + workspace, + buffer_lsp_url: lsp::Url::from_file_path(format!("/root/dir/{file_name}")).unwrap(), + } + } + + pub async fn new_rust( + capabilities: lsp::ServerCapabilities, + cx: &'a mut gpui::TestAppContext, + ) -> EditorLspTestContext<'a> { + let language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_queries(LanguageQueries { + indents: Some(Cow::from(indoc! {r#" + [ + ((where_clause) _ @end) + (field_expression) + (call_expression) + (assignment_expression) + (let_declaration) + (let_chain) + (await_expression) + ] @indent + + (_ "[" "]" @end) @indent + (_ "<" ">" @end) @indent + (_ "{" "}" @end) @indent + (_ "(" ")" @end) @indent"#})), + brackets: Some(Cow::from(indoc! {r#" + ("(" @open ")" @close) + ("[" @open "]" @close) + ("{" @open "}" @close) + ("<" @open ">" @close) + ("\"" @open "\"" @close) + (closure_parameters "|" @open "|" @close)"#})), + ..Default::default() + }) + .expect("Could not parse queries"); + + Self::new(language, capabilities, cx).await + } + + pub async fn new_typescript( + capabilities: lsp::ServerCapabilities, + cx: &'a mut gpui::TestAppContext, + ) -> EditorLspTestContext<'a> { + let mut word_characters: HashSet = Default::default(); + word_characters.insert('$'); + word_characters.insert('#'); + let language = Language::new( + LanguageConfig { + name: "Typescript".into(), + path_suffixes: vec!["ts".to_string()], + brackets: language::BracketPairConfig { + pairs: vec![language::BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }], + disabled_scopes_by_bracket_ix: Default::default(), + }, + word_characters, + ..Default::default() + }, + Some(tree_sitter_typescript::language_typescript()), + ) + .with_queries(LanguageQueries { + brackets: Some(Cow::from(indoc! {r#" + ("(" @open ")" @close) + ("[" @open "]" @close) + ("{" @open "}" @close) + ("<" @open ">" @close) + ("\"" @open "\"" @close)"#})), + indents: Some(Cow::from(indoc! {r#" + [ + (call_expression) + (assignment_expression) + (member_expression) + (lexical_declaration) + (variable_declaration) + (assignment_expression) + (if_statement) + (for_statement) + ] @indent + + (_ "[" "]" @end) @indent + (_ "<" ">" @end) @indent + (_ "{" "}" @end) @indent + (_ "(" ")" @end) @indent + "#})), + ..Default::default() + }) + .expect("Could not parse queries"); + + Self::new(language, capabilities, cx).await + } + + // Constructs lsp range using a marked string with '[', ']' range delimiters + pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range { + let ranges = self.ranges(marked_text); + self.to_lsp_range(ranges[0].clone()) + } + + pub fn to_lsp_range(&mut self, range: Range) -> lsp::Range { + let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); + let start_point = range.start.to_point(&snapshot.buffer_snapshot); + let end_point = range.end.to_point(&snapshot.buffer_snapshot); + + self.editor(|editor, cx| { + let buffer = editor.buffer().read(cx); + let start = point_to_lsp( + buffer + .point_to_buffer_offset(start_point, cx) + .unwrap() + .1 + .to_point_utf16(&buffer.read(cx)), + ); + let end = point_to_lsp( + buffer + .point_to_buffer_offset(end_point, cx) + .unwrap() + .1 + .to_point_utf16(&buffer.read(cx)), + ); + + lsp::Range { start, end } + }) + } + + pub fn to_lsp(&mut self, offset: usize) -> lsp::Position { + let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); + let point = offset.to_point(&snapshot.buffer_snapshot); + + self.editor(|editor, cx| { + let buffer = editor.buffer().read(cx); + point_to_lsp( + buffer + .point_to_buffer_offset(point, cx) + .unwrap() + .1 + .to_point_utf16(&buffer.read(cx)), + ) + }) + } + + pub fn update_workspace(&mut self, update: F) -> T + where + F: FnOnce(&mut Workspace, &mut ViewContext) -> T, + { + self.workspace.update(self.cx.cx, update) + } + + pub fn handle_request( + &self, + mut handler: F, + ) -> futures::channel::mpsc::UnboundedReceiver<()> + where + T: 'static + request::Request, + T::Params: 'static + Send, + F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut, + Fut: 'static + Send + Future>, + { + let url = self.buffer_lsp_url.clone(); + self.lsp.handle_request::(move |params, cx| { + let url = url.clone(); + handler(url, params, cx) + }) + } + + pub fn notify(&self, params: T::Params) { + self.lsp.notify::(params); + } +} + +impl<'a> Deref for EditorLspTestContext<'a> { + type Target = EditorTestContext<'a>; + + fn deref(&self) -> &Self::Target { + &self.cx + } +} + +impl<'a> DerefMut for EditorLspTestContext<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.cx + } +} diff --git a/crates/editor2/src/test/editor_test_context.rs b/crates/editor2/src/test/editor_test_context.rs new file mode 100644 index 0000000000..c083ce0e68 --- /dev/null +++ b/crates/editor2/src/test/editor_test_context.rs @@ -0,0 +1,332 @@ +use crate::{ + display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer, +}; +use futures::Future; +use gpui::{ + executor::Foreground, keymap_matcher::Keystroke, AnyWindowHandle, AppContext, ContextHandle, + ModelContext, ViewContext, ViewHandle, +}; +use indoc::indoc; +use language::{Buffer, BufferSnapshot}; +use project::{FakeFs, Project}; +use std::{ + any::TypeId, + ops::{Deref, DerefMut, Range}, +}; +use util::{ + assert_set_eq, + test::{generate_marked_text, marked_text_ranges}, +}; + +use super::build_editor_with_project; + +pub struct EditorTestContext<'a> { + pub cx: &'a mut gpui::TestAppContext, + pub window: AnyWindowHandle, + pub editor: ViewHandle, +} + +impl<'a> EditorTestContext<'a> { + pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> { + let fs = FakeFs::new(cx.background()); + // fs.insert_file("/file", "".to_owned()).await; + fs.insert_tree( + "/root", + gpui::serde_json::json!({ + "file": "", + }), + ) + .await; + let project = Project::test(fs, ["/root".as_ref()], cx).await; + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/root/file", cx) + }) + .await + .unwrap(); + let window = cx.add_window(|cx| { + cx.focus_self(); + build_editor_with_project(project, MultiBuffer::build_from_buffer(buffer, cx), cx) + }); + let editor = window.root(cx); + Self { + cx, + window: window.into(), + editor, + } + } + + pub fn condition( + &self, + predicate: impl FnMut(&Editor, &AppContext) -> bool, + ) -> impl Future { + self.editor.condition(self.cx, predicate) + } + + pub fn editor(&self, read: F) -> T + where + F: FnOnce(&Editor, &ViewContext) -> T, + { + self.editor.read_with(self.cx, read) + } + + pub fn update_editor(&mut self, update: F) -> T + where + F: FnOnce(&mut Editor, &mut ViewContext) -> T, + { + self.editor.update(self.cx, update) + } + + pub fn multibuffer(&self, read: F) -> T + where + F: FnOnce(&MultiBuffer, &AppContext) -> T, + { + self.editor(|editor, cx| read(editor.buffer().read(cx), cx)) + } + + pub fn update_multibuffer(&mut self, update: F) -> T + where + F: FnOnce(&mut MultiBuffer, &mut ModelContext) -> T, + { + self.update_editor(|editor, cx| editor.buffer().update(cx, update)) + } + + pub fn buffer_text(&self) -> String { + self.multibuffer(|buffer, cx| buffer.snapshot(cx).text()) + } + + pub fn buffer(&self, read: F) -> T + where + F: FnOnce(&Buffer, &AppContext) -> T, + { + self.multibuffer(|multibuffer, cx| { + let buffer = multibuffer.as_singleton().unwrap().read(cx); + read(buffer, cx) + }) + } + + pub fn update_buffer(&mut self, update: F) -> T + where + F: FnOnce(&mut Buffer, &mut ModelContext) -> T, + { + self.update_multibuffer(|multibuffer, cx| { + let buffer = multibuffer.as_singleton().unwrap(); + buffer.update(cx, update) + }) + } + + pub fn buffer_snapshot(&self) -> BufferSnapshot { + self.buffer(|buffer, _| buffer.snapshot()) + } + + pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle { + let keystroke_under_test_handle = + self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text)); + let keystroke = Keystroke::parse(keystroke_text).unwrap(); + + self.cx.dispatch_keystroke(self.window, keystroke, false); + + keystroke_under_test_handle + } + + pub fn simulate_keystrokes( + &mut self, + keystroke_texts: [&str; COUNT], + ) -> ContextHandle { + let keystrokes_under_test_handle = + self.add_assertion_context(format!("Simulated Keystrokes: {:?}", keystroke_texts)); + for keystroke_text in keystroke_texts.into_iter() { + self.simulate_keystroke(keystroke_text); + } + // it is common for keyboard shortcuts to kick off async actions, so this ensures that they are complete + // before returning. + // NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too + // quickly races with async actions. + if let Foreground::Deterministic { cx_id: _, executor } = self.cx.foreground().as_ref() { + executor.run_until_parked(); + } else { + unreachable!(); + } + + keystrokes_under_test_handle + } + + pub fn ranges(&self, marked_text: &str) -> Vec> { + let (unmarked_text, ranges) = marked_text_ranges(marked_text, false); + assert_eq!(self.buffer_text(), unmarked_text); + ranges + } + + pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint { + let ranges = self.ranges(marked_text); + let snapshot = self + .editor + .update(self.cx, |editor, cx| editor.snapshot(cx)); + ranges[0].start.to_display_point(&snapshot) + } + + // Returns anchors for the current buffer using `«` and `»` + pub fn text_anchor_range(&self, marked_text: &str) -> Range { + let ranges = self.ranges(marked_text); + let snapshot = self.buffer_snapshot(); + snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end) + } + + pub fn set_diff_base(&mut self, diff_base: Option<&str>) { + let diff_base = diff_base.map(String::from); + self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base, cx)); + } + + /// Change the editor's text and selections using a string containing + /// embedded range markers that represent the ranges and directions of + /// each selection. + /// + /// Returns a context handle so that assertion failures can print what + /// editor state was needed to cause the failure. + /// + /// See the `util::test::marked_text_ranges` function for more information. + pub fn set_state(&mut self, marked_text: &str) -> ContextHandle { + let state_context = self.add_assertion_context(format!( + "Initial Editor State: \"{}\"", + marked_text.escape_debug().to_string() + )); + let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true); + self.editor.update(self.cx, |editor, cx| { + editor.set_text(unmarked_text, cx); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges(selection_ranges) + }) + }); + state_context + } + + /// Only change the editor's selections + pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle { + let state_context = self.add_assertion_context(format!( + "Initial Editor State: \"{}\"", + marked_text.escape_debug().to_string() + )); + let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true); + self.editor.update(self.cx, |editor, cx| { + assert_eq!(editor.text(cx), unmarked_text); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges(selection_ranges) + }) + }); + state_context + } + + /// Make an assertion about the editor's text and the ranges and directions + /// of its selections using a string containing embedded range markers. + /// + /// See the `util::test::marked_text_ranges` function for more information. + #[track_caller] + pub fn assert_editor_state(&mut self, marked_text: &str) { + let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true); + let buffer_text = self.buffer_text(); + + if buffer_text != unmarked_text { + panic!("Unmarked text doesn't match buffer text\nBuffer text: {buffer_text:?}\nUnmarked text: {unmarked_text:?}\nRaw buffer text\n{buffer_text}Raw unmarked text\n{unmarked_text}"); + } + + self.assert_selections(expected_selections, marked_text.to_string()) + } + + pub fn editor_state(&mut self) -> String { + generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true) + } + + #[track_caller] + pub fn assert_editor_background_highlights(&mut self, marked_text: &str) { + let expected_ranges = self.ranges(marked_text); + let actual_ranges: Vec> = self.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + editor + .background_highlights + .get(&TypeId::of::()) + .map(|h| h.1.clone()) + .unwrap_or_default() + .into_iter() + .map(|range| range.to_offset(&snapshot.buffer_snapshot)) + .collect() + }); + assert_set_eq!(actual_ranges, expected_ranges); + } + + #[track_caller] + pub fn assert_editor_text_highlights(&mut self, marked_text: &str) { + let expected_ranges = self.ranges(marked_text); + let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); + let actual_ranges: Vec> = snapshot + .text_highlight_ranges::() + .map(|ranges| ranges.as_ref().clone().1) + .unwrap_or_default() + .into_iter() + .map(|range| range.to_offset(&snapshot.buffer_snapshot)) + .collect(); + assert_set_eq!(actual_ranges, expected_ranges); + } + + #[track_caller] + pub fn assert_editor_selections(&mut self, expected_selections: Vec>) { + let expected_marked_text = + generate_marked_text(&self.buffer_text(), &expected_selections, true); + self.assert_selections(expected_selections, expected_marked_text) + } + + fn editor_selections(&self) -> Vec> { + self.editor + .read_with(self.cx, |editor, cx| editor.selections.all::(cx)) + .into_iter() + .map(|s| { + if s.reversed { + s.end..s.start + } else { + s.start..s.end + } + }) + .collect::>() + } + + #[track_caller] + fn assert_selections( + &mut self, + expected_selections: Vec>, + expected_marked_text: String, + ) { + let actual_selections = self.editor_selections(); + let actual_marked_text = + generate_marked_text(&self.buffer_text(), &actual_selections, true); + if expected_selections != actual_selections { + panic!( + indoc! {" + + {}Editor has unexpected selections. + + Expected selections: + {} + + Actual selections: + {} + "}, + self.assertion_context(), + expected_marked_text, + actual_marked_text, + ); + } + } +} + +impl<'a> Deref for EditorTestContext<'a> { + type Target = gpui::TestAppContext; + + fn deref(&self) -> &Self::Target { + self.cx + } +} + +impl<'a> DerefMut for EditorTestContext<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.cx + } +}