From 9e5275cc18f6d18b0e02b9f214e343ef7f5e390a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 2 Nov 2023 14:34:43 -0600 Subject: [PATCH 01/23] Fix error handling of open_path Co-Authored-By: Nathan --- crates/gpui2/src/executor.rs | 6 ++--- crates/zed2/Cargo.toml | 2 +- crates/zed2/src/main.rs | 44 ++++++++++++++++++++++++------------ crates/zed2/src/zed2.rs | 6 ++--- 4 files changed, 37 insertions(+), 21 deletions(-) diff --git a/crates/gpui2/src/executor.rs b/crates/gpui2/src/executor.rs index 25e88068c30d93f58d10aec2f9c0a998cbb1b6fb..018f575d2d8d6e9c34368d28638df42415dcb4c1 100644 --- a/crates/gpui2/src/executor.rs +++ b/crates/gpui2/src/executor.rs @@ -49,11 +49,11 @@ impl Task { impl Task> where - T: 'static + Send, - E: 'static + Send + Debug, + T: 'static, + E: 'static + Debug, { pub fn detach_and_log_err(self, cx: &mut AppContext) { - cx.background_executor().spawn(self.log_err()).detach(); + cx.foreground_executor().spawn(self.log_err()).detach(); } } diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 71963ff30bf7b19445c3ddd7547533785928d4ba..47aafe85fa60e2ae7c650bbe9442df78b0cb29c7 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -70,7 +70,7 @@ theme = { package = "theme2", path = "../theme2" } util = { path = "../util" } # semantic_index = { path = "../semantic_index" } # vim = { path = "../vim" } -workspace2 = { path = "../workspace2" } +workspace = { package = "workspace2", path = "../workspace2" } # welcome = { path = "../welcome" } # zed-actions = {path = "../zed-actions"} anyhow.workspace = true diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index f8b77fe9df554fb670e1c08654506ba5b0d4afa2..1e3b8d0d2bba686deec67a6555a7bfd46066dc98 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -34,7 +34,7 @@ use std::{ fs::OpenOptions, io::{IsTerminal, Write}, panic, - path::Path, + path::{Path, PathBuf}, sync::{ atomic::{AtomicU32, Ordering}, Arc, @@ -49,7 +49,7 @@ use util::{ paths, ResultExt, }; use uuid::Uuid; -use workspace2::{AppState, WorkspaceStore}; +use workspace::{AppState, WorkspaceStore}; use zed2::{build_window_options, initialize_workspace, languages}; use zed2::{ensure_only_instance, Assets, IsOnlyInstance}; @@ -191,7 +191,7 @@ fn main() { // audio::init(Assets, cx); // auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx); - workspace2::init(app_state.clone(), cx); + workspace::init(app_state.clone(), cx); // recent_projects::init(cx); // journal2::init(app_state.clone(), cx); @@ -210,6 +210,7 @@ fn main() { if stdout_is_a_pty() { cx.activate(true); let urls = collect_url_args(); + dbg!(&urls); if !urls.is_empty() { listener.open_urls(urls) } @@ -227,11 +228,27 @@ fn main() { let mut _triggered_authentication = false; + fn open_paths_and_log_errs( + paths: &[PathBuf], + app_state: &Arc, + cx: &mut AppContext, + ) { + let task = workspace::open_paths(&paths, &app_state, None, cx); + cx.spawn(|cx| async move { + if let Some((_window, results)) = task.await.log_err() { + for result in results { + if let Some(Err(e)) = result { + log::error!("Error opening path: {}", e); + } + } + } + }) + .detach(); + } + match open_rx.try_next() { - Ok(Some(OpenRequest::Paths { paths: _ })) => { - // todo!("workspace") - // cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) - // .detach(); + Ok(Some(OpenRequest::Paths { paths })) => { + open_paths_and_log_errs(&paths, &app_state, cx) } Ok(Some(OpenRequest::CliConnection { connection })) => { let app_state = app_state.clone(); @@ -263,10 +280,9 @@ fn main() { async move { while let Some(request) = open_rx.next().await { match request { - OpenRequest::Paths { paths: _ } => { - // todo!("workspace") - // cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) - // .detach(); + OpenRequest::Paths { paths } => { + cx.update(|cx| open_paths_and_log_errs(&paths, &app_state, cx)) + .ok(); } OpenRequest::CliConnection { connection } => { let app_state = app_state.clone(); @@ -324,8 +340,8 @@ async fn installation_id() -> Result { async fn restore_or_create_workspace(app_state: &Arc, mut cx: AsyncAppContext) { async_maybe!({ - if let Some(location) = workspace2::last_opened_workspace_paths().await { - cx.update(|cx| workspace2::open_paths(location.paths().as_ref(), app_state, None, cx))? + if let Some(location) = workspace::last_opened_workspace_paths().await { + cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))? .await .log_err(); } else if matches!(KEY_VALUE_STORE.read_kvp("******* THIS IS A BAD KEY PLEASE UNCOMMENT BELOW TO FIX THIS VERY LONG LINE *******"), Ok(None)) { @@ -335,7 +351,7 @@ async fn restore_or_create_workspace(app_state: &Arc, mut cx: AsyncApp // cx.update(|cx| show_welcome_experience(app_state, cx)); } else { cx.update(|cx| { - workspace2::open_new(app_state, cx, |workspace, cx| { + workspace::open_new(app_state, cx, |workspace, cx| { // todo!(editor) // Editor::new_file(workspace, &Default::default(), cx) }) diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 04778f29dd8e96ad6d440faeb83d7caed43eff1e..cd52ea33e1e21677108991e6cd07e4b809519209 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -27,7 +27,7 @@ use futures::{ use std::{path::Path, sync::Arc, thread, time::Duration}; use util::{paths::PathLikeWithPosition, ResultExt}; use uuid::Uuid; -use workspace2::{AppState, Workspace}; +use workspace::{AppState, Workspace}; pub fn connect_to_cli( server_name: &str, @@ -104,7 +104,7 @@ pub async fn handle_cli_connection( let mut errored = false; if let Some(open_paths_task) = cx - .update(|cx| workspace2::open_paths(&paths, &app_state, None, cx)) + .update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) .log_err() { match open_paths_task.await { @@ -258,7 +258,7 @@ pub fn initialize_workspace( let workspace_handle = cx.view(); cx.subscribe(&workspace_handle, { move |workspace, _, event, cx| { - if let workspace2::Event::PaneAdded(pane) = event { + if let workspace::Event::PaneAdded(pane) = event { pane.update(cx, |pane, cx| { // todo!() // pane.toolbar().update(cx, |toolbar, cx| { From 09efa017d4bf7c33c06092ddb6c9df9ed2e50395 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 2 Nov 2023 14:36:42 -0600 Subject: [PATCH 02/23] Editor2 --- crates/editor2/Cargo.toml | 92 + crates/editor2/src/blink_manager.rs | 113 + crates/editor2/src/display_map.rs | 1912 +++ crates/editor2/src/display_map/block_map.rs | 1667 +++ crates/editor2/src/display_map/fold_map.rs | 1706 +++ crates/editor2/src/display_map/inlay_map.rs | 1895 +++ crates/editor2/src/display_map/tab_map.rs | 765 ++ crates/editor2/src/display_map/wrap_map.rs | 1355 +++ crates/editor2/src/editor.rs | 10095 ++++++++++++++++ crates/editor2/src/editor_settings.rs | 62 + crates/editor2/src/editor_tests.rs | 8195 +++++++++++++ crates/editor2/src/element.rs | 3478 ++++++ crates/editor2/src/git.rs | 282 + .../editor2/src/highlight_matching_bracket.rs | 138 + crates/editor2/src/hover_popover.rs | 1329 ++ crates/editor2/src/inlay_hint_cache.rs | 3349 +++++ crates/editor2/src/items.rs | 1327 ++ crates/editor2/src/link_go_to_definition.rs | 1269 ++ crates/editor2/src/mouse_context_menu.rs | 96 + crates/editor2/src/movement.rs | 927 ++ crates/editor2/src/persistence.rs | 83 + crates/editor2/src/scroll.rs | 436 + crates/editor2/src/scroll/actions.rs | 152 + crates/editor2/src/scroll/autoscroll.rs | 258 + crates/editor2/src/scroll/scroll_amount.rs | 28 + crates/editor2/src/selections_collection.rs | 886 ++ crates/editor2/src/test.rs | 83 + .../src/test/editor_lsp_test_context.rs | 297 + .../editor2/src/test/editor_test_context.rs | 332 + 29 files changed, 42607 insertions(+) create mode 100644 crates/editor2/Cargo.toml create mode 100644 crates/editor2/src/blink_manager.rs create mode 100644 crates/editor2/src/display_map.rs create mode 100644 crates/editor2/src/display_map/block_map.rs create mode 100644 crates/editor2/src/display_map/fold_map.rs create mode 100644 crates/editor2/src/display_map/inlay_map.rs create mode 100644 crates/editor2/src/display_map/tab_map.rs create mode 100644 crates/editor2/src/display_map/wrap_map.rs create mode 100644 crates/editor2/src/editor.rs create mode 100644 crates/editor2/src/editor_settings.rs create mode 100644 crates/editor2/src/editor_tests.rs create mode 100644 crates/editor2/src/element.rs create mode 100644 crates/editor2/src/git.rs create mode 100644 crates/editor2/src/highlight_matching_bracket.rs create mode 100644 crates/editor2/src/hover_popover.rs create mode 100644 crates/editor2/src/inlay_hint_cache.rs create mode 100644 crates/editor2/src/items.rs create mode 100644 crates/editor2/src/link_go_to_definition.rs create mode 100644 crates/editor2/src/mouse_context_menu.rs create mode 100644 crates/editor2/src/movement.rs create mode 100644 crates/editor2/src/persistence.rs create mode 100644 crates/editor2/src/scroll.rs create mode 100644 crates/editor2/src/scroll/actions.rs create mode 100644 crates/editor2/src/scroll/autoscroll.rs create mode 100644 crates/editor2/src/scroll/scroll_amount.rs create mode 100644 crates/editor2/src/selections_collection.rs create mode 100644 crates/editor2/src/test.rs create mode 100644 crates/editor2/src/test/editor_lsp_test_context.rs create mode 100644 crates/editor2/src/test/editor_test_context.rs diff --git a/crates/editor2/Cargo.toml b/crates/editor2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..5113b5e7de60a5d8c947d4048e8f59ec42ac6db7 --- /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 0000000000000000000000000000000000000000..fa5a3af0c69717de9d5eb9d76374914644836479 --- /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 0000000000000000000000000000000000000000..1d6deb910aa8d55d8f780cdfd8ce169662cfc940 --- /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 0000000000000000000000000000000000000000..c07625bf9c1b758279c2e0cb939619bfd9457919 --- /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 0000000000000000000000000000000000000000..4636d9a17f756325a59a1c63364be52379fbdb3e --- /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 0000000000000000000000000000000000000000..c0c352453b8805393a9d5da1048c1c2bce48e624 --- /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 0000000000000000000000000000000000000000..6b38ea2d243e77fcb8ea16e8d2d29ba83c4b003b --- /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 0000000000000000000000000000000000000000..60337661c1abfed638dd861a4c2e9464f70cf2ca --- /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 0000000000000000000000000000000000000000..4e449bb7f7732c8d65968c3723d800e29278e748 --- /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 0000000000000000000000000000000000000000..75f8b800f93757bec3bcbbf01b68226880267d57 --- /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 0000000000000000000000000000000000000000..feca741737c26eb357b1188c0b09470b7768b180 --- /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 0000000000000000000000000000000000000000..7b1155890fdd166228bc2b6bf5b0d263d030637c --- /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 0000000000000000000000000000000000000000..f8c6ef9a1fe8fe2a32a15b02de35e62b0b3b8964 --- /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 0000000000000000000000000000000000000000..a0baf6882fba4952488701ad8b36a25e5566332a --- /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 0000000000000000000000000000000000000000..5b3985edf933b75deaaa3ba49803a6151442be37 --- /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 0000000000000000000000000000000000000000..6b2712e7bf98fd81f89b6369ddfa7d9465ecec24 --- /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 0000000000000000000000000000000000000000..c87606070e5660c90a7e53962098b4da923a2f1e --- /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 0000000000000000000000000000000000000000..7da0b88622ff4152127b46714c62394210b4f4d0 --- /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 0000000000000000000000000000000000000000..8dfdcdff53b77b8bb1dcb41c71104b9901406b94 --- /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 0000000000000000000000000000000000000000..332eb3c1c5f852691555bf60fed9d6716f2fd2bb --- /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 0000000000000000000000000000000000000000..6e37735c1371ff466e345c95d0fa92d6d3c892a6 --- /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 0000000000000000000000000000000000000000..8233f92a1a24e526855f1d8108995d8893ccc642 --- /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 0000000000000000000000000000000000000000..82c2e10589a4d15e8f1c1fb0ac158764878255f1 --- /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 0000000000000000000000000000000000000000..ffada50179fa233b12e4a02b4fed6e52bcf137ca --- /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 0000000000000000000000000000000000000000..2cb22d15163323eae5f396e2415b973d099aae74 --- /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 0000000000000000000000000000000000000000..4b2dc855c39312fe62c38c621e086601901011e2 --- /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 0000000000000000000000000000000000000000..08cc533d62c376d9e3d2871e2fbb391dba591caf --- /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 0000000000000000000000000000000000000000..3e2f38a0b665e964eb51583c13d4cd4a87422b3d --- /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 0000000000000000000000000000000000000000..c083ce0e682e3164ec29b7bb9087c0f3a2ac32c0 --- /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 + } +} From 583c36e24b4e90042fd37f8fc03cbebeaf15b302 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 2 Nov 2023 15:27:49 -0600 Subject: [PATCH 03/23] WIP --- Cargo.lock | 55 + crates/editor2/Cargo.toml | 44 +- crates/editor2/src/blink_manager.rs | 6 +- crates/editor2/src/display_map.rs | 11 +- crates/editor2/src/display_map/block_map.rs | 1358 +- crates/editor2/src/display_map/fold_map.rs | 4 +- crates/editor2/src/display_map/inlay_map.rs | 2 +- crates/editor2/src/display_map/wrap_map.rs | 678 +- crates/editor2/src/editor.rs | 14747 +++++++++--------- crates/editor2/src/editor_settings.rs | 4 +- crates/editor2/src/element.rs | 6484 ++++---- crates/editor2/src/hover_popover.rs | 2 +- crates/editor2/src/inlay_hint_cache.rs | 4 +- crates/editor2/src/items.rs | 316 +- crates/editor2/src/movement.rs | 2 +- crates/editor2/src/scroll/actions.rs | 292 +- crates/editor2/src/selections_collection.rs | 2 +- crates/editor2/src/test.rs | 2 +- crates/language2/src/buffer.rs | 1 + crates/workspace2/src/workspace2.rs | 15 +- crates/zed2/Cargo.toml | 2 +- 21 files changed, 12029 insertions(+), 12002 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index db8e88cb1cc02a7dd66bdd87a900096e5a9e2c2d..e08eb58f5e25b362f59cd9fdf92060bb36ad7e4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2622,6 +2622,60 @@ dependencies = [ "workspace", ] +[[package]] +name = "editor2" +version = "0.1.0" +dependencies = [ + "aho-corasick", + "anyhow", + "client2", + "clock", + "collections", + "context_menu", + "convert_case 0.6.0", + "copilot2", + "ctor", + "db2", + "drag_and_drop", + "env_logger 0.9.3", + "futures 0.3.28", + "fuzzy2", + "git", + "gpui2", + "indoc", + "itertools 0.10.5", + "language2", + "lazy_static", + "log", + "lsp2", + "multi_buffer", + "ordered-float 2.10.0", + "parking_lot 0.11.2", + "postage", + "project2", + "rand 0.8.5", + "rich_text", + "rpc2", + "schemars", + "serde", + "serde_derive", + "settings2", + "smallvec", + "smol", + "snippet", + "sqlez", + "sum_tree", + "text2", + "theme2", + "tree-sitter", + "tree-sitter-html", + "tree-sitter-rust", + "tree-sitter-typescript", + "unindent", + "util", + "workspace2", +] + [[package]] name = "either" version = "1.9.0" @@ -11071,6 +11125,7 @@ dependencies = [ "copilot2", "ctor", "db2", + "editor2", "env_logger 0.9.3", "feature_flags2", "fs2", diff --git a/crates/editor2/Cargo.toml b/crates/editor2/Cargo.toml index 5113b5e7de60a5d8c947d4048e8f59ec42ac6db7..3f94d5cdc024f82d542bf412f2757d2407d462fb 100644 --- a/crates/editor2/Cargo.toml +++ b/crates/editor2/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "editor" +name = "editor2" version = "0.1.0" edition = "2021" publish = false @@ -23,30 +23,30 @@ test-support = [ ] [dependencies] -client = { path = "../client" } +client = { package = "client2", path = "../client2" } clock = { path = "../clock" } -copilot = { path = "../copilot" } -db = { path = "../db" } +copilot = { package="copilot2", path = "../copilot2" } +db = { package="db2", path = "../db2" } drag_and_drop = { path = "../drag_and_drop" } collections = { path = "../collections" } context_menu = { path = "../context_menu" } -fuzzy = { path = "../fuzzy" } +fuzzy = { package = "fuzzy2", path = "../fuzzy2" } git = { path = "../git" } -gpui = { path = "../gpui" } -language = { path = "../language" } -lsp = { path = "../lsp" } +gpui = { package = "gpui2", path = "../gpui2" } +language = { package = "language2", path = "../language2" } +lsp = { package = "lsp2", path = "../lsp2" } multi_buffer = { path = "../multi_buffer" } -project = { path = "../project" } -rpc = { path = "../rpc" } +project = { package = "project2", path = "../project2" } +rpc = { package = "rpc2", path = "../rpc2" } rich_text = { path = "../rich_text" } -settings = { path = "../settings" } +settings = { package="settings2", path = "../settings2" } snippet = { path = "../snippet" } sum_tree = { path = "../sum_tree" } -text = { path = "../text" } -theme = { path = "../theme" } +text = { package="text2", path = "../text2" } +theme = { package="theme2", path = "../theme2" } util = { path = "../util" } sqlez = { path = "../sqlez" } -workspace = { path = "../workspace" } +workspace = { package="workspace2", path = "../workspace2" } aho-corasick = "1.1" anyhow.workspace = true @@ -71,15 +71,15 @@ 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"] } +copilot = { package="copilot2", path = "../copilot2", features = ["test-support"] } +text = { package="text2", path = "../text2", features = ["test-support"] } +language = { package="language2", path = "../language2", features = ["test-support"] } +lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", 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"] } +project = { package = "project2", path = "../project2", features = ["test-support"] } +settings = { package = "settings2", path = "../settings2", features = ["test-support"] } +workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } multi_buffer = { path = "../multi_buffer", features = ["test-support"] } ctor.workspace = true diff --git a/crates/editor2/src/blink_manager.rs b/crates/editor2/src/blink_manager.rs index fa5a3af0c69717de9d5eb9d76374914644836479..7d242b6684d58bb7df791b6ccaf876510c1db9cd 100644 --- a/crates/editor2/src/blink_manager.rs +++ b/crates/editor2/src/blink_manager.rs @@ -61,7 +61,7 @@ impl BlinkManager { } fn blink_cursors(&mut self, epoch: usize, cx: &mut ModelContext) { - if settings::get::(cx).cursor_blink { + if EditorSettings::get_global(cx).cursor_blink { if epoch == self.blink_epoch && self.enabled && !self.blinking_paused { self.visible = !self.visible; cx.notify(); @@ -107,7 +107,3 @@ impl BlinkManager { self.visible } } - -impl Entity for BlinkManager { - type Event = (); -} diff --git a/crates/editor2/src/display_map.rs b/crates/editor2/src/display_map.rs index 1d6deb910aa8d55d8f780cdfd8ce169662cfc940..a26b07efec74b4f037516da755d470f3ccdaf0ba 100644 --- a/crates/editor2/src/display_map.rs +++ b/crates/editor2/src/display_map.rs @@ -12,10 +12,9 @@ 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, + Entity, Hsla, Model, ModelContext, }; use inlay_map::InlayMap; use language::{ @@ -49,12 +48,12 @@ type TextHighlights = TreeMap, Arc<(HighlightStyle, Vec>; pub struct DisplayMap { - buffer: ModelHandle, + buffer: Model, buffer_subscription: BufferSubscription, fold_map: FoldMap, inlay_map: InlayMap, tab_map: TabMap, - wrap_map: ModelHandle, + wrap_map: Model, block_map: BlockMap, text_highlights: TextHighlights, inlay_highlights: InlayHighlights, @@ -67,7 +66,7 @@ impl Entity for DisplayMap { impl DisplayMap { pub fn new( - buffer: ModelHandle, + buffer: Model, font_id: FontId, font_size: f32, wrap_width: Option, @@ -1015,7 +1014,7 @@ pub mod tests { movement, test::{editor_test_context::EditorTestContext, marked_display_snapshot}, }; - use gpui::{color::Color, elements::*, test::observe, AppContext}; + use gpui::{elements::*, test::observe, AppContext, Hsla}; use language::{ language_settings::{AllLanguageSettings, AllLanguageSettingsContent}, Buffer, Language, LanguageConfig, SelectionGoal, diff --git a/crates/editor2/src/display_map/block_map.rs b/crates/editor2/src/display_map/block_map.rs index c07625bf9c1b758279c2e0cb939619bfd9457919..e6830a9039cb8836d1a64ee72b584ca109f4758a 100644 --- a/crates/editor2/src/display_map/block_map.rs +++ b/crates/editor2/src/display_map/block_map.rs @@ -80,8 +80,8 @@ pub enum BlockStyle { Sticky, } -pub struct BlockContext<'a, 'b, 'c> { - pub view_context: &'c mut ViewContext<'a, 'b, Editor>, +pub struct BlockContext<'a, 'b> { + pub view_context: &'b mut ViewContext<'a, Editor>, pub anchor_x: f32, pub scroll_x: f32, pub gutter_width: f32, @@ -988,680 +988,680 @@ fn offset_for_row(s: &str, target: u32) -> (u32, 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) - } - } -} +// #[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::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 index 4636d9a17f756325a59a1c63364be52379fbdb3e..4645f644519bd43b3564703f7d6d69f233baf5f5 100644 --- a/crates/editor2/src/display_map/fold_map.rs +++ b/crates/editor2/src/display_map/fold_map.rs @@ -3,7 +3,7 @@ use super::{ Highlights, }; use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset}; -use gpui::{color::Color, fonts::HighlightStyle}; +use gpui::{fonts::HighlightStyle, Hsla}; use language::{Chunk, Edit, Point, TextSummary}; use std::{ any::TypeId, @@ -174,7 +174,7 @@ impl<'a> FoldMapWriter<'a> { pub struct FoldMap { snapshot: FoldSnapshot, - ellipses_color: Option, + ellipses_color: Option, } impl FoldMap { diff --git a/crates/editor2/src/display_map/inlay_map.rs b/crates/editor2/src/display_map/inlay_map.rs index c0c352453b8805393a9d5da1048c1c2bce48e624..fbd638429a7e05bbe8a03e2b84b626d7178f6edb 100644 --- a/crates/editor2/src/display_map/inlay_map.rs +++ b/crates/editor2/src/display_map/inlay_map.rs @@ -1890,6 +1890,6 @@ mod tests { fn init_test(cx: &mut AppContext) { cx.set_global(SettingsStore::test(cx)); - theme::init((), cx); + theme::init(cx); } } diff --git a/crates/editor2/src/display_map/wrap_map.rs b/crates/editor2/src/display_map/wrap_map.rs index 60337661c1abfed638dd861a4c2e9464f70cf2ca..706e16c24fcb62622a7a7bb3590d4c67f0584f2e 100644 --- a/crates/editor2/src/display_map/wrap_map.rs +++ b/crates/editor2/src/display_map/wrap_map.rs @@ -4,9 +4,7 @@ use super::{ Highlights, }; use crate::MultiBufferSnapshot; -use gpui::{ - fonts::FontId, text_layout::LineWrapper, AppContext, Entity, ModelContext, ModelHandle, Task, -}; +use gpui::{AppContext, Entity, Model, ModelContext, Task}; use language::{Chunk, Point}; use lazy_static::lazy_static; use smol::future::yield_now; @@ -27,10 +25,6 @@ pub struct WrapMap { font: (FontId, f32), } -impl Entity for WrapMap { - type Event = (); -} - #[derive(Clone)] pub struct WrapSnapshot { tab_snapshot: TabSnapshot, @@ -78,7 +72,7 @@ impl WrapMap { font_size: f32, wrap_width: Option, cx: &mut AppContext, - ) -> (ModelHandle, WrapSnapshot) { + ) -> (Model, WrapSnapshot) { let handle = cx.add_model(|cx| { let mut this = Self { font: (font_id, font_size), @@ -1019,337 +1013,337 @@ fn consolidate_wrap_edits(edits: &mut Vec) { } } -#[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 - ); - } - } - } -} +// #[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 index 4e449bb7f7732c8d65968c3723d800e29278e748..196527acdbebfbb7fe77b9a4c6d7f4bddd260136 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -38,18 +38,9 @@ pub use element::{ 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, + serde_json, AnyElement, AppContext, AsyncAppContext, ClipboardItem, + Element, Entity, Hsla, Model, Subscription, Task, View, ViewContext, + WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -59,8 +50,8 @@ 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, + point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, + Completion, CursorShape, Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, LanguageRegistry, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, }; @@ -551,7 +542,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(Editor::context_menu_last); hover_popover::init(cx); - scroll::actions::init(cx); + /scroll::actions::init(cx); workspace::register_project_item::(cx); workspace::register_followable_item::(cx); @@ -626,8 +617,8 @@ type InlayBackgroundHighlight = (fn(&Theme) -> Color, Vec); pub struct Editor { handle: WeakViewHandle, - buffer: ModelHandle, - display_map: ModelHandle, + buffer: Model, + display_map: Model, pub selections: SelectionsCollection, pub scroll_manager: ScrollManager, columnar_selection_tail: Option, @@ -643,10 +634,10 @@ pub struct Editor { soft_wrap_mode_override: Option, get_field_editor_theme: Option>, override_text_style: Option>, - project: Option>, + project: Option>, collaboration_hub: Option>, focused: bool, - blink_manager: ModelHandle, + blink_manager: Model, pub show_local_selections: bool, mode: EditorMode, show_gutter: bool, @@ -660,7 +651,7 @@ pub struct Editor { mouse_context_menu: ViewHandle, completion_tasks: Vec<(CompletionId, Task>)>, next_completion_id: CompletionId, - available_code_actions: Option<(ModelHandle, Arc<[CodeAction]>)>, + available_code_actions: Option<(Model, Arc<[CodeAction]>)>, code_actions_task: Option>, document_highlights_task: Option>, pending_rename: Option, @@ -678,7 +669,7 @@ pub struct Editor { gutter_hovered: bool, link_go_to_definition_state: LinkGoToDefinitionState, copilot_state: CopilotState, - inlay_hint_cache: InlayHintCache, + // inlay_hint_cache: InlayHintCache, next_inlay_id: usize, _subscriptions: Vec, pixel_position_of_newest_cursor: Option, @@ -851,7 +842,7 @@ enum ContextMenu { impl ContextMenu { fn select_first( &mut self, - project: Option<&ModelHandle>, + project: Option<&Model>, cx: &mut ViewContext, ) -> bool { if self.visible() { @@ -867,7 +858,7 @@ impl ContextMenu { fn select_prev( &mut self, - project: Option<&ModelHandle>, + project: Option<&Model>, cx: &mut ViewContext, ) -> bool { if self.visible() { @@ -883,7 +874,7 @@ impl ContextMenu { fn select_next( &mut self, - project: Option<&ModelHandle>, + project: Option<&Model>, cx: &mut ViewContext, ) -> bool { if self.visible() { @@ -899,7 +890,7 @@ impl ContextMenu { fn select_last( &mut self, - project: Option<&ModelHandle>, + project: Option<&Model>, cx: &mut ViewContext, ) -> bool { if self.visible() { @@ -938,7 +929,7 @@ impl ContextMenu { struct CompletionsMenu { id: CompletionId, initial_position: Anchor, - buffer: ModelHandle, + buffer: Model, completions: Arc>>, match_candidates: Arc<[StringMatchCandidate]>, matches: Arc<[StringMatch]>, @@ -949,7 +940,7 @@ struct CompletionsMenu { impl CompletionsMenu { fn select_first( &mut self, - project: Option<&ModelHandle>, + project: Option<&Model>, cx: &mut ViewContext, ) { self.selected_item = 0; @@ -960,7 +951,7 @@ impl CompletionsMenu { fn select_prev( &mut self, - project: Option<&ModelHandle>, + project: Option<&Model>, cx: &mut ViewContext, ) { if self.selected_item > 0 { @@ -975,7 +966,7 @@ impl CompletionsMenu { fn select_next( &mut self, - project: Option<&ModelHandle>, + project: Option<&Model>, cx: &mut ViewContext, ) { if self.selected_item + 1 < self.matches.len() { @@ -990,7 +981,7 @@ impl CompletionsMenu { fn select_last( &mut self, - project: Option<&ModelHandle>, + project: Option<&Model>, cx: &mut ViewContext, ) { self.selected_item = self.matches.len() - 1; @@ -1001,7 +992,7 @@ impl CompletionsMenu { fn pre_resolve_completion_documentation( &self, - project: Option>, + project: Option>, cx: &mut ViewContext, ) { let settings = settings::get::(cx); @@ -1089,7 +1080,7 @@ impl CompletionsMenu { fn attempt_resolve_selected_completion_documentation( &mut self, - project: Option<&ModelHandle>, + project: Option<&Model>, cx: &mut ViewContext, ) { let settings = settings::get::(cx); @@ -1519,7 +1510,7 @@ impl CompletionsMenu { #[derive(Clone)] struct CodeActionsMenu { actions: Arc<[CodeAction]>, - buffer: ModelHandle, + buffer: Model, selected_item: usize, list: UniformListState, deployed_from_indicator: bool, @@ -1798,7347 +1789,7349 @@ impl InlayHintRefreshReason { } } -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 - } -} +// 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: Model, +// 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: Model, +// 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: Model, +// 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) -> &Model { +// &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, Model, 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) { +// todo!(); +// // self.refresh_inlay_hints( +// // InlayHintRefreshReason::Toggle(!self.inlay_hint_cache.enabled), +// // cx, +// // ); +// } + +// pub fn inlay_hints_enabled(&self) -> bool { +// todo!(); +// 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: Model, +// 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, _: Model, cx: &mut ViewContext) { +// cx.notify(); +// } + +// fn on_buffer_event( +// &mut self, +// multibuffer: Model, +// 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, _: Model, 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; @@ -9148,7 +9141,7 @@ pub trait CollaborationHub { ) -> &'a HashMap; } -impl CollaborationHub for ModelHandle { +impl CollaborationHub for Model { fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap { self.read(cx).collaborators() } @@ -9164,7 +9157,7 @@ impl CollaborationHub for ModelHandle { fn inlay_hint_settings( location: Anchor, snapshot: &MultiBufferSnapshot, - cx: &mut ViewContext<'_, '_, Editor>, + cx: &mut ViewContext<'_, Editor>, ) -> InlayHintSettings { let file = snapshot.file_at(location); let language = snapshot.language_at(location); @@ -9267,7 +9260,7 @@ pub enum Event { text: Arc, }, ExcerptsAdded { - buffer: ModelHandle, + buffer: Model, predecessor: ExcerptId, excerpts: Vec<(ExcerptId, ExcerptRange)>, }, diff --git a/crates/editor2/src/editor_settings.rs b/crates/editor2/src/editor_settings.rs index 75f8b800f93757bec3bcbbf01b68226880267d57..349d1c62fa9b6c5623ae7312a1d224500ceebc8f 100644 --- a/crates/editor2/src/editor_settings.rs +++ b/crates/editor2/src/editor_settings.rs @@ -1,6 +1,6 @@ +use gpui::Settings; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::Setting; #[derive(Deserialize)] pub struct EditorSettings { @@ -47,7 +47,7 @@ pub struct ScrollbarContent { pub selections: Option, } -impl Setting for EditorSettings { +impl Settings for EditorSettings { const KEY: Option<&'static str> = None; type FileContent = EditorSettingsContent; diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 7b1155890fdd166228bc2b6bf5b0d263d030637c..3583085fcc6b3928493e42c2368b775dad4649a1 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -1,60 +1,14 @@ use super::{ - display_map::{BlockContext, ToDisplayPoint}, - Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, SelectPhase, SoftWrap, ToPoint, - MAX_LINE_LEN, + display_map::ToDisplayPoint, DisplayPoint, Editor, EditorSnapshot, 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, - }, + display_map::{BlockStyle, DisplaySnapshot}, 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; +use gpui::{AnyElement, Bounds, Element, Hsla, Line, Pixels, Size, Style, TextRun, TextSystem}; +use language::{CursorShape, Selection}; +use std::{ops::Range, sync::Arc}; +use sum_tree::Bias; enum FoldMarkers {} @@ -129,1695 +83,1695 @@ impl EditorElement { } } - 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) - } + // fn attach_mouse_handlers( + // position_map: &Arc, + // has_popovers: bool, + // visible_bounds: Bounds, + // text_bounds: Bounds, + // gutter_bounds: Bounds, + // bounds: Bounds, + // 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: Bounds, + // gutter_bounds: Bounds, + // 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: gpui::Point, + // position_map: &PositionMap, + // text_bounds: Bounds, + // 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: gpui::Point, + // cmd: bool, + // shift: bool, + // alt: bool, + // position_map: &PositionMap, + // text_bounds: Bounds, + // 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: Bounds, + // 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 = gpui::Point::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(gpui::Point::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: Bounds, + // 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: gpui::Point, + // mut delta: gpui::Point, + // precise: bool, + // position_map: &PositionMap, + // bounds: Bounds, + // 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(gpui::Point::zero(), position_map.scroll_max); + // editor.scroll(scroll_position, axis, cx); + + // true + // } + + // fn paint_background( + // &self, + // gutter_bounds: Bounds, + // text_bounds: Bounds, + // 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: Bounds::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: Bounds::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: Bounds::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: Bounds, + // visible_bounds: Bounds, + // 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: Bounds, 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 = Bounds::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 = Bounds::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 = Bounds::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: Bounds, + // visible_bounds: Bounds, + // 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, + // Bounds::from_points(gpui::Point::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, + // Bounds::from_points(gpui::Point::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, + // Bounds::from_points(gpui::Point::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: &Bounds) -> f32 { + // bounds.max_x() - self.style.theme.scrollbar.width + // } + + // fn paint_scrollbar( + // &mut self, + // bounds: Bounds, + // 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 = Bounds::from_points(vec2f(left, top), vec2f(right, bottom)); + // let thumb_bounds = Bounds::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 = Bounds::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 = Bounds::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: gpui::Point, + // scroll_top: f32, + // scroll_left: f32, + // bounds: Bounds, + // 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: Bounds, + // visible_bounds: Bounds, + // 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, - ) - } + // 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: gpui::Point::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)] @@ -1826,194 +1780,194 @@ pub struct LineWithInvisibles { 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); - } - } -} +// 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: gpui::Point, +// scroll_left: f32, +// visible_text_bounds: Bounds, +// whitespace_setting: ShowWhitespaceSetting, +// selection_ranges: &[Range], +// visible_bounds: Bounds, +// 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: gpui::Point, +// scroll_left: f32, +// line_y: f32, +// row: u32, +// visible_bounds: Bounds, +// 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 { @@ -2022,640 +1976,688 @@ enum Invisible { } 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, - }; + type ElementState = (); - 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 id(&self) -> Option { + None } - fn paint( + fn initialize( &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(); + view_state: &mut Editor, + element_state: Option, + cx: &mut gpui::ViewContext, + ) -> Self::ElementState { + () } - 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 layout( + &mut self, + view_state: &mut Editor, + element_state: &mut Self::ElementState, + cx: &mut gpui::ViewContext, + ) -> gpui::LayoutId { + cx.request_layout(Style::default().size_full(), None) } - fn debug( - &self, - bounds: RectF, - _: &Self::LayoutState, - _: &Self::PaintState, - _: &Editor, - _: &ViewContext, - ) -> json::Value { - json!({ - "type": "BufferElement", - "bounds": bounds.to_json() - }) + fn paint( + &mut self, + bounds: Bounds, + view_state: &mut Editor, + element_state: &mut Self::ElementState, + cx: &mut gpui::ViewContext, + ) { + let text_style = cx.text_style(); + + let layout_text = cx.text_system().layout_text( + "hello world", + text_style.font_size, + &[TextRun { + len: "hello world".len(), + font: text_style.font, + color: text_style.color, + underline: text_style.underline, + }], + None, + ); } } +// impl EditorElement { +// type LayoutState = LayoutState; +// type PaintState = (); + +// fn layout( +// &mut self, +// constraint: SizeConstraint, +// editor: &mut Editor, +// cx: &mut ViewContext, +// ) -> (gpui::Point, 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: gpui::Point::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: gpui::Point::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: Bounds, +// visible_bounds: Bounds, +// 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 = Bounds::new(bounds.origin(), layout.gutter_size); +// let text_bounds = Bounds::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: Bounds, +// _: Bounds, +// layout: &Self::LayoutState, +// _: &Self::PaintState, +// _: &Editor, +// _: &ViewContext, +// ) -> Option> { +// let text_bounds = Bounds::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(Bounds::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: Bounds, +// _: &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, -} +// pub struct LayoutState { +// position_map: Arc, +// gutter_size: gpui::Point, +// gutter_padding: f32, +// gutter_margin: f32, +// text_size: gpui::Point, +// 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, + size: Size, + line_height: Pixels, + scroll_max: Size, + em_width: Pixels, + em_advance: Pixels, line_layouts: Vec, snapshot: EditorSnapshot, } @@ -2689,7 +2691,11 @@ impl PointForPosition { } impl PositionMap { - fn point_for_position(&self, text_bounds: RectF, position: Vector2F) -> PointForPosition { + fn point_for_position( + &self, + text_bounds: Bounds, + position: gpui::Point, + ) -> 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()); @@ -2734,8 +2740,8 @@ fn layout_line( row: u32, snapshot: &EditorSnapshot, style: &EditorStyle, - layout_cache: &TextLayoutCache, -) -> text_layout::Line { + text_system: &TextSystem, +) -> Line { let mut line = snapshot.line(row); if line.len() > MAX_LINE_LEN { @@ -2747,103 +2753,101 @@ fn layout_line( line.truncate(len); } - layout_cache.layout_str( + text_system.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(), - }, - )], + &[TextRun { + len: snapshot.line_len(row) as usize, + font: style.text.font.clone(), + color: Hsla::black(), + underline: Default::default(), + }], ) } #[derive(Debug)] pub struct Cursor { - origin: Vector2F, - block_width: f32, - line_height: f32, - color: Color, + origin: gpui::Point, + block_width: Pixels, + line_height: Pixels, + color: Hsla, 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 - } + // pub fn new( + // origin: gpui::Point, + // 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: gpui::Point) -> Bounds { + // Bounds::new( + // self.origin + origin, + // vec2f(self.block_width, self.line_height), + // ) + // } + + // pub fn paint(&self, origin: gpui::Point, cx: &mut WindowContext) { + // let bounds = match self.shape { + // CursorShape::Bar => Bounds::new(self.origin + origin, vec2f(2.0, self.line_height)), + // CursorShape::Block | CursorShape::Hollow => Bounds::new( + // self.origin + origin, + // vec2f(self.block_width, self.line_height), + // ), + // CursorShape::Underscore => Bounds::new( + // self.origin + origin + gpui::Point::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 start_y: Pixels, + pub line_height: Pixels, pub lines: Vec, - pub color: Color, - pub corner_radius: f32, + pub color: Hsla, + pub corner_radius: Pixels, } #[derive(Debug)] @@ -2853,178 +2857,178 @@ pub struct HighlightedRangeLine { } 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))); - } + // pub fn paint(&self, bounds: Bounds, 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: Bounds, + // 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() -} +// fn range_to_bounds( +// range: &Range, +// content_origin: gpui::Point, +// scroll_left: f32, +// scroll_top: f32, +// visible_row_range: &Range, +// line_end_overshoot: f32, +// position_map: &PositionMap, +// ) -> impl Iterator> { +// let mut bounds: SmallVec<[Bounds; 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(Bounds::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 @@ -3034,445 +3038,445 @@ 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() - } -} +// #[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 = Bounds::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/hover_popover.rs b/crates/editor2/src/hover_popover.rs index 5b3985edf933b75deaaa3ba49803a6151442be37..89946638b1a8894a9f582f487b3232bb7e731354 100644 --- a/crates/editor2/src/hover_popover.rs +++ b/crates/editor2/src/hover_popover.rs @@ -9,7 +9,7 @@ use gpui::{ actions, elements::{Flex, MouseEventHandler, Padding, ParentElement, Text}, platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, Element, ModelHandle, Task, ViewContext, WeakViewHandle, + AnyElement, AppContext, Element, Model, Task, ViewContext, WeakViewHandle, }; use language::{ markdown, Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, ParsedMarkdown, diff --git a/crates/editor2/src/inlay_hint_cache.rs b/crates/editor2/src/inlay_hint_cache.rs index 6b2712e7bf98fd81f89b6369ddfa7d9465ecec24..c5f090084ef1721a433ad64b3cb3711559046329 100644 --- a/crates/editor2/src/inlay_hint_cache.rs +++ b/crates/editor2/src/inlay_hint_cache.rs @@ -11,7 +11,7 @@ use crate::{ use anyhow::Context; use clock::Global; use futures::future; -use gpui::{ModelContext, ModelHandle, Task, ViewContext}; +use gpui::{Model, ModelContext, Task, ViewContext}; use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot}; use parking_lot::RwLock; use project::{InlayHint, ResolveState}; @@ -3244,7 +3244,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" cx.update(|cx| { cx.set_global(SettingsStore::test(cx)); - theme::init((), cx); + theme::init(cx); client::init_settings(cx); language::init(cx); Project::init_settings(cx); diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index c87606070e5660c90a7e53962098b4da923a2f1e..2d249f03740a77235182806ec916fbe609559334 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -7,10 +7,8 @@ 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, + point, AnyElement, AppContext, AsyncAppContext, Entity, Model, Pixels, Subscription, Task, + View, ViewContext, WeakView, }; use language::{ proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point, @@ -49,12 +47,12 @@ impl FollowableItem for Editor { } fn from_state_proto( - pane: ViewHandle, - workspace: ViewHandle, + pane: View, + workspace: View, remote_id: ViewId, state: &mut Option, cx: &mut AppContext, - ) -> Option>>> { + ) -> Option>>> { let project = workspace.read(cx).project().to_owned(); let Some(proto::view::Variant::Editor(_)) = state else { return None; @@ -286,7 +284,7 @@ impl FollowableItem for Editor { fn apply_update_proto( &mut self, - project: &ModelHandle, + project: &Model, message: update_view::Variant, cx: &mut ViewContext, ) -> Task> { @@ -312,8 +310,8 @@ impl FollowableItem for Editor { } async fn update_editor_from_message( - this: WeakViewHandle, - project: ModelHandle, + this: WeakView, + project: Model, message: proto::update_view::Editor, cx: &mut AsyncAppContext, ) -> Result<()> { @@ -353,7 +351,7 @@ async fn update_editor_from_message( continue; }; let buffer_id = excerpt.buffer_id; - let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { + let Some(buffer) = project.read(cx).buffer_for_id(buffer_id) else { continue; }; @@ -430,7 +428,7 @@ async fn update_editor_from_message( editor.set_scroll_anchor_remote( ScrollAnchor { anchor: scroll_top_anchor, - offset: vec2f(message.scroll_x, message.scroll_y), + offset: point(message.scroll_x, message.scroll_y), }, cx, ); @@ -573,29 +571,26 @@ impl Item for Editor { } } - 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 tab_content(&self, detail: Option, cx: &AppContext) -> AnyElement { + todo!(); + + // 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)) { @@ -646,11 +641,7 @@ impl Item for Editor { } } - fn save( - &mut self, - project: ModelHandle, - cx: &mut ViewContext, - ) -> Task> { + fn save(&mut self, project: Model, 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(); @@ -690,7 +681,7 @@ impl Item for Editor { fn save_as( &mut self, - project: ModelHandle, + project: Model, abs_path: PathBuf, cx: &mut ViewContext, ) -> Task> { @@ -710,11 +701,7 @@ impl Item for Editor { }) } - fn reload( - &mut self, - project: ModelHandle, - cx: &mut ViewContext, - ) -> Task> { + fn reload(&mut self, project: Model, cx: &mut ViewContext) -> Task> { let buffer = self.buffer().clone(); let buffers = self.buffer.read(cx).all_buffers(); let reload_buffers = @@ -761,11 +748,11 @@ impl Item for Editor { result } - fn as_searchable(&self, handle: &ViewHandle) -> Option> { + fn as_searchable(&self, handle: &View) -> Option> { Some(Box::new(handle.clone())) } - fn pixel_position_of_cursor(&self, _: &AppContext) -> Option { + fn pixel_position_of_cursor(&self, _: &AppContext) -> Option> { self.pixel_position_of_newest_cursor } @@ -773,35 +760,36 @@ impl Item for Editor { 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 breadcrumbs(&self, cx: &AppContext) -> Option> { + todo!(); + // 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) { @@ -810,7 +798,7 @@ impl Item for Editor { self.workspace = Some((workspace.weak_handle(), workspace.database_id())); fn serialize( - buffer: ModelHandle, + buffer: Model, workspace_id: WorkspaceId, item_id: ItemId, cx: &mut AppContext, @@ -847,12 +835,12 @@ impl Item for Editor { } fn deserialize( - project: ModelHandle, - _workspace: WeakViewHandle, + project: Model, + _workspace: WeakView, workspace_id: workspace::WorkspaceId, item_id: ItemId, cx: &mut ViewContext, - ) -> Task>> { + ) -> 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 @@ -894,8 +882,8 @@ impl ProjectItem for Editor { type Item = Buffer; fn for_project_item( - project: ModelHandle, - buffer: ModelHandle, + project: Model, + buffer: Model, cx: &mut ViewContext, ) -> Self { Self::for_buffer(buffer, Some(project), cx) @@ -1134,89 +1122,89 @@ pub struct CursorPosition { _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(); - } -} +// 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: View, 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, + buffer: &Model, height: usize, include_filename: bool, cx: &'a AppContext, diff --git a/crates/editor2/src/movement.rs b/crates/editor2/src/movement.rs index 332eb3c1c5f852691555bf60fed9d6716f2fd2bb..05b9b039c474c77bd831db7e76d728c96878041f 100644 --- a/crates/editor2/src/movement.rs +++ b/crates/editor2/src/movement.rs @@ -919,7 +919,7 @@ mod tests { fn init_test(cx: &mut gpui::AppContext) { cx.set_global(SettingsStore::test(cx)); - theme::init((), cx); + theme::init(cx); language::init(cx); crate::init(cx); Project::init_settings(cx); diff --git a/crates/editor2/src/scroll/actions.rs b/crates/editor2/src/scroll/actions.rs index 82c2e10589a4d15e8f1c1fb0ac158764878255f1..e943bed7671bf9147776eb096ae3ae867f949ff2 100644 --- a/crates/editor2/src/scroll/actions.rs +++ b/crates/editor2/src/scroll/actions.rs @@ -1,152 +1,148 @@ -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, - ] -); +use gpui::AppContext; + +// 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) - }); + /// todo!() + // 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, - ) - } -} +// 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/selections_collection.rs b/crates/editor2/src/selections_collection.rs index 4b2dc855c39312fe62c38c621e086601901011e2..151a27c90198a656f4bb806bf5eab167f1dfd3c2 100644 --- a/crates/editor2/src/selections_collection.rs +++ b/crates/editor2/src/selections_collection.rs @@ -6,7 +6,7 @@ use std::{ }; use collections::HashMap; -use gpui::{AppContext, ModelHandle}; +use gpui::{AppContext, Model}; use itertools::Itertools; use language::{Bias, Point, Selection, SelectionGoal, TextDimension, ToPoint}; use util::post_inc; diff --git a/crates/editor2/src/test.rs b/crates/editor2/src/test.rs index 08cc533d62c376d9e3d2871e2fbb391dba591caf..45a61e58de6abab97a1969a581cec4546e5bc079 100644 --- a/crates/editor2/src/test.rs +++ b/crates/editor2/src/test.rs @@ -6,7 +6,7 @@ use crate::{ DisplayPoint, Editor, EditorMode, MultiBuffer, }; -use gpui::{ModelHandle, ViewContext}; +use gpui::{Model, ViewContext}; use project::Project; use util::test::{marked_text_offsets, marked_text_ranges}; diff --git a/crates/language2/src/buffer.rs b/crates/language2/src/buffer.rs index 36c1f39e1c7c388860e76f5d34e96486aa64718c..b19cf973546e847f60e4e66eef751cdba49c4d42 100644 --- a/crates/language2/src/buffer.rs +++ b/crates/language2/src/buffer.rs @@ -1,6 +1,7 @@ pub use crate::{ diagnostic_set::DiagnosticSet, highlight_map::{HighlightId, HighlightMap}, + markdown::ParsedMarkdown, proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, PLAIN_TEXT, }; use crate::{ diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 6d29751073e386cfd04c871df6e333e267fda51c..bf4ed73c274cace6e01ecd5dee769e50dbd78340 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -13,9 +13,12 @@ mod status_bar; mod toolbar; mod workspace_settings; -use crate::persistence::model::{ - DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup, - SerializedWorkspace, +pub use crate::persistence::{ + model::{ + DockData, DockStructure, ItemId, SerializedItem, SerializedPane, SerializedPaneGroup, + SerializedWorkspace, + }, + WorkspaceDb, }; use anyhow::{anyhow, Context as _, Result}; use call2::ActiveCall; @@ -44,15 +47,13 @@ use node_runtime::NodeRuntime; use notifications::{simple_message_notification::MessageNotification, NotificationHandle}; pub use pane::*; pub use pane_group::*; -use persistence::{ - model::{ItemId, WorkspaceLocation}, - DB, -}; +use persistence::{model::WorkspaceLocation, DB}; use postage::stream::Stream; use project2::{Project, ProjectEntryId, ProjectPath, Worktree}; use serde::Deserialize; use settings2::Settings; use status_bar::StatusBar; +pub use status_bar::StatusItemView; use std::{ any::TypeId, borrow::Cow, diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 47aafe85fa60e2ae7c650bbe9442df78b0cb29c7..d510de24ee5a1e71a2fe75fddac1d790f8fbab8d 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -34,7 +34,7 @@ copilot = { package = "copilot2", path = "../copilot2" } # copilot_button = { path = "../copilot_button" } # diagnostics = { path = "../diagnostics" } db = { package = "db2", path = "../db2" } -# editor = { path = "../editor" } +editor = { package="editor2", path = "../editor2" } # feedback = { path = "../feedback" } # file_finder = { path = "../file_finder" } # search = { path = "../search" } From 7b712ac68fa53d9aaefb3397f9c1915d90820203 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 2 Nov 2023 16:47:14 -0600 Subject: [PATCH 04/23] WIP --- Cargo.lock | 1 + crates/editor2/src/display_map.rs | 1857 +- crates/editor2/src/display_map/fold_map.rs | 8 +- crates/editor2/src/display_map/inlay_map.rs | 2 +- crates/editor2/src/display_map/wrap_map.rs | 4 +- crates/editor2/src/editor.rs | 423 +- crates/editor2/src/editor_tests.rs | 16386 ++++++++-------- crates/editor2/src/git.rs | 364 +- .../editor2/src/highlight_matching_bracket.rs | 198 +- crates/editor2/src/hover_popover.rs | 1649 +- crates/editor2/src/inlay_hint_cache.rs | 4318 ++-- crates/editor2/src/items.rs | 11 +- crates/editor2/src/link_go_to_definition.rs | 1403 +- crates/editor2/src/mouse_context_menu.rs | 122 +- crates/editor2/src/movement.rs | 5 +- crates/editor2/src/scroll.rs | 63 +- crates/editor2/src/scroll/actions.rs | 2 +- crates/editor2/src/selections_collection.rs | 6 +- crates/editor2/src/test.rs | 9 +- .../src/test/editor_lsp_test_context.rs | 2 +- .../editor2/src/test/editor_test_context.rs | 343 +- crates/gpui2/src/text_system.rs | 2 +- crates/language2/Cargo.toml | 1 + crates/language2/src/language2.rs | 1 + crates/language2/src/markdown.rs | 301 + 25 files changed, 13875 insertions(+), 13606 deletions(-) create mode 100644 crates/language2/src/markdown.rs diff --git a/Cargo.lock b/Cargo.lock index e08eb58f5e25b362f59cd9fdf92060bb36ad7e4d..29509ad4b59acb3f604a3f7e2c1fb1601d9bcba7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4408,6 +4408,7 @@ dependencies = [ "lsp2", "parking_lot 0.11.2", "postage", + "pulldown-cmark", "rand 0.8.5", "regex", "rpc2", diff --git a/crates/editor2/src/display_map.rs b/crates/editor2/src/display_map.rs index a26b07efec74b4f037516da755d470f3ccdaf0ba..5848a12531a435f37b26378afda8eb0d4430c605 100644 --- a/crates/editor2/src/display_map.rs +++ b/crates/editor2/src/display_map.rs @@ -11,11 +11,7 @@ use crate::{ pub use block_map::{BlockMap, BlockPoint}; use collections::{BTreeMap, HashMap, HashSet}; use fold_map::FoldMap; -use gpui::{ - fonts::{FontId, HighlightStyle, Underline}, - text_layout::{Line, RunStyle}, - Entity, Hsla, Model, ModelContext, -}; +use gpui::{FontId, HighlightStyle, Hsla, Line, Model, ModelContext, UnderlineStyle}; use inlay_map::InlayMap; use language::{ language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription, @@ -60,10 +56,6 @@ pub struct DisplayMap { pub clip_at_line_ends: bool, } -impl Entity for DisplayMap { - type Event = (); -} - impl DisplayMap { pub fn new( buffer: Model, @@ -253,7 +245,7 @@ impl DisplayMap { .update(cx, |map, cx| map.set_font(font_id, font_size, cx)) } - pub fn set_fold_ellipses_color(&mut self, color: Color) -> bool { + pub fn set_fold_ellipses_color(&mut self, color: Hsla) -> bool { self.fold_map.set_ellipses_color(color) } @@ -295,7 +287,7 @@ impl DisplayMap { self.block_map.read(snapshot, edits); } - fn tab_size(buffer: &ModelHandle, cx: &mut ModelContext) -> NonZeroU32 { + fn tab_size(buffer: &Model, cx: &mut ModelContext) -> NonZeroU32 { let language = buffer .read(cx) .as_singleton() @@ -540,10 +532,10 @@ impl DisplaySnapshot { // 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 { + diagnostic_highlight.underline = Some(UnderlineStyle { color: Some(diagnostic_style.message.text.color), thickness: 1.0.into(), - squiggly: true, + wavy: true, }); } } @@ -566,8 +558,8 @@ impl DisplaySnapshot { &self, display_row: u32, TextLayoutDetails { - font_cache, - text_layout_cache, + text_system: font_cache, + text_system: text_layout_cache, editor_style, }: &TextLayoutDetails, ) -> Line { @@ -591,14 +583,12 @@ impl DisplaySnapshot { }; 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, - }, - )); + styles.push( + todo!(), // len: chunk.chunk.len(), + // font_id: text_style.font_id, + // color: text_style.color, + // underline: text_style.underline, + ); } // our pixel positioning logic assumes each line ends in \n, @@ -607,17 +597,16 @@ impl DisplaySnapshot { 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, - }, - )); + todo!(); + // styles.push(RunStyle { + // len: "\n".len(), + // 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) + text_layout_cache.layout_text(&line, editor_style.text.font_size, &styles) } pub fn x_for_point( @@ -1007,905 +996,905 @@ pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterat }) } -#[cfg(test)] -pub mod tests { - use super::*; - use crate::{ - movement, - test::{editor_test_context::EditorTestContext, marked_display_snapshot}, - }; - use gpui::{elements::*, test::observe, AppContext, Hsla}; - 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); - }); - } -} +// #[cfg(test)] +// pub mod tests { +// use super::*; +// use crate::{ +// movement, +// test::{editor_test_context::EditorTestContext, marked_display_snapshot}, +// }; +// use gpui::{AppContext, Hsla}; +// 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(), Hsla::red().into()), +// ("fn.name".to_string(), Hsla::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(Hsla::blue())), +// ("() {}\n\nmod module ".to_string(), None), +// ("{\n fn ".to_string(), Some(Hsla::red())), +// ("inner".to_string(), Some(Hsla::blue())), +// ("() {}\n}".to_string(), Some(Hsla::red())), +// ] +// ); +// assert_eq!( +// cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)), +// vec![ +// (" fn ".to_string(), Some(Hsla::red())), +// ("inner".to_string(), Some(Hsla::blue())), +// ("() {}\n}".to_string(), Some(Hsla::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(Hsla::blue())), +// ("⋯".to_string(), None), +// (" fn ".to_string(), Some(Hsla::red())), +// ("inner".to_string(), Some(Hsla::blue())), +// ("() {}\n}".to_string(), Some(Hsla::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(), Hsla::red().into()), +// ("fn.name".to_string(), Hsla::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(Hsla::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(Hsla::blue())), +// ("⋯\n".to_string(), None), +// (" \nfn ".to_string(), Some(Hsla::red())), +// ("i\n".to_string(), Some(Hsla::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(), Hsla::red().into()), +// ("string".to_string(), Hsla::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(Hsla::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(Hsla::blue())), +// (":".to_string(), Some(Hsla::red()), None), +// (" B = ".to_string(), None, None), +// ("\"c ".to_string(), Some(Hsla::green()), None), +// ("d".to_string(), Some(Hsla::green()), Some(Hsla::blue())), +// ("\"".to_string(), Some(Hsla::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: &Model, +// 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: &Model, +// 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/fold_map.rs b/crates/editor2/src/display_map/fold_map.rs index 4645f644519bd43b3564703f7d6d69f233baf5f5..61047043dffbcae84aa418dbc1eda0c8ccd9e8bd 100644 --- a/crates/editor2/src/display_map/fold_map.rs +++ b/crates/editor2/src/display_map/fold_map.rs @@ -3,7 +3,7 @@ use super::{ Highlights, }; use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset}; -use gpui::{fonts::HighlightStyle, Hsla}; +use gpui::{HighlightStyle, Hsla}; use language::{Chunk, Edit, Point, TextSummary}; use std::{ any::TypeId, @@ -221,7 +221,7 @@ impl FoldMap { (FoldMapWriter(self), snapshot, edits) } - pub fn set_ellipses_color(&mut self, color: Color) -> bool { + pub fn set_ellipses_color(&mut self, color: Hsla) -> bool { if self.ellipses_color != Some(color) { self.ellipses_color = Some(color); true @@ -469,7 +469,7 @@ pub struct FoldSnapshot { folds: SumTree, pub inlay_snapshot: InlaySnapshot, pub version: usize, - pub ellipses_color: Option, + pub ellipses_color: Option, } impl FoldSnapshot { @@ -959,7 +959,7 @@ pub struct FoldChunks<'a> { inlay_offset: InlayOffset, output_offset: usize, max_output_offset: usize, - ellipses_color: Option, + ellipses_color: Option, } impl<'a> Iterator for FoldChunks<'a> { diff --git a/crates/editor2/src/display_map/inlay_map.rs b/crates/editor2/src/display_map/inlay_map.rs index fbd638429a7e05bbe8a03e2b84b626d7178f6edb..0afed4028d117708825080b2ff1bc479c2b0fa3b 100644 --- a/crates/editor2/src/display_map/inlay_map.rs +++ b/crates/editor2/src/display_map/inlay_map.rs @@ -1,6 +1,6 @@ use crate::{Anchor, InlayId, MultiBufferSnapshot, ToOffset}; use collections::{BTreeMap, BTreeSet}; -use gpui::fonts::HighlightStyle; +use gpui::HighlightStyle; use language::{Chunk, Edit, Point, TextSummary}; use multi_buffer::{MultiBufferChunks, MultiBufferRows}; use std::{ diff --git a/crates/editor2/src/display_map/wrap_map.rs b/crates/editor2/src/display_map/wrap_map.rs index 706e16c24fcb62622a7a7bb3590d4c67f0584f2e..c2f181efcdaf6fca4e68e28ad8c7065fc3e3fbf2 100644 --- a/crates/editor2/src/display_map/wrap_map.rs +++ b/crates/editor2/src/display_map/wrap_map.rs @@ -4,7 +4,7 @@ use super::{ Highlights, }; use crate::MultiBufferSnapshot; -use gpui::{AppContext, Entity, Model, ModelContext, Task}; +use gpui::{AppContext, FontId, Model, ModelContext, Pixels, Task}; use language::{Chunk, Point}; use lazy_static::lazy_static; use smol::future::yield_now; @@ -22,7 +22,7 @@ pub struct WrapMap { edits_since_sync: Patch, wrap_width: Option, background_task: Option>, - font: (FontId, f32), + font: (FontId, Pixels), } #[derive(Clone)] diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 196527acdbebfbb7fe77b9a4c6d7f4bddd260136..a7378aa705a8b4378fbbe1e069243fdf1ab2828e 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -38,9 +38,8 @@ pub use element::{ use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - serde_json, AnyElement, AppContext, AsyncAppContext, ClipboardItem, - Element, Entity, Hsla, Model, Subscription, Task, View, ViewContext, - WindowContext, + serde_json, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, Hsla, + Model, Quad, Subscription, Task, Text, View, ViewContext, WeakView, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -50,10 +49,10 @@ use itertools::Itertools; pub use language::{char_kind, CharKind}; use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, - point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, - Completion, CursorShape, Diagnostic, DiagnosticSeverity, File, IndentKind, - IndentSize, Language, LanguageRegistry, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, - Selection, SelectionGoal, TransactionId, + point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, + CursorShape, Diagnostic, DiagnosticSeverity, 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, @@ -113,7 +112,7 @@ pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); pub fn render_parsed_markdown( parsed: &language::ParsedMarkdown, editor_style: &EditorStyle, - workspace: Option>, + workspace: Option>, cx: &mut ViewContext, ) -> Text { enum RenderedMarkdown {} @@ -124,51 +123,55 @@ pub fn render_parsed_markdown( 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) + todo!() + // 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.draw_quad(Quad { + // bounds, + // background: Some(code_span_background_color), + // corner_radii: (2.0).into(), + // order: todo!(), + // content_mask: todo!(), + // border_color: todo!(), + // border_widths: todo!(), + // }); + // } + // }) + // .with_soft_wrap(true) } #[derive(Clone, Deserialize, PartialEq, Default)] @@ -416,133 +419,133 @@ pub fn init_settings(cx: &mut AppContext) { 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); + // 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); + scroll::actions::init(cx); workspace::register_project_item::(cx); workspace::register_followable_item::(cx); @@ -571,7 +574,7 @@ pub enum SelectPhase { Update { position: DisplayPoint, goal_column: u32, - scroll_position: Vector2F, + scroll_position: Point, }, End, } @@ -612,11 +615,11 @@ 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); +type BackgroundHighlight = (fn(&Theme) -> Hsla, Vec>); +type InlayBackgroundHighlight = (fn(&Theme) -> Hsla, Vec); pub struct Editor { - handle: WeakViewHandle, + handle: WeakView, buffer: Model, display_map: Model, pub selections: SelectionsCollection, @@ -648,7 +651,7 @@ pub struct Editor { inlay_background_highlights: TreeMap, InlayBackgroundHighlight>, nav_history: Option, context_menu: RwLock>, - mouse_context_menu: ViewHandle, + mouse_context_menu: View, completion_tasks: Vec<(CompletionId, Task>)>, next_completion_id: CompletionId, available_code_actions: Option<(Model, Arc<[CodeAction]>)>, @@ -659,7 +662,7 @@ pub struct Editor { cursor_shape: CursorShape, collapse_matches: bool, autoindent_mode: Option, - workspace: Option<(WeakViewHandle, i64)>, + workspace: Option<(WeakView, i64)>, keymap_context_layers: BTreeMap, input_enabled: bool, read_only: bool, @@ -672,7 +675,7 @@ pub struct Editor { // inlay_hint_cache: InlayHintCache, next_inlay_id: usize, _subscriptions: Vec, - pixel_position_of_newest_cursor: Option, + pixel_position_of_newest_cursor: Option>, } pub struct EditorSnapshot { @@ -828,7 +831,7 @@ struct SnippetState { pub struct RenameState { pub range: Range, pub old_name: Arc, - pub editor: ViewHandle, + pub editor: View, block_id: BlockId, } @@ -915,7 +918,7 @@ impl ContextMenu { &self, cursor_position: DisplayPoint, style: EditorStyle, - workspace: Option>, + workspace: Option>, cx: &mut ViewContext, ) -> (DisplayPoint, AnyElement) { match self { @@ -938,22 +941,14 @@ struct CompletionsMenu { } impl CompletionsMenu { - fn select_first( - &mut self, - project: Option<&Model>, - cx: &mut ViewContext, - ) { + fn select_first(&mut self, project: Option<&Model>, 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<&Model>, - cx: &mut ViewContext, - ) { + fn select_prev(&mut self, project: Option<&Model>, cx: &mut ViewContext) { if self.selected_item > 0 { self.selected_item -= 1; } else { @@ -964,11 +959,7 @@ impl CompletionsMenu { cx.notify(); } - fn select_next( - &mut self, - project: Option<&Model>, - cx: &mut ViewContext, - ) { + fn select_next(&mut self, project: Option<&Model>, cx: &mut ViewContext) { if self.selected_item + 1 < self.matches.len() { self.selected_item += 1; } else { @@ -979,11 +970,7 @@ impl CompletionsMenu { cx.notify(); } - fn select_last( - &mut self, - project: Option<&Model>, - cx: &mut ViewContext, - ) { + fn select_last(&mut self, project: Option<&Model>, 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); @@ -1241,7 +1228,7 @@ impl CompletionsMenu { fn render( &self, style: EditorStyle, - workspace: Option>, + workspace: Option>, cx: &mut ViewContext, ) -> AnyElement { enum CompletionTag {} @@ -1760,7 +1747,7 @@ pub struct NavigationData { scroll_top_row: u32, } -pub struct EditorCreated(pub ViewHandle); +pub struct EditorCreated(pub View); enum GotoDefinitionKind { Symbol, @@ -3845,8 +3832,8 @@ impl InlayHintRefreshReason { // } // async fn open_project_transaction( -// this: &WeakViewHandle, -// workspace: WeakViewHandle, +// this: &WeakViewHandle Vector2F { + pub fn scroll_position(&self) -> Point { self.scroll_anchor.scroll_position(&self.display_snapshot) } } @@ -9286,9 +9273,9 @@ pub enum Event { Closed, } -pub struct EditorFocused(pub ViewHandle); -pub struct EditorBlurred(pub ViewHandle); -pub struct EditorReleased(pub WeakViewHandle); +pub struct EditorFocused(pub View); +pub struct EditorBlurred(pub View); +pub struct EditorReleased(pub WeakView); impl Entity for Editor { type Event = Event; @@ -9323,7 +9310,7 @@ impl View for Editor { "Editor" } - fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext) { + fn focus_in(&mut self, focused: AnyView, cx: &mut ViewContext) { if cx.is_self_focused() { let focused_event = EditorFocused(cx.handle()); cx.emit(Event::Focused); @@ -9350,7 +9337,7 @@ impl View for Editor { } } - fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + fn focus_out(&mut self, _: AnyView, cx: &mut ViewContext) { let blurred_event = EditorBlurred(cx.handle()); cx.emit_global(blurred_event); self.focused = false; @@ -9649,7 +9636,7 @@ fn build_style( settings: &ThemeSettings, get_field_editor_theme: Option<&GetFieldEditorTheme>, override_text_style: Option<&OverrideTextStyle>, - cx: &AppContext, + cx: &mut AppContext, ) -> EditorStyle { let font_cache = cx.font_cache(); let line_height_scalar = settings.line_height(); diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index feca741737c26eb357b1188c0b09470b7768b180..5b5a40ba8e0ac0b3c000ec908edbb39fceb0d060 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -1,8195 +1,8191 @@ -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); -} +// 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, Point::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, Point::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, Point::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, Point::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, Point::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, Point::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(Point::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(Point::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, Point::zero(), cx); +// view.end_selection(cx); + +// view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx); +// view.update_selection(DisplayPoint::new(0, 3), 0, Point::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)), +// ], +// |_| Hsla::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)), +// ], +// |_| Hsla::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), +// Hsla::green(), +// ), +// ( +// DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6), +// Hsla::green(), +// ), +// ( +// DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4), +// Hsla::red(), +// ), +// ( +// DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), +// Hsla::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), +// Hsla::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(Hsla::red()), +// ..Default::default() +// }, +// ), +// ( +// 4..8, +// HighlightStyle { +// color: Some(Hsla::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(Hsla::red()), +// ..Default::default() +// }, +// ), +// ( +// 4..5, +// HighlightStyle { +// color: Some(Hsla::green()), +// weight: Some(fonts::Weight::BOLD), +// ..Default::default() +// }, +// ), +// ( +// 5..6, +// HighlightStyle { +// color: Some(Hsla::green()), +// ..Default::default() +// }, +// ), +// ( +// 6..8, +// HighlightStyle { +// color: Some(Hsla::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/git.rs b/crates/editor2/src/git.rs index f8c6ef9a1fe8fe2a32a15b02de35e62b0b3b8964..e04372f0a7bfe79a97ba196ac853aad7b37c8119 100644 --- a/crates/editor2/src/git.rs +++ b/crates/editor2/src/git.rs @@ -88,195 +88,195 @@ pub fn diff_hunk_to_display(hunk: DiffHunk, snapshot: &DisplaySnapshot) -> } } -#[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, |_| {}); +// #[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; +// 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 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, - ); - }); +// // 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(); +// 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 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)); +// 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() - ); +// 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), - ]; +// 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(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(), - ); - } -} +// 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 index a0baf6882fba4952488701ad8b36a25e5566332a..e0fc2c0d00a3d15ce3d88c0ac49482aebe19e1da 100644 --- a/crates/editor2/src/highlight_matching_bracket.rs +++ b/crates/editor2/src/highlight_matching_bracket.rs @@ -30,109 +30,109 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon } } -#[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}; +// #[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, |_| {}); +// #[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; +// 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); - } - "#}); +// // 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«)»; +// } +// "#}); - 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); - } - "#}); +// // 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); - } - "#}); - } -} +// // 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 index 89946638b1a8894a9f582f487b3232bb7e731354..784b912c8c4e045443a53456c1e3c32204f69781 100644 --- a/crates/editor2/src/hover_popover.rs +++ b/crates/editor2/src/hover_popover.rs @@ -6,10 +6,7 @@ use crate::{ }; use futures::FutureExt; use gpui::{ - actions, - elements::{Flex, MouseEventHandler, Padding, ParentElement, Text}, - platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, Element, Model, Task, ViewContext, WeakViewHandle, + AnyElement, AppContext, CursorStyle, Element, Model, MouseButton, Task, ViewContext, WeakView, }; use language::{ markdown, Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, ParsedMarkdown, @@ -26,22 +23,23 @@ 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]); +// actions!(editor, [Hover]); pub fn init(cx: &mut AppContext) { - cx.add_action(hover); + // 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); -} +// todo!() +// /// 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 EditorSettings::get_global(cx).hover_popover_enabled { if let Some(point) = point { show_hover(editor, point, false, cx); } else { @@ -79,7 +77,7 @@ pub fn find_hovered_hint_part( } pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext) { - if settings::get::(cx).hover_popover_enabled { + if EditorSettings::get_global(cx).hover_popover_enabled { if editor.pending_rename.is_some() { return; } @@ -423,7 +421,7 @@ impl HoverState { snapshot: &EditorSnapshot, style: &EditorStyle, visible_rows: Range, - workspace: Option>, + workspace: Option>, cx: &mut ViewContext, ) -> Option<(DisplayPoint, Vec>)> { // If there is a diagnostic, position the popovers based on that. @@ -462,7 +460,7 @@ impl HoverState { #[derive(Debug, Clone)] pub struct InfoPopover { - pub project: ModelHandle, + pub project: Model, symbol_range: RangeInEditor, pub blocks: Vec, parsed_content: ParsedMarkdown, @@ -472,7 +470,7 @@ impl InfoPopover { pub fn render( &mut self, style: &EditorStyle, - workspace: Option>, + workspace: Option>, cx: &mut ViewContext, ) -> AnyElement { MouseEventHandler::new::(0, cx, |_, cx| { @@ -506,55 +504,56 @@ pub struct DiagnosticPopover { 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() + todo!() + // 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) { @@ -567,763 +566,763 @@ impl DiagnosticPopover { } } -#[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" - ); - }); - } -} +// #[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 index c5f090084ef1721a433ad64b3cb3711559046329..759323ecdf3b35b03466bec596af170908360d22 100644 --- a/crates/editor2/src/inlay_hint_cache.rs +++ b/crates/editor2/src/inlay_hint_cache.rs @@ -250,7 +250,7 @@ impl InlayHintCache { pub fn update_settings( &mut self, - multi_buffer: &ModelHandle, + multi_buffer: &Model, new_hint_settings: InlayHintSettings, visible_hints: Vec, cx: &mut ViewContext, @@ -302,7 +302,7 @@ impl InlayHintCache { pub fn spawn_hint_refresh( &mut self, reason: &'static str, - excerpts_to_query: HashMap, Global, Range)>, + excerpts_to_query: HashMap, Global, Range)>, invalidate: InvalidationStrategy, cx: &mut ViewContext, ) -> Option { @@ -355,7 +355,7 @@ impl InlayHintCache { fn new_allowed_hint_kinds_splice( &self, - multi_buffer: &ModelHandle, + multi_buffer: &Model, visible_hints: &[Inlay], new_kinds: &HashSet>, cx: &mut ViewContext, @@ -579,7 +579,7 @@ impl InlayHintCache { fn spawn_new_update_tasks( editor: &mut Editor, reason: &'static str, - excerpts_to_query: HashMap, Global, Range)>, + excerpts_to_query: HashMap, Global, Range)>, invalidate: InvalidationStrategy, update_cache_version: usize, cx: &mut ViewContext<'_, '_, Editor>, @@ -684,7 +684,7 @@ impl QueryRanges { fn determine_query_ranges( multi_buffer: &mut MultiBuffer, excerpt_id: ExcerptId, - excerpt_buffer: &ModelHandle, + excerpt_buffer: &Model, excerpt_visible_range: Range, cx: &mut ModelContext<'_, MultiBuffer>, ) -> Option { @@ -837,7 +837,7 @@ fn new_update_task( } async fn fetch_and_update_hints( - editor: gpui::WeakViewHandle, + editor: gpui::WeakView, multi_buffer_snapshot: MultiBufferSnapshot, buffer_snapshot: BufferSnapshot, visible_hints: Arc>, @@ -1194,2156 +1194,2156 @@ fn apply_hint_update( } } -#[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 - } -} +// #[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, View}; +// 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 index 2d249f03740a77235182806ec916fbe609559334..d5c479ef6b99abb6fa81f8aaa2da1eea55b2ec09 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -20,23 +20,18 @@ 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 util::{paths::PathExt, 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, + ItemId, ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, }; pub const MAX_TAB_TITLE_LEN: usize = 24; @@ -607,7 +602,7 @@ impl Item for Editor { where Self: Sized, { - Some(self.clone(cx)) + Some(self.clone()) } fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { diff --git a/crates/editor2/src/link_go_to_definition.rs b/crates/editor2/src/link_go_to_definition.rs index 7da0b88622ff4152127b46714c62394210b4f4d0..8ac27b480f77d74cb9202f45c87d8da1fa89c905 100644 --- a/crates/editor2/src/link_go_to_definition.rs +++ b/crates/editor2/src/link_go_to_definition.rs @@ -483,39 +483,40 @@ pub fn show_link_definition( }); 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, - ), - } + // todo!() + // // 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); } @@ -599,671 +600,671 @@ fn go_to_fetched_definition_of_kind( } } -#[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; - } - "}); - } -} +// #[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 index 8dfdcdff53b77b8bb1dcb41c71104b9901406b94..84c670c79d1b0297de88ccda35cd86660f8c77df 100644 --- a/crates/editor2/src/mouse_context_menu.rs +++ b/crates/editor2/src/mouse_context_menu.rs @@ -1,13 +1,10 @@ -use crate::{ - DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition, - Rename, RevealInFinder, SelectMode, ToggleCodeActions, -}; +use crate::{DisplayPoint, Editor, EditorMode, SelectMode}; use context_menu::ContextMenuItem; -use gpui::{elements::AnchorCorner, geometry::vector::Vector2F, ViewContext}; +use gpui::{Pixels, Point, ViewContext}; pub fn deploy_context_menu( editor: &mut Editor, - position: Vector2F, + position: Point, point: DisplayPoint, cx: &mut ViewContext, ) { @@ -31,66 +28,67 @@ pub fn deploy_context_menu( 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, - ); - }); + // todo!() + // 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; +// #[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, |_| {}); +// #[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; +// 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.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())); - } -} +// 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 index 05b9b039c474c77bd831db7e76d728c96878041f..593f7a4831029af73d17182adad2f710e4c9220c 100644 --- a/crates/editor2/src/movement.rs +++ b/crates/editor2/src/movement.rs @@ -1,6 +1,6 @@ use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint}; -use gpui::{FontCache, TextLayoutCache}; +use gpui::TextSystem; use language::Point; use std::{ops::Range, sync::Arc}; @@ -13,8 +13,7 @@ pub enum FindRange { /// 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 text_system: TextSystem, pub editor_style: EditorStyle, } diff --git a/crates/editor2/src/scroll.rs b/crates/editor2/src/scroll.rs index 8233f92a1a24e526855f1d8108995d8893ccc642..9a6af2961aa6ac1cca38bcdc08f530dfa0ecc6bb 100644 --- a/crates/editor2/src/scroll.rs +++ b/crates/editor2/src/scroll.rs @@ -2,19 +2,6 @@ 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, @@ -22,6 +9,14 @@ use crate::{ Anchor, DisplayPoint, Editor, EditorMode, Event, InlayHintRefreshReason, MultiBufferSnapshot, ToPoint, }; +use gpui::{point, AppContext, Pixels, Task, ViewContext}; +use language::{Bias, Point}; +use std::{ + cmp::Ordering, + time::{Duration, Instant}, +}; +use util::ResultExt; +use workspace::WorkspaceId; use self::{ autoscroll::{Autoscroll, AutoscrollStrategy}, @@ -37,19 +32,19 @@ pub struct ScrollbarAutoHide(pub bool); #[derive(Clone, Copy, Debug, PartialEq)] pub struct ScrollAnchor { - pub offset: Vector2F, + pub offset: gpui::Point, pub anchor: Anchor, } impl ScrollAnchor { fn new() -> Self { Self { - offset: Vector2F::zero(), + offset: Point::zero(), anchor: Anchor::min(), } } - pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F { + pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Point { let mut scroll_position = self.offset; if self.anchor != Anchor::min() { let scroll_top = self.anchor.to_display_point(snapshot).row() as f32; @@ -65,6 +60,12 @@ impl ScrollAnchor { } } +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum Axis { + Vertical, + Horizontal, +} + #[derive(Clone, Copy, Debug)] pub struct OngoingScroll { last_event: Instant, @@ -79,7 +80,7 @@ impl OngoingScroll { } } - pub fn filter(&self, delta: &mut Vector2F) -> Option { + pub fn filter(&self, delta: &mut Point) -> Option { const UNLOCK_PERCENT: f32 = 1.9; const UNLOCK_LOWER_BOUND: f32 = 6.; let mut axis = self.axis; @@ -114,8 +115,8 @@ impl OngoingScroll { } match axis { - Some(Axis::Vertical) => *delta = vec2f(0., delta.y()), - Some(Axis::Horizontal) => *delta = vec2f(delta.x(), 0.), + Some(Axis::Vertical) => *delta = point(0., delta.y()), + Some(Axis::Horizontal) => *delta = point(delta.x(), 0.), None => {} } @@ -128,7 +129,7 @@ pub struct ScrollManager { anchor: ScrollAnchor, ongoing: OngoingScroll, autoscroll_request: Option<(Autoscroll, bool)>, - last_autoscroll: Option<(Vector2F, f32, f32, AutoscrollStrategy)>, + last_autoscroll: Option<(gpui::Point, f32, f32, AutoscrollStrategy)>, show_scrollbars: bool, hide_scrollbar_task: Option>, visible_line_count: Option, @@ -166,13 +167,13 @@ impl ScrollManager { self.ongoing.axis = axis; } - pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F { + pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Point { self.anchor.scroll_position(snapshot) } fn set_scroll_position( &mut self, - scroll_position: Vector2F, + scroll_position: Point, map: &DisplaySnapshot, local: bool, autoscroll: bool, @@ -183,7 +184,7 @@ impl ScrollManager { ( ScrollAnchor { anchor: Anchor::min(), - offset: scroll_position.max(vec2f(0., 0.)), + offset: scroll_position.max(Point::zero()), }, 0, ) @@ -197,7 +198,7 @@ impl ScrollManager { ( ScrollAnchor { anchor: top_anchor, - offset: vec2f( + offset: point( scroll_position.x(), scroll_position.y() - top_anchor.to_display_point(&map).row() as f32, ), @@ -310,13 +311,17 @@ impl Editor { } } - pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext) { + pub fn set_scroll_position( + &mut self, + scroll_position: Point, + 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, + scroll_position: Point, local: bool, autoscroll: bool, cx: &mut ViewContext, @@ -337,7 +342,7 @@ impl Editor { self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); } - pub fn scroll_position(&self, cx: &mut ViewContext) -> Vector2F { + pub fn scroll_position(&self, cx: &mut ViewContext) -> Point { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); self.scroll_manager.anchor.scroll_position(&display_map) } @@ -379,7 +384,7 @@ impl Editor { } let cur_position = self.scroll_position(cx); - let new_pos = cur_position + vec2f(0., amount.lines(self)); + let new_pos = cur_position + point(0., amount.lines(self)); self.set_scroll_position(new_pos, cx); } @@ -427,7 +432,7 @@ impl Editor { .snapshot(cx) .anchor_at(Point::new(top_row as u32, 0), Bias::Left); let scroll_anchor = ScrollAnchor { - offset: Vector2F::new(x, y), + offset: Point::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 index e943bed7671bf9147776eb096ae3ae867f949ff2..ba39d3849bab612489d40c53a4dca0a346eefb29 100644 --- a/crates/editor2/src/scroll/actions.rs +++ b/crates/editor2/src/scroll/actions.rs @@ -17,7 +17,7 @@ use gpui::AppContext; // ); pub fn init(cx: &mut AppContext) { - /// todo!() + // todo!() // cx.add_action(Editor::next_screen); // cx.add_action(Editor::scroll_cursor_top); // cx.add_action(Editor::scroll_cursor_center); diff --git a/crates/editor2/src/selections_collection.rs b/crates/editor2/src/selections_collection.rs index 151a27c90198a656f4bb806bf5eab167f1dfd3c2..e1ef69ac8aa41a461853469b42de6aa6cbafbc42 100644 --- a/crates/editor2/src/selections_collection.rs +++ b/crates/editor2/src/selections_collection.rs @@ -25,8 +25,8 @@ pub struct PendingSelection { #[derive(Debug, Clone)] pub struct SelectionsCollection { - display_map: ModelHandle, - buffer: ModelHandle, + display_map: Model, + buffer: Model, pub next_selection_id: usize, pub line_mode: bool, disjoint: Arc<[Selection]>, @@ -34,7 +34,7 @@ pub struct SelectionsCollection { } impl SelectionsCollection { - pub fn new(display_map: ModelHandle, buffer: ModelHandle) -> Self { + pub fn new(display_map: Model, buffer: Model) -> Self { Self { display_map, buffer, diff --git a/crates/editor2/src/test.rs b/crates/editor2/src/test.rs index 45a61e58de6abab97a1969a581cec4546e5bc079..8087569925309c1da68b78088f63f597f52c8288 100644 --- a/crates/editor2/src/test.rs +++ b/crates/editor2/src/test.rs @@ -67,16 +67,13 @@ pub fn assert_text_with_selections( // 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 { +pub(crate) fn build_editor(buffer: Model, cx: &mut ViewContext) -> Editor { Editor::new(EditorMode::Full, buffer, None, None, cx) } pub(crate) fn build_editor_with_project( - project: ModelHandle, - buffer: ModelHandle, + project: Model, + buffer: Model, 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 index 3e2f38a0b665e964eb51583c13d4cd4a87422b3d..6d1662857d641a3f419a7a5aa4316a0cdd443dd4 100644 --- a/crates/editor2/src/test/editor_lsp_test_context.rs +++ b/crates/editor2/src/test/editor_lsp_test_context.rs @@ -9,7 +9,7 @@ use anyhow::Result; use crate::{Editor, ToPoint}; use collections::HashSet; use futures::Future; -use gpui::{json, ViewContext, ViewHandle}; +use gpui::{json, View, ViewContext}; use indoc::indoc; use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries}; use lsp::{notification, request}; diff --git a/crates/editor2/src/test/editor_test_context.rs b/crates/editor2/src/test/editor_test_context.rs index c083ce0e682e3164ec29b7bb9087c0f3a2ac32c0..c856afeefe3a71256ee9655487fd36ec6b72b602 100644 --- a/crates/editor2/src/test/editor_test_context.rs +++ b/crates/editor2/src/test/editor_test_context.rs @@ -3,8 +3,7 @@ use crate::{ }; use futures::Future; use gpui::{ - executor::Foreground, keymap_matcher::Keystroke, AnyWindowHandle, AppContext, ContextHandle, - ModelContext, ViewContext, ViewHandle, + AnyWindowHandle, AppContext, ForegroundExecutor, Keystroke, ModelContext, View, ViewContext, }; use indoc::indoc; use language::{Buffer, BufferSnapshot}; @@ -23,7 +22,7 @@ use super::build_editor_with_project; pub struct EditorTestContext<'a> { pub cx: &'a mut gpui::TestAppContext, pub window: AnyWindowHandle, - pub editor: ViewHandle, + pub editor: View, } impl<'a> EditorTestContext<'a> { @@ -119,37 +118,37 @@ impl<'a> EditorTestContext<'a> { 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 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); @@ -177,144 +176,144 @@ impl<'a> EditorTestContext<'a> { 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, - ); - } - } + // /// 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> { diff --git a/crates/gpui2/src/text_system.rs b/crates/gpui2/src/text_system.rs index 7c96b1a180c70fb2039995e969890ff5878b3733..ee8c65386691434a6d6fc1728d98cc97c9f01a49 100644 --- a/crates/gpui2/src/text_system.rs +++ b/crates/gpui2/src/text_system.rs @@ -7,7 +7,7 @@ use anyhow::anyhow; pub use font_features::*; pub use line::*; pub use line_layout::*; -use line_wrapper::*; +pub use line_wrapper::*; use smallvec::SmallVec; use crate::{ diff --git a/crates/language2/Cargo.toml b/crates/language2/Cargo.toml index 4fca16bcb595c3e90e6e19b04b6cc2916d35aa0d..bd43465b55ca14e305b044da0512dcf0580f0872 100644 --- a/crates/language2/Cargo.toml +++ b/crates/language2/Cargo.toml @@ -58,6 +58,7 @@ unicase = "2.6" rand = { workspace = true, optional = true } tree-sitter-rust = { workspace = true, optional = true } tree-sitter-typescript = { workspace = true, optional = true } +pulldown-cmark = { version = "0.9.2", default-features = false } [dev-dependencies] client = { package = "client2", path = "../client2", features = ["test-support"] } diff --git a/crates/language2/src/language2.rs b/crates/language2/src/language2.rs index 381284659bffd624cc1c4d04129844b743ce6128..826817c99a54f2034ca7867565d80c36eae2edc5 100644 --- a/crates/language2/src/language2.rs +++ b/crates/language2/src/language2.rs @@ -8,6 +8,7 @@ mod syntax_map; #[cfg(test)] mod buffer_tests; +pub mod markdown; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; diff --git a/crates/language2/src/markdown.rs b/crates/language2/src/markdown.rs new file mode 100644 index 0000000000000000000000000000000000000000..df75b610ef844ec858d74ae7a3a8bafa5a59edbe --- /dev/null +++ b/crates/language2/src/markdown.rs @@ -0,0 +1,301 @@ +use std::sync::Arc; +use std::{ops::Range, path::PathBuf}; + +use crate::{HighlightId, Language, LanguageRegistry}; +use gpui::{px, FontStyle, FontWeight, HighlightStyle, UnderlineStyle}; +use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; + +#[derive(Debug, Clone)] +pub struct ParsedMarkdown { + pub text: String, + pub highlights: Vec<(Range, MarkdownHighlight)>, + pub region_ranges: Vec>, + pub regions: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MarkdownHighlight { + Style(MarkdownHighlightStyle), + Code(HighlightId), +} + +impl MarkdownHighlight { + pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option { + match self { + MarkdownHighlight::Style(style) => { + let mut highlight = HighlightStyle::default(); + + if style.italic { + highlight.font_style = Some(FontStyle::Italic); + } + + if style.underline { + highlight.underline = Some(UnderlineStyle { + thickness: px(1.), + ..Default::default() + }); + } + + if style.weight != FontWeight::default() { + highlight.font_weight = Some(style.weight); + } + + Some(highlight) + } + + MarkdownHighlight::Code(id) => id.style(theme), + } + } +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct MarkdownHighlightStyle { + pub italic: bool, + pub underline: bool, + pub weight: FontWeight, +} + +#[derive(Debug, Clone)] +pub struct ParsedRegion { + pub code: bool, + pub link: Option, +} + +#[derive(Debug, Clone)] +pub enum Link { + Web { url: String }, + Path { path: PathBuf }, +} + +impl Link { + fn identify(text: String) -> Option { + if text.starts_with("http") { + return Some(Link::Web { url: text }); + } + + let path = PathBuf::from(text); + if path.is_absolute() { + return Some(Link::Path { path }); + } + + None + } +} + +pub async fn parse_markdown( + markdown: &str, + language_registry: &Arc, + language: Option>, +) -> ParsedMarkdown { + let mut text = String::new(); + let mut highlights = Vec::new(); + let mut region_ranges = Vec::new(); + let mut regions = Vec::new(); + + parse_markdown_block( + markdown, + language_registry, + language, + &mut text, + &mut highlights, + &mut region_ranges, + &mut regions, + ) + .await; + + ParsedMarkdown { + text, + highlights, + region_ranges, + regions, + } +} + +pub async fn parse_markdown_block( + markdown: &str, + language_registry: &Arc, + language: Option>, + text: &mut String, + highlights: &mut Vec<(Range, MarkdownHighlight)>, + region_ranges: &mut Vec>, + regions: &mut Vec, +) { + let mut bold_depth = 0; + let mut italic_depth = 0; + let mut link_url = None; + let mut current_language = None; + let mut list_stack = Vec::new(); + + for event in Parser::new_ext(&markdown, Options::all()) { + let prev_len = text.len(); + match event { + Event::Text(t) => { + if let Some(language) = ¤t_language { + highlight_code(text, highlights, t.as_ref(), language); + } else { + text.push_str(t.as_ref()); + + let mut style = MarkdownHighlightStyle::default(); + + if bold_depth > 0 { + style.weight = FontWeight::BOLD; + } + + if italic_depth > 0 { + style.italic = true; + } + + if let Some(link) = link_url.clone().and_then(|u| Link::identify(u)) { + region_ranges.push(prev_len..text.len()); + regions.push(ParsedRegion { + code: false, + link: Some(link), + }); + style.underline = true; + } + + if style != MarkdownHighlightStyle::default() { + let mut new_highlight = true; + if let Some((last_range, MarkdownHighlight::Style(last_style))) = + highlights.last_mut() + { + if last_range.end == prev_len && last_style == &style { + last_range.end = text.len(); + new_highlight = false; + } + } + if new_highlight { + let range = prev_len..text.len(); + highlights.push((range, MarkdownHighlight::Style(style))); + } + } + } + } + + Event::Code(t) => { + text.push_str(t.as_ref()); + region_ranges.push(prev_len..text.len()); + + let link = link_url.clone().and_then(|u| Link::identify(u)); + if link.is_some() { + highlights.push(( + prev_len..text.len(), + MarkdownHighlight::Style(MarkdownHighlightStyle { + underline: true, + ..Default::default() + }), + )); + } + regions.push(ParsedRegion { code: true, link }); + } + + Event::Start(tag) => match tag { + Tag::Paragraph => new_paragraph(text, &mut list_stack), + + Tag::Heading(_, _, _) => { + new_paragraph(text, &mut list_stack); + bold_depth += 1; + } + + Tag::CodeBlock(kind) => { + new_paragraph(text, &mut list_stack); + current_language = if let CodeBlockKind::Fenced(language) = kind { + language_registry + .language_for_name(language.as_ref()) + .await + .ok() + } else { + language.clone() + } + } + + Tag::Emphasis => italic_depth += 1, + + Tag::Strong => bold_depth += 1, + + Tag::Link(_, url, _) => link_url = Some(url.to_string()), + + Tag::List(number) => { + list_stack.push((number, false)); + } + + Tag::Item => { + let len = list_stack.len(); + if let Some((list_number, has_content)) = list_stack.last_mut() { + *has_content = false; + if !text.is_empty() && !text.ends_with('\n') { + text.push('\n'); + } + for _ in 0..len - 1 { + text.push_str(" "); + } + if let Some(number) = list_number { + text.push_str(&format!("{}. ", number)); + *number += 1; + *has_content = false; + } else { + text.push_str("- "); + } + } + } + + _ => {} + }, + + Event::End(tag) => match tag { + Tag::Heading(_, _, _) => bold_depth -= 1, + Tag::CodeBlock(_) => current_language = None, + Tag::Emphasis => italic_depth -= 1, + Tag::Strong => bold_depth -= 1, + Tag::Link(_, _, _) => link_url = None, + Tag::List(_) => drop(list_stack.pop()), + _ => {} + }, + + Event::HardBreak => text.push('\n'), + + Event::SoftBreak => text.push(' '), + + _ => {} + } + } +} + +pub fn highlight_code( + text: &mut String, + highlights: &mut Vec<(Range, MarkdownHighlight)>, + content: &str, + language: &Arc, +) { + let prev_len = text.len(); + text.push_str(content); + for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) { + let highlight = MarkdownHighlight::Code(highlight_id); + highlights.push((prev_len + range.start..prev_len + range.end, highlight)); + } +} + +pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { + let mut is_subsequent_paragraph_of_list = false; + if let Some((_, has_content)) = list_stack.last_mut() { + if *has_content { + is_subsequent_paragraph_of_list = true; + } else { + *has_content = true; + return; + } + } + + if !text.is_empty() { + if !text.ends_with('\n') { + text.push('\n'); + } + text.push('\n'); + } + for _ in 0..list_stack.len().saturating_sub(1) { + text.push_str(" "); + } + if is_subsequent_paragraph_of_list { + text.push_str(" "); + } +} From 0aabb19a451eaf04d766dc21a7b2efc3fa03f1df Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 2 Nov 2023 18:52:08 -0600 Subject: [PATCH 05/23] Into the woods --- crates/editor2/Cargo.toml | 4 +- crates/editor2/src/blink_manager.rs | 27 +- crates/editor2/src/display_map.rs | 115 +- crates/editor2/src/display_map/block_map.rs | 6 +- crates/editor2/src/display_map/wrap_map.rs | 12 +- crates/editor2/src/editor.rs | 3018 ++++++++--------- crates/editor2/src/editor_settings.rs | 4 +- crates/editor2/src/element.rs | 32 +- crates/editor2/src/hover_popover.rs | 58 +- crates/editor2/src/inlay_hint_cache.rs | 264 +- crates/editor2/src/items.rs | 260 +- crates/editor2/src/link_go_to_definition.rs | 2 +- crates/editor2/src/scroll.rs | 337 +- crates/editor2/src/selections_collection.rs | 47 +- crates/editor2/src/test.rs | 133 +- .../editor2/src/test/editor_test_context.rs | 596 ++-- 16 files changed, 2460 insertions(+), 2455 deletions(-) diff --git a/crates/editor2/Cargo.toml b/crates/editor2/Cargo.toml index 3f94d5cdc024f82d542bf412f2757d2407d462fb..f0002787f3d7806c0c46865160a9e3c6860acde8 100644 --- a/crates/editor2/Cargo.toml +++ b/crates/editor2/Cargo.toml @@ -35,7 +35,7 @@ git = { path = "../git" } gpui = { package = "gpui2", path = "../gpui2" } language = { package = "language2", path = "../language2" } lsp = { package = "lsp2", path = "../lsp2" } -multi_buffer = { path = "../multi_buffer" } +multi_buffer = { package = "multi_buffer2", path = "../multi_buffer2" } project = { package = "project2", path = "../project2" } rpc = { package = "rpc2", path = "../rpc2" } rich_text = { path = "../rich_text" } @@ -80,7 +80,7 @@ util = { path = "../util", features = ["test-support"] } project = { package = "project2", path = "../project2", features = ["test-support"] } settings = { package = "settings2", path = "../settings2", features = ["test-support"] } workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } -multi_buffer = { path = "../multi_buffer", features = ["test-support"] } +multi_buffer = { package = "multi_buffer2", path = "../multi_buffer2", features = ["test-support"] } ctor.workspace = true env_logger.workspace = true diff --git a/crates/editor2/src/blink_manager.rs b/crates/editor2/src/blink_manager.rs index 7d242b6684d58bb7df791b6ccaf876510c1db9cd..d25e30f649659c6b379acb71c4e9b274bfbfdc31 100644 --- a/crates/editor2/src/blink_manager.rs +++ b/crates/editor2/src/blink_manager.rs @@ -1,5 +1,6 @@ use crate::EditorSettings; -use gpui::{Entity, ModelContext}; +use gpui::ModelContext; +use settings::Settings; use settings::SettingsStore; use smol::Timer; use std::time::Duration; @@ -16,7 +17,7 @@ pub struct BlinkManager { 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| { + cx.observe_global::(move |this, cx| { this.blink_cursors(this.blink_epoch, cx) }) .detach(); @@ -41,14 +42,9 @@ impl BlinkManager { 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)) - } - } + cx.spawn(|this, mut cx| async move { + Timer::after(interval).await; + this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx)) }) .detach(); } @@ -68,13 +64,10 @@ impl BlinkManager { 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)); - } + cx.spawn(|this, mut cx| async move { + Timer::after(interval).await; + if let Some(this) = this.upgrade() { + this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx)); } }) .detach(); diff --git a/crates/editor2/src/display_map.rs b/crates/editor2/src/display_map.rs index 5848a12531a435f37b26378afda8eb0d4430c605..ab137df74f3d8c32f853a11c41d0e4b2320a80db 100644 --- a/crates/editor2/src/display_map.rs +++ b/crates/editor2/src/display_map.rs @@ -497,62 +497,63 @@ impl DisplaySnapshot { ) } - 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(UnderlineStyle { - color: Some(diagnostic_style.message.text.color), - thickness: 1.0.into(), - wavy: 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 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 { + // todo!() + // // let diagnostic_style = super::diagnostic_style(severity, true, style); + // // diagnostic_highlight.underline = Some(UnderlineStyle { + // // color: Some(diagnostic_style.message.text.color), + // // thickness: 1.0.into(), + // // wavy: 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, @@ -606,7 +607,7 @@ impl DisplaySnapshot { // }); } - text_layout_cache.layout_text(&line, editor_style.text.font_size, &styles) + text_layout_cache.layout_text(&line, editor_style.text.font_size, &styles, None) } pub fn x_for_point( diff --git a/crates/editor2/src/display_map/block_map.rs b/crates/editor2/src/display_map/block_map.rs index e6830a9039cb8836d1a64ee72b584ca109f4758a..aa5ff0e3d22d873156ef1088dc3d31d74ef9f130 100644 --- a/crates/editor2/src/display_map/block_map.rs +++ b/crates/editor2/src/display_map/block_map.rs @@ -932,15 +932,15 @@ impl BlockDisposition { } } -impl<'a, 'b, 'c> Deref for BlockContext<'a, 'b, 'c> { - type Target = ViewContext<'a, 'b, Editor>; +impl<'a> Deref for BlockContext<'a, '_> { + type Target = ViewContext<'a, Editor>; fn deref(&self) -> &Self::Target { self.view_context } } -impl DerefMut for BlockContext<'_, '_, '_> { +impl DerefMut for BlockContext<'_, '_> { fn deref_mut(&mut self) -> &mut Self::Target { self.view_context } diff --git a/crates/editor2/src/display_map/wrap_map.rs b/crates/editor2/src/display_map/wrap_map.rs index c2f181efcdaf6fca4e68e28ad8c7065fc3e3fbf2..d6f321f6161402bf90ae8912f291e5370d9cb45f 100644 --- a/crates/editor2/src/display_map/wrap_map.rs +++ b/crates/editor2/src/display_map/wrap_map.rs @@ -4,7 +4,7 @@ use super::{ Highlights, }; use crate::MultiBufferSnapshot; -use gpui::{AppContext, FontId, Model, ModelContext, Pixels, Task}; +use gpui::{AppContext, FontId, LineWrapper, Model, ModelContext, Pixels, Task}; use language::{Chunk, Point}; use lazy_static::lazy_static; use smol::future::yield_now; @@ -20,7 +20,7 @@ pub struct WrapMap { pending_edits: VecDeque<(TabSnapshot, Vec)>, interpolated_edits: Patch, edits_since_sync: Patch, - wrap_width: Option, + wrap_width: Option, background_task: Option>, font: (FontId, Pixels), } @@ -130,7 +130,11 @@ impl WrapMap { } } - pub fn set_wrap_width(&mut self, wrap_width: Option, cx: &mut ModelContext) -> bool { + pub fn set_wrap_width( + &mut self, + wrap_width: Option, + cx: &mut ModelContext, + ) -> bool { if wrap_width == self.wrap_width { return false; } @@ -379,7 +383,7 @@ impl WrapSnapshot { &mut self, new_tab_snapshot: TabSnapshot, tab_edits: &[TabEdit], - wrap_width: f32, + wrap_width: Pixels, line_wrapper: &mut LineWrapper, ) -> Patch { #[derive(Debug)] diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index a7378aa705a8b4378fbbe1e069243fdf1ab2828e..0ae03661e862809d4f534fac902605d91edee3ed 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -19,7 +19,6 @@ pub mod selections_collection; 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}; @@ -38,8 +37,9 @@ pub use element::{ use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - serde_json, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, Hsla, - Model, Quad, Subscription, Task, Text, View, ViewContext, WeakView, WindowContext, + serde_json, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, + EventEmitter, FontWeight, HighlightStyle, Hsla, Model, Pixels, Quad, Render, Subscription, + Task, Text, View, ViewContext, WeakView, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -74,30 +74,17 @@ use rpc::proto::{self, PeerId}; use scroll::{ autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide, }; -use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection}; +use selections_collection::SelectionsCollection; use serde::{Deserialize, Serialize}; -use settings::SettingsStore; -use smallvec::SmallVec; -use snippet::Snippet; +use settings::Settings; use std::{ - any::TypeId, - borrow::Cow, - cmp::{self, Ordering, Reverse}, - mem, - num::NonZeroU32, - ops::{ControlFlow, Deref, DerefMut, Range, RangeInclusive}, - path::Path, + ops::{Deref, DerefMut, Range}, sync::Arc, - time::{Duration, Instant}, + time::Duration, }; 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; +use util::TryFutureExt; +use workspace::{ItemNavHistory, ViewId, Workspace}; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); const MAX_LINE_LEN: usize = 1024; @@ -109,70 +96,70 @@ pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis 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; - - todo!() - // 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.draw_quad(Quad { - // bounds, - // background: Some(code_span_background_color), - // corner_radii: (2.0).into(), - // order: todo!(), - // content_mask: todo!(), - // border_color: todo!(), - // border_widths: todo!(), - // }); - // } - // }) - // .with_soft_wrap(true) -} +// 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; + +// todo!() +// // 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.draw_quad(Quad { +// // bounds, +// // background: Some(code_span_background_color), +// // corner_radii: (2.0).into(), +// // order: todo!(), +// // content_mask: todo!(), +// // border_color: todo!(), +// // border_widths: todo!(), +// // }); +// // } +// // }) +// // .with_soft_wrap(true) +// } #[derive(Clone, Deserialize, PartialEq, Default)] pub struct SelectNext { @@ -270,138 +257,138 @@ impl InlayId { } } -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 - ] -); +// 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 {} @@ -414,7 +401,7 @@ pub enum Direction { } pub fn init_settings(cx: &mut AppContext) { - settings::register::(cx); + EditorSettings::register(cx); } pub fn init(cx: &mut AppContext) { @@ -574,7 +561,7 @@ pub enum SelectPhase { Update { position: DisplayPoint, goal_column: u32, - scroll_position: Point, + scroll_position: gpui::Point, }, End, } @@ -603,20 +590,20 @@ pub enum SoftWrap { #[derive(Clone)] pub struct EditorStyle { - pub text: TextStyle, + // pub text: TextStyle, pub line_height_scalar: f32, - pub placeholder_text: Option, - pub theme: theme::Editor, + // 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 GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor; +// type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option; -type BackgroundHighlight = (fn(&Theme) -> Hsla, Vec>); -type InlayBackgroundHighlight = (fn(&Theme) -> Hsla, Vec); +// type BackgroundHighlight = (fn(&Theme) -> Hsla, Vec>); +// type InlayBackgroundHighlight = (fn(&Theme) -> Hsla, Vec); pub struct Editor { handle: WeakView, @@ -635,8 +622,8 @@ pub struct Editor { ime_transaction: Option, active_diagnostics: Option, soft_wrap_mode_override: Option, - get_field_editor_theme: Option>, - override_text_style: Option>, + // get_field_editor_theme: Option>, + // override_text_style: Option>, project: Option>, collaboration_hub: Option>, focused: bool, @@ -647,8 +634,8 @@ pub struct Editor { show_wrap_guides: Option, placeholder_text: Option>, highlighted_rows: Option>, - background_highlights: BTreeMap, - inlay_background_highlights: TreeMap, InlayBackgroundHighlight>, + // background_highlights: BTreeMap, + // inlay_background_highlights: TreeMap, InlayBackgroundHighlight>, nav_history: Option, context_menu: RwLock>, mouse_context_menu: View, @@ -663,7 +650,7 @@ pub struct Editor { collapse_matches: bool, autoindent_mode: Option, workspace: Option<(WeakView, i64)>, - keymap_context_layers: BTreeMap, + // keymap_context_layers: BTreeMap, input_enabled: bool, read_only: bool, leader_peer_id: Option, @@ -675,7 +662,7 @@ pub struct Editor { // inlay_hint_cache: InlayHintCache, next_inlay_id: usize, _subscriptions: Vec, - pixel_position_of_newest_cursor: Option>, + pixel_position_of_newest_cursor: Option>, } pub struct EditorSnapshot { @@ -937,763 +924,763 @@ struct CompletionsMenu { match_candidates: Arc<[StringMatchCandidate]>, matches: Arc<[StringMatch]>, selected_item: usize, - list: UniformListState, + // list: UniformListState, } -impl CompletionsMenu { - fn select_first(&mut self, project: Option<&Model>, 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(); - } +// impl CompletionsMenu { +// fn select_first(&mut self, project: Option<&Model>, 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<&Model>, 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_prev(&mut self, project: Option<&Model>, 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<&Model>, 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_next(&mut self, project: Option<&Model>, 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<&Model>, 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 select_last(&mut self, project: Option<&Model>, 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; - } +// 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 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 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(); +// 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; - }; +// 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(); - } +// for completion_index in completion_indices { +// let completions_guard = completions.read(); +// let completion = &completions_guard[completion_index]; +// if completion.documentation.is_some() { +// continue; +// } - fn attempt_resolve_selected_completion_documentation( - &mut self, - project: Option<&Model>, - cx: &mut ViewContext, - ) { - let settings = settings::get::(cx); - if !settings.show_completion_documentation { - return; - } +// 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; - 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; - } +// _ = 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_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(); - } - } +// let server = project.read_with(&mut cx, |project, _| { +// project.language_server_for_id(server_id) +// }); +// let Some(server) = server else { +// return; +// }; - 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); - } +// Self::resolve_completion_documentation_local( +// server, +// completions.clone(), +// completion_index, +// completion, +// language_registry.clone(), +// ) +// .await; - 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) - }; +// _ = this.update(&mut cx, |_, cx| cx.notify()); +// } +// } +// }) +// .detach(); +// } - let mut completions = completions.write(); - let completion = &mut completions[completion_index]; - completion.documentation = Some(documentation); - } +// fn attempt_resolve_selected_completion_documentation( +// &mut self, +// project: Option<&Model>, +// cx: &mut ViewContext, +// ) { +// let settings = settings::get::(cx); +// if !settings.show_completion_documentation { +// return; +// } - 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 completion_index = self.matches[self.selected_item].candidate_id; +// let Some(project) = project else { +// return; +// }; +// let language_registry = project.read(cx).languages().clone(); - 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); - } - } +// let completions = self.completions.clone(); +// let completions_guard = completions.read(); +// let completion = &completions_guard[completion_index]; +// if completion.documentation.is_some() { +// return; +// } - fn visible(&self) -> bool { - !self.matches.is_empty() - } +// let server_id = completion.server_id; +// let completion = completion.lsp_completion.clone(); +// drop(completions_guard); - fn render( - &self, - style: EditorStyle, - workspace: Option>, - cx: &mut ViewContext, - ) -> AnyElement { - enum CompletionTag {} +// 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 settings = settings::get::(cx); - let show_completion_documentation = settings.show_completion_documentation; +// 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; - 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(); - } - } +// _ = this.update(&mut cx, |_, cx| cx.notify()); +// }) +// .detach(); +// } else { +// let Some(server) = project.read(cx).language_server_for_id(server_id) else { +// return; +// }; - 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() - } +// cx.spawn(move |this, mut cx| async move { +// Self::resolve_completion_documentation_local( +// server, +// completions, +// completion_index, +// completion, +// language_registry, +// ) +// .await; - 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) - }) - }); - } - } +// _ = this.update(&mut cx, |_, cx| cx.notify()); +// }) +// .detach(); +// } +// } - 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; - } - } +// 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(), +// }; - self.matches = matches.into(); - self.selected_item = 0; - } -} +// let Some(response) = client +// .request(request) +// .await +// .context("completion documentation resolve proto request") +// .log_err() +// else { +// return; +// }; -#[derive(Clone)] -struct CodeActionsMenu { - actions: Arc<[CodeAction]>, - buffer: Model, - selected_item: usize, - list: UniformListState, - deployed_from_indicator: bool, -} +// if response.text.is_empty() { +// let mut completions = completions.write(); +// let completion = &mut completions[completion_index]; +// completion.documentation = Some(Documentation::Undocumented); +// } -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() - } +// 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) +// }; - 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(); - } +// let mut completions = completions.write(); +// let completion = &mut completions[completion_index]; +// completion.documentation = Some(documentation); +// } - 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(); - } +// 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; +// } - 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() - } +// let request = server.request::(completion); +// let Some(completion_item) = request.await.log_err() else { +// return; +// }; - fn visible(&self) -> bool { - !self.actions.is_empty() - } +// 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; - 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; - } +// 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); +// } +// } - (cursor_position, element) - } -} +// fn visible(&self) -> bool { +// !self.matches.is_empty() +// } -pub struct CopilotState { - excerpt_id: Option, - pending_refresh: Task>, - pending_cycling_refresh: Task>, - cycled: bool, - completions: Vec, - active_completion_index: usize, - suggestion: Option, -} +// fn render( +// &self, +// style: EditorStyle, +// workspace: Option>, +// cx: &mut ViewContext, +// ) -> AnyElement { +// enum CompletionTag {} -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, - } - } -} +// let settings = settings::get::(cx); +// let show_completion_documentation = settings.show_completion_documentation; -impl CopilotState { - fn active_completion(&self) -> Option<&copilot::Completion> { - self.completions.get(self.active_completion_index) - } +// 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(); +// } +// } - fn text_for_active_completion( - &self, - cursor: Anchor, - buffer: &MultiBufferSnapshot, - ) -> Option<&str> { - use language::ToOffset as _; +// len +// }) +// .map(|(ix, _)| ix); - 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 completions = self.completions.clone(); +// let matches = self.matches.clone(); +// let selected_item = self.selected_item; - 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); +// 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(); - 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 - } - } +// 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]; - 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 +// 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: Model, + 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 => { @@ -2040,9 +2027,9 @@ impl InlayHintRefreshReason { // self.leader_peer_id // } -// pub fn buffer(&self) -> &Model { -// &self.buffer -// } +pub fn buffer(&self) -> &Model { + &self.buffer +} // fn workspace(&self, cx: &AppContext) -> Option> { // self.workspace.as_ref()?.0.upgrade(cx) @@ -9224,7 +9211,7 @@ impl EditorSnapshot { self.placeholder_text.as_ref() } - pub fn scroll_position(&self) -> Point { + pub fn scroll_position(&self) -> gpui::Point { self.scroll_anchor.scroll_position(&self.display_snapshot) } } @@ -9277,425 +9264,438 @@ pub struct EditorFocused(pub View); pub struct EditorBlurred(pub View); pub struct EditorReleased(pub WeakView); -impl Entity for Editor { +// impl Entity for Editor { +// type Event = Event; + +// fn release(&mut self, cx: &mut AppContext) { +// cx.emit_global(EditorReleased(self.handle.clone())); +// } +// } +// +impl EventEmitter for Editor { type Event = Event; +} - fn release(&mut self, cx: &mut AppContext) { - cx.emit_global(EditorReleased(self.handle.clone())); +impl Render for Editor { + type Element = EditorElement; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + todo!() } } -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); - }); - } +// 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) +// }); - Stack::new() - .with_child(EditorElement::new(style.clone())) - .with_child(ChildView::new(&self.mouse_context_menu, cx)) - .into_any() - } +// if font_changed { +// cx.defer(move |editor, cx: &mut ViewContext| { +// hide_hover(editor, cx); +// hide_link_definition(editor, cx); +// }); +// } - fn ui_name() -> &'static str { - "Editor" - } +// Stack::new() +// .with_child(EditorElement::new(style.clone())) +// .with_child(ChildView::new(&self.mouse_context_menu, cx)) +// .into_any() +// } - fn focus_in(&mut self, focused: AnyView, 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 ui_name() -> &'static str { +// "Editor" +// } - fn focus_out(&mut self, _: AnyView, 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 focus_in(&mut self, focused: AnyView, 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 modifiers_changed( - &mut self, - event: &gpui::platform::ModifiersChangedEvent, - cx: &mut ViewContext, - ) -> bool { - let pending_selection = self.has_pending_selection(); +// fn focus_out(&mut self, _: AnyView, 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(); +// } - 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); +// fn modifiers_changed( +// &mut self, +// event: &gpui::platform::ModifiersChangedEvent, +// cx: &mut ViewContext, +// ) -> bool { +// let pending_selection = self.has_pending_selection(); - show_link_definition(kind, self, point, snapshot, cx); - return false; - } - } +// 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); - { - 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(); - } +// show_link_definition(kind, self, point, snapshot, cx); +// return false; +// } +// } - self.link_go_to_definition_state.task = None; +// { +// 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.clear_highlights::(cx); - } +// self.link_go_to_definition_state.task = None; - false - } +// self.clear_highlights::(cx); +// } - 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 => {} - } - } +// false +// } - for layer in self.keymap_context_layers.values() { - keymap.extend(layer); - } +// 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 => {} +// } +// } - 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()); - } - } +// for layer in self.keymap_context_layers.values() { +// keymap.extend(layer); +// } - 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(), - ) - } +// 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 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; - } +// 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(), +// ) +// } - let range = self.selections.newest::(cx).range(); - Some(range.start.0..range.end.0) - } +// 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; +// } - 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) - } +// let range = self.selections.newest::(cx).range(); +// Some(range.start.0..range.end.0) +// } - fn unmark_text(&mut self, cx: &mut ViewContext) { - self.clear_highlights::(cx); - self.ime_transaction.take(); - } +// 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 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; - } +// fn unmark_text(&mut self, cx: &mut ViewContext) { +// self.clear_highlights::(cx); +// self.ime_transaction.take(); +// } - 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) - }); - } +// 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; +// } - this.handle_input(text, cx); - }); +// 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) +// }; - if let Some(transaction) = self.ime_transaction { - self.buffer.update(cx, |buffer, cx| { - buffer.group_until_transaction(transaction, 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 +// } +// }) +// }); - self.unmark_text(cx); - } +// cx.emit(Event::InputHandled { +// utf16_range_to_replace: range_to_replace, +// text: text.into(), +// }); - 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; - } +// if let Some(new_selected_ranges) = new_selected_ranges { +// this.change_selections(None, cx, |selections| { +// selections.select_ranges(new_selected_ranges) +// }); +// } - 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)); - } +// this.handle_input(text, cx); +// }); - 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, - ); - } +// if let Some(transaction) = self.ime_transaction { +// self.buffer.update(cx, |buffer, cx| { +// buffer.group_until_transaction(transaction, 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.unmark_text(cx); +// } - 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); - }); - } +// 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; +// } - if self.text_highlights::(cx).is_none() { - self.ime_transaction.take(); - } - } -} +// 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 +// }; -fn build_style( - settings: &ThemeSettings, - get_field_editor_theme: Option<&GetFieldEditorTheme>, - override_text_style: Option<&OverrideTextStyle>, - cx: &mut 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; - } - } +// 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 +// } +// }) +// }); - style -} +// 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: &mut 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 { +// todo!(); +// // 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; @@ -9813,68 +9813,68 @@ impl InvalidationRegion for SnippetState { } } -impl Deref for EditorStyle { - type Target = theme::Editor; +// impl Deref for EditorStyle { +// type Target = theme::Editor; - fn deref(&self) -> &Self::Target { - &self.theme - } -} +// fn deref(&self) -> &Self::Target { +// &self.theme +// } +// } -pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> RenderBlock { - let mut highlighted_lines = Vec::new(); +// 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}")) - } +// 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() - }) -} +// _ => 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 = ThemeSettings::get_global(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, @@ -9901,139 +9901,139 @@ pub fn highlight_diagnostic_message( (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 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(); +// 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; +// 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)); - } +// // 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(FontWeight::BOLD); +// result.push((match_index..end_index, match_style)); +// } - if range.start == usize::MAX { - break; - } +// 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; - } +// // 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)); - } +// 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 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; - } +// let mut match_style = syntax_highlight; +// match_style.weight = Some(FontWeight::BOLD); +// result.push((match_index..end_index, match_style)); +// offset = end_index; +// } - if offset < range.end { - result.push((offset..range.end, syntax_highlight)); - } - } +// 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() - } +// fn char_ix_after(ix: usize, text: &str) -> usize { +// ix + text[ix..].chars().next().unwrap().len_utf8() +// } - result -} +// 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); +// 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); - if ix + 1 == label.runs.len() && label.text.len() > prev_end { - runs.push((prev_end..label.text.len(), 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); - runs - }) -} +// 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; diff --git a/crates/editor2/src/editor_settings.rs b/crates/editor2/src/editor_settings.rs index 349d1c62fa9b6c5623ae7312a1d224500ceebc8f..45c797598f51c4bde2f0008d02ac62d55accb3c7 100644 --- a/crates/editor2/src/editor_settings.rs +++ b/crates/editor2/src/editor_settings.rs @@ -1,6 +1,6 @@ -use gpui::Settings; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use settings::Settings; #[derive(Deserialize)] pub struct EditorSettings { @@ -55,7 +55,7 @@ impl Settings for EditorSettings { fn load( default_value: &Self::FileContent, user_values: &[&Self::FileContent], - _: &gpui::AppContext, + _: &mut gpui::AppContext, ) -> anyhow::Result { Self::load_via_json_merge(default_value, user_values) } diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 3583085fcc6b3928493e42c2368b775dad4649a1..dc0b9f6751216618f4c6a46f4d905e479e53a2cc 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -3,9 +3,11 @@ use super::{ }; use crate::{ display_map::{BlockStyle, DisplaySnapshot}, - mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt, + EditorStyle, +}; +use gpui::{ + px, relative, AnyElement, Bounds, Element, Hsla, Line, Pixels, Size, Style, TextRun, TextSystem, }; -use gpui::{AnyElement, Bounds, Element, Hsla, Line, Pixels, Size, Style, TextRun, TextSystem}; use language::{CursorShape, Selection}; use std::{ops::Range, sync::Arc}; use sum_tree::Bias; @@ -1997,7 +1999,10 @@ impl Element for EditorElement { element_state: &mut Self::ElementState, cx: &mut gpui::ViewContext, ) -> gpui::LayoutId { - cx.request_layout(Style::default().size_full(), None) + let mut style = Style::default(); + style.size.width = relative(1.).into(); + style.size.height = relative(1.).into(); + cx.request_layout(&style, None) } fn paint( @@ -2011,13 +2016,8 @@ impl Element for EditorElement { let layout_text = cx.text_system().layout_text( "hello world", - text_style.font_size, - &[TextRun { - len: "hello world".len(), - font: text_style.font, - color: text_style.color, - underline: text_style.underline, - }], + text_style.font_size * cx.rem_size(), + &[text_style.to_run("hello world".len())], None, ); } @@ -2697,19 +2697,19 @@ impl PositionMap { position: gpui::Point, ) -> 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 position = position - text_bounds.origin; + let y = position.y.max(px(0.)).min(self.size.width); + let x = position.x + (scroll_position.x * self.em_width); + let row = (y / self.line_height + scroll_position.y).into(); let (column, x_overshoot_after_line_end) = if let Some(line) = self .line_layouts - .get(row as usize - scroll_position.y() as usize) + .get(row as usize - scroll_position.y.into()) .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())) + (line.len() as u32, px(0.).max(x - line.width())) } } else { (0, x) diff --git a/crates/editor2/src/hover_popover.rs b/crates/editor2/src/hover_popover.rs index 784b912c8c4e045443a53456c1e3c32204f69781..b17c5df28ff5035a6b02790b18e0a687f353095f 100644 --- a/crates/editor2/src/hover_popover.rs +++ b/crates/editor2/src/hover_popover.rs @@ -466,35 +466,35 @@ pub struct InfoPopover { 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() - } -} +// 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 { diff --git a/crates/editor2/src/inlay_hint_cache.rs b/crates/editor2/src/inlay_hint_cache.rs index 759323ecdf3b35b03466bec596af170908360d22..74d6092ffa9eda57bef5425c258862ea1358b719 100644 --- a/crates/editor2/src/inlay_hint_cache.rs +++ b/crates/editor2/src/inlay_hint_cache.rs @@ -521,7 +521,7 @@ impl InlayHintCache { buffer_id: u64, excerpt_id: ExcerptId, id: InlayId, - cx: &mut ViewContext<'_, '_, Editor>, + cx: &mut ViewContext<'_, Editor>, ) { if let Some(excerpt_hints) = self.hints.get(&excerpt_id) { let mut guard = excerpt_hints.write(); @@ -582,7 +582,7 @@ fn spawn_new_update_tasks( excerpts_to_query: HashMap, Global, Range)>, invalidate: InvalidationStrategy, update_cache_version: usize, - cx: &mut ViewContext<'_, '_, Editor>, + 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 @@ -760,7 +760,7 @@ fn new_update_task( visible_hints: Arc>, cached_excerpt_hints: Option>>, lsp_request_limiter: Arc, - cx: &mut ViewContext<'_, '_, Editor>, + cx: &mut ViewContext<'_, Editor>, ) -> Task<()> { cx.spawn(|editor, mut cx| async move { let closure_cx = cx.clone(); @@ -836,134 +836,134 @@ fn new_update_task( }) } -async fn fetch_and_update_hints( - editor: gpui::WeakView, - 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(()) -} +// async fn fetch_and_update_hints( +// editor: gpui::WeakView, +// 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, @@ -1071,7 +1071,7 @@ fn apply_hint_update( invalidate: bool, buffer_snapshot: BufferSnapshot, multi_buffer_snapshot: MultiBufferSnapshot, - cx: &mut ViewContext<'_, '_, Editor>, + cx: &mut ViewContext<'_, Editor>, ) { let cached_excerpt_hints = editor .inlay_hint_cache diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index d5c479ef6b99abb6fa81f8aaa2da1eea55b2ec09..13d5dc4d1bdbe59b60b31b226a18f49970de98bf 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -7,8 +7,8 @@ use anyhow::{Context, Result}; use collections::HashSet; use futures::future::try_join_all; use gpui::{ - point, AnyElement, AppContext, AsyncAppContext, Entity, Model, Pixels, Subscription, Task, - View, ViewContext, WeakView, + point, AnyElement, AppContext, AsyncAppContext, Entity, Model, Pixels, SharedString, + Subscription, Task, View, ViewContext, WeakView, }; use language::{ proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point, @@ -304,133 +304,133 @@ impl FollowableItem for Editor { } } -async fn update_editor_from_message( - this: WeakView, - project: Model, - 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) 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, - ); - } +// async fn update_editor_from_message( +// this: WeakView, +// project: Model, +// 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) 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: point(message.scroll_x, message.scroll_y), - }, - cx, - ); - } - })?; - Ok(()) -} +// 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: point(message.scroll_x, message.scroll_y), +// }, +// cx, +// ); +// } +// })?; +// Ok(()) +// } fn serialize_excerpt( buffer_id: u64, @@ -544,7 +544,7 @@ impl Item for Editor { } } - fn tab_tooltip_text(&self, cx: &AppContext) -> Option> { + fn tab_tooltip_text(&self, cx: &AppContext) -> Option { let file_path = self .buffer() .read(cx) @@ -559,7 +559,7 @@ impl Item for Editor { Some(file_path.into()) } - fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option> { + 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()), diff --git a/crates/editor2/src/link_go_to_definition.rs b/crates/editor2/src/link_go_to_definition.rs index 8ac27b480f77d74cb9202f45c87d8da1fa89c905..cdbab3b3fc8bfa9c825a61b9e1a30be6eb7089bc 100644 --- a/crates/editor2/src/link_go_to_definition.rs +++ b/crates/editor2/src/link_go_to_definition.rs @@ -168,7 +168,7 @@ pub fn update_inlay_link_and_hover_points( editor: &mut Editor, cmd_held: bool, shift_held: bool, - cx: &mut ViewContext<'_, '_, Editor>, + 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)) diff --git a/crates/editor2/src/scroll.rs b/crates/editor2/src/scroll.rs index 9a6af2961aa6ac1cca38bcdc08f530dfa0ecc6bb..4e809dbef4aff350c33e22c286301158d4b4fbd9 100644 --- a/crates/editor2/src/scroll.rs +++ b/crates/editor2/src/scroll.rs @@ -9,7 +9,7 @@ use crate::{ Anchor, DisplayPoint, Editor, EditorMode, Event, InlayHintRefreshReason, MultiBufferSnapshot, ToPoint, }; -use gpui::{point, AppContext, Pixels, Task, ViewContext}; +use gpui::{point, px, AppContext, Pixels, Styled, Task, ViewContext}; use language::{Bias, Point}; use std::{ cmp::Ordering, @@ -44,7 +44,7 @@ impl ScrollAnchor { } } - pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Point { + pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point { let mut scroll_position = self.offset; if self.anchor != Anchor::min() { let scroll_top = self.anchor.to_display_point(snapshot).row() as f32; @@ -80,13 +80,13 @@ impl OngoingScroll { } } - pub fn filter(&self, delta: &mut Point) -> Option { + pub fn filter(&self, delta: &mut gpui::Point) -> 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 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 @@ -115,8 +115,12 @@ impl OngoingScroll { } match axis { - Some(Axis::Vertical) => *delta = point(0., delta.y()), - Some(Axis::Horizontal) => *delta = point(delta.x(), 0.), + Some(Axis::Vertical) => { + *delta = point(pk(0.), delta.y()); + } + Some(Axis::Horizontal) => { + *delta = point(delta.x(), px(0.)); + } None => {} } @@ -167,13 +171,13 @@ impl ScrollManager { self.ongoing.axis = axis; } - pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Point { + pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point { self.anchor.scroll_position(snapshot) } fn set_scroll_position( &mut self, - scroll_position: Point, + scroll_position: gpui::Point, map: &DisplaySnapshot, local: bool, autoscroll: bool, @@ -282,160 +286,161 @@ impl ScrollManager { } } -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: Point, - cx: &mut ViewContext, - ) { - self.set_scroll_position_internal(scroll_position, true, false, cx); - } - - pub(crate) fn set_scroll_position_internal( - &mut self, - scroll_position: Point, - 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) -> Point { - 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 + point(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: Point::new(x, y), - anchor: top_anchor, - }; - self.set_scroll_anchor(scroll_anchor, cx); - } - } -} +// todo!() +// 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: gpui::Point, +// cx: &mut ViewContext, +// ) { +// self.set_scroll_position_internal(scroll_position, true, false, cx); +// } + +// pub(crate) fn set_scroll_position_internal( +// &mut self, +// scroll_position: gpui::Point, +// 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) -> gpui::Point { +// 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 + point(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: Point::new(x, y), +// anchor: top_anchor, +// }; +// self.set_scroll_anchor(scroll_anchor, cx); +// } +// } +// } diff --git a/crates/editor2/src/selections_collection.rs b/crates/editor2/src/selections_collection.rs index e1ef69ac8aa41a461853469b42de6aa6cbafbc42..0d10db7af9983b4c8c300d088e0f08a5a0f2e2cc 100644 --- a/crates/editor2/src/selections_collection.rs +++ b/crates/editor2/src/selections_collection.rs @@ -593,29 +593,30 @@ impl<'a> MutableSelectionsCollection<'a> { } 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) + todo!() + // 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 { diff --git a/crates/editor2/src/test.rs b/crates/editor2/src/test.rs index 8087569925309c1da68b78088f63f597f52c8288..14619c4a98237cff93595b6ba52abee033babbcc 100644 --- a/crates/editor2/src/test.rs +++ b/crates/editor2/src/test.rs @@ -1,80 +1,81 @@ pub mod editor_lsp_test_context; pub mod editor_test_context; -use crate::{ - display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint}, - DisplayPoint, Editor, EditorMode, MultiBuffer, -}; +// todo!() +// use crate::{ +// display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint}, +// DisplayPoint, Editor, EditorMode, MultiBuffer, +// }; -use gpui::{Model, ViewContext}; +// use gpui::{Model, ViewContext}; -use project::Project; -use util::test::{marked_text_offsets, marked_text_ranges}; +// 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(); - } -} +// #[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); +// // 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 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(); +// 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) -} +// (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 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); -} +// 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: Model, cx: &mut ViewContext) -> Editor { - Editor::new(EditorMode::Full, buffer, None, None, cx) -} +// // 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: Model, cx: &mut ViewContext) -> Editor { +// Editor::new(EditorMode::Full, buffer, None, None, cx) +// } -pub(crate) fn build_editor_with_project( - project: Model, - buffer: Model, - cx: &mut ViewContext, -) -> Editor { - Editor::new(EditorMode::Full, buffer, Some(project), None, cx) -} +// pub(crate) fn build_editor_with_project( +// project: Model, +// buffer: Model, +// cx: &mut ViewContext, +// ) -> Editor { +// Editor::new(EditorMode::Full, buffer, Some(project), None, cx) +// } diff --git a/crates/editor2/src/test/editor_test_context.rs b/crates/editor2/src/test/editor_test_context.rs index c856afeefe3a71256ee9655487fd36ec6b72b602..0ca043462e6bfb3c9287f8a49cf754e4d2634945 100644 --- a/crates/editor2/src/test/editor_test_context.rs +++ b/crates/editor2/src/test/editor_test_context.rs @@ -17,304 +17,304 @@ use util::{ 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: View, -} - -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, - // ); - // } - // } -} +// use super::build_editor_with_project; + +// pub struct EditorTestContext<'a> { +// pub cx: &'a mut gpui::TestAppContext, +// pub window: AnyWindowHandle, +// pub editor: View, +// } + +// 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.update(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; From dfc7c815000b3dfd0cf6b25254ab9f87009abb6c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 2 Nov 2023 21:03:29 -0600 Subject: [PATCH 06/23] WIP --- Cargo.lock | 2 +- crates/editor2/src/display_map.rs | 5 +- crates/editor2/src/display_map/wrap_map.rs | 2 +- crates/editor2/src/editor.rs | 15128 ++++++++-------- crates/editor2/src/items.rs | 227 +- crates/editor2/src/movement.rs | 962 +- crates/editor2/src/scroll.rs | 28 +- crates/editor2/src/scroll/autoscroll.rs | 17 +- .../src/test/editor_lsp_test_context.rs | 594 +- .../editor2/src/test/editor_test_context.rs | 24 +- crates/gpui2/src/geometry.rs | 4 + crates/workspace2/src/item.rs | 2 +- 12 files changed, 8488 insertions(+), 8507 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ffd69fb1dfd09a7683841499154d98db326909eb..bd4fca5fa531388d556be9ae470ec3be605c74f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2648,7 +2648,7 @@ dependencies = [ "lazy_static", "log", "lsp2", - "multi_buffer", + "multi_buffer2", "ordered-float 2.10.0", "parking_lot 0.11.2", "postage", diff --git a/crates/editor2/src/display_map.rs b/crates/editor2/src/display_map.rs index ab137df74f3d8c32f853a11c41d0e4b2320a80db..94391f7cb5be070e8131972a37c923f946f3eb3a 100644 --- a/crates/editor2/src/display_map.rs +++ b/crates/editor2/src/display_map.rs @@ -6,17 +6,16 @@ mod wrap_map; use crate::{ link_go_to_definition::InlayHighlight, movement::TextLayoutDetails, Anchor, AnchorRangeExt, - EditorStyle, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, + InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; pub use block_map::{BlockMap, BlockPoint}; use collections::{BTreeMap, HashMap, HashSet}; use fold_map::FoldMap; -use gpui::{FontId, HighlightStyle, Hsla, Line, Model, ModelContext, UnderlineStyle}; +use gpui::{FontId, HighlightStyle, Hsla, Line, Model, ModelContext}; 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; diff --git a/crates/editor2/src/display_map/wrap_map.rs b/crates/editor2/src/display_map/wrap_map.rs index d6f321f6161402bf90ae8912f291e5370d9cb45f..06f06fb5c36dded4cb379db4134017f3fcc95651 100644 --- a/crates/editor2/src/display_map/wrap_map.rs +++ b/crates/editor2/src/display_map/wrap_map.rs @@ -73,7 +73,7 @@ impl WrapMap { wrap_width: Option, cx: &mut AppContext, ) -> (Model, WrapSnapshot) { - let handle = cx.add_model(|cx| { + let handle = cx.build_model(|cx| { let mut this = Self { font: (font_id, font_size), wrap_width: None, diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 0ae03661e862809d4f534fac902605d91edee3ed..ee552d3dbd7ecf39c4ab04a7416ddec4c2e9da9c 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -19,15 +19,11 @@ pub mod selections_collection; 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; +use client::{Collaborator, ParticipantIndex}; +use clock::ReplicaId; +use collections::{HashMap, HashSet, VecDeque}; pub use display_map::DisplayPoint; use display_map::*; pub use editor_settings::EditorSettings; @@ -37,43 +33,26 @@ pub use element::{ use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - serde_json, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, - EventEmitter, FontWeight, HighlightStyle, Hsla, Model, Pixels, Quad, Render, Subscription, - Task, Text, View, ViewContext, WeakView, WindowContext, + AnyElement, AppContext, Element, EventEmitter, Model, Pixels, Render, Subscription, Task, View, + ViewContext, WeakView, }; -use highlight_matching_bracket::refresh_matching_bracket_highlights; -use hover_popover::{hide_hover, HoverState}; -use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; +use hover_popover::HoverState; 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}, - point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, - CursorShape, Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, - LanguageRegistry, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, Selection, - SelectionGoal, TransactionId, + AutoindentMode, BracketPair, Buffer, CodeAction, Completion, CursorShape, Diagnostic, Language, + OffsetRangeExt, 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; +use link_go_to_definition::LinkGoToDefinitionState; 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 project::Project; +use rpc::proto::*; +use scroll::{autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager}; use selections_collection::SelectionsCollection; use serde::{Deserialize, Serialize}; use settings::Settings; @@ -1763,7577 +1742,7577 @@ impl InlayHintRefreshReason { } } -// 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) -// } +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: Model, + // 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: Model, + // 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: Model, + // 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) -> &Model { + &self.buffer + } -// 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) -// } + // 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, Model, 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) { + // todo!(); + // // self.refresh_inlay_hints( + // // InlayHintRefreshReason::Toggle(!self.inlay_hint_cache.enabled), + // // cx, + // // ); + // } + + // pub fn inlay_hints_enabled(&self) -> bool { + // todo!(); + // 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 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: Model, + // 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, _: Model, cx: &mut ViewContext) { + // cx.notify(); + // } + + // fn on_buffer_event( + // &mut self, + // multibuffer: Model, + // 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, _: Model, 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 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 trait CollaborationHub { + fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap; + fn user_participant_indices<'a>( + &self, + cx: &'a AppContext, + ) -> &'a HashMap; +} -// pub fn for_buffer( -// buffer: Model, -// project: Option>, -// cx: &mut ViewContext, -// ) -> Self { -// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); -// Self::new(EditorMode::Full, buffer, project, None, cx) -// } +impl CollaborationHub for Model { + fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap { + self.read(cx).collaborators() + } -// pub fn for_multibuffer( -// buffer: Model, -// project: Option>, -// cx: &mut ViewContext, -// ) -> Self { -// Self::new(EditorMode::Full, buffer, project, None, cx) -// } + fn user_participant_indices<'a>( + &self, + cx: &'a AppContext, + ) -> &'a HashMap { + self.read(cx).user_store().read(cx).participant_indices() + } +} -// 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 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 new( -// mode: EditorMode, -// buffer: Model, -// 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, -// ) -// }); +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); -// let selections = SelectionsCollection::new(display_map.clone(), buffer.clone()); + 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) +} -// let blink_manager = cx.add_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx)); +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 + } +} -// let soft_wrap_mode_override = -// (mode == EditorMode::SingleLine).then(|| language_settings::SoftWrap::None); +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, + }) + }) + } -// 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); -// }; -// })); -// } -// } + pub fn language_at(&self, position: T) -> Option<&Arc> { + self.display_snapshot.buffer_snapshot.language_at(position) + } -// 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); -// } -// }); -// }), -// ], -// }; + pub fn is_focused(&self) -> bool { + self.is_focused + } -// this._subscriptions.extend(project_subscriptions); + pub fn placeholder_text(&self) -> Option<&Arc> { + self.placeholder_text.as_ref() + } -// this.end_selection(cx); -// this.scroll_manager.show_scrollbar(cx); + pub fn scroll_position(&self) -> gpui::Point { + self.scroll_anchor.scroll_position(&self.display_snapshot) + } +} -// let editor_created_event = EditorCreated(cx.handle()); -// cx.emit_global(editor_created_event); +impl Deref for EditorSnapshot { + type Target = DisplaySnapshot; -// if mode == EditorMode::Full { -// let should_auto_hide_scrollbars = cx.platform().should_auto_hide_scrollbars(); -// cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars)); -// } + fn deref(&self) -> &Self::Target { + &self.display_snapshot + } +} -// this.report_editor_event("open", None, cx); -// this -// } +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Event { + InputIgnored { + text: Arc, + }, + InputHandled { + utf16_range_to_replace: Option>, + text: Arc, + }, + ExcerptsAdded { + buffer: Model, + 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 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 struct EditorFocused(pub View); +pub struct EditorBlurred(pub View); +pub struct EditorReleased(pub WeakView); -// 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, -// ); -// } -// } +// impl Entity for Editor { +// type Event = Event; -// pub fn replica_id(&self, cx: &AppContext) -> ReplicaId { -// self.buffer.read(cx).replica_id() +// fn release(&mut self, cx: &mut AppContext) { +// cx.emit_global(EditorReleased(self.handle.clone())); // } +// } +// +impl EventEmitter for Editor { + type Event = Event; +} -// pub fn leader_peer_id(&self) -> Option { -// self.leader_peer_id -// } +impl Render for Editor { + type Element = EditorElement; -pub fn buffer(&self) -> &Model { - &self.buffer + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + todo!() + } } -// 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) -// } +// 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) +// }); -// 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)), +// 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() // } -// pub fn language_at<'a, T: ToOffset>( -// &self, -// point: T, -// cx: &'a AppContext, -// ) -> Option> { -// self.buffer.read(cx).language_at(point, cx) +// fn ui_name() -> &'static str { +// "Editor" // } -// 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, Model, 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) { -// todo!(); -// // self.refresh_inlay_hints( -// // InlayHintRefreshReason::Toggle(!self.inlay_hint_cache.enabled), -// // cx, -// // ); -// } - -// pub fn inlay_hints_enabled(&self) -> bool { -// todo!(); -// 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 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: Model, -// 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, _: Model, cx: &mut ViewContext) { -// cx.notify(); -// } - -// fn on_buffer_event( -// &mut self, -// multibuffer: Model, -// 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, _: Model, 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 Model { - 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) -> gpui::Point { - 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: Model, - 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 View); -pub struct EditorBlurred(pub View); -pub struct EditorReleased(pub WeakView); - -// impl Entity for Editor { -// type Event = Event; - -// fn release(&mut self, cx: &mut AppContext) { -// cx.emit_global(EditorReleased(self.handle.clone())); -// } -// } -// -impl EventEmitter for Editor { - type Event = Event; -} - -impl Render for Editor { - type Element = EditorElement; - - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - todo!() - } -} - -// 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: AnyView, 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_in(&mut self, focused: AnyView, 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, _: AnyView, cx: &mut ViewContext) { @@ -10033,7 +10012,6 @@ pub fn highlight_diagnostic_message( // runs // }) -// } pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator + 'a { let mut index = 0; diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 13d5dc4d1bdbe59b60b31b226a18f49970de98bf..f01e6ab2b30a18e5fd0cdfc3c667526bc3d47740 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -48,106 +48,108 @@ impl FollowableItem for Editor { 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) - })) + todo!() } + // 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; @@ -197,8 +199,8 @@ impl FollowableItem for Editor { 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(), + scroll_x: scroll_anchor.offset.x, + scroll_y: scroll_anchor.offset.y, selections: self .selections .disjoint_anchors() @@ -254,8 +256,8 @@ impl FollowableItem for Editor { 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(); + update.scroll_x = scroll_anchor.offset.x; + update.scroll_y = scroll_anchor.offset.y; true } Event::SelectionsChanged { .. } => { @@ -561,8 +563,8 @@ impl Item for Editor { 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()), + Cow::Borrowed(path) => Some(path.to_string_lossy), + Cow::Owned(path) => Some(path.to_string_lossy.to_string().into()), } } @@ -598,7 +600,11 @@ impl Item for Editor { self.buffer.read(cx).is_singleton() } - fn clone_on_split(&self, _workspace_id: WorkspaceId, cx: &mut ViewContext) -> Option + fn clone_on_split( + &self, + _workspace_id: WorkspaceId, + cx: &mut ViewContext, + ) -> Option> where Self: Sized, { @@ -611,7 +617,8 @@ impl Item for Editor { fn deactivated(&mut self, cx: &mut ViewContext) { let selection = self.selections.newest_anchor(); - self.push_to_nav_history(selection.head(), None, cx); + todo!() + // self.push_to_nav_history(selection.head(), None, cx); } fn workspace_deactivated(&mut self, cx: &mut ViewContext) { @@ -652,7 +659,7 @@ impl Item for Editor { // 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()) + buffer.read_with(&cx, |buffer, _| buffer.is_dirty || buffer.has_conflict()) }); project @@ -686,9 +693,7 @@ impl Item for Editor { .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()); + 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| { diff --git a/crates/editor2/src/movement.rs b/crates/editor2/src/movement.rs index 593f7a4831029af73d17182adad2f710e4c9220c..0749c3f17855f5a8b8fdd8b4c108e944cb3181ac 100644 --- a/crates/editor2/src/movement.rs +++ b/crates/editor2/src/movement.rs @@ -2,7 +2,7 @@ use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint}; use gpui::TextSystem; use language::Point; -use std::{ops::Range, sync::Arc}; +use std::ops::Range; #[derive(Debug, PartialEq)] pub enum FindRange { @@ -444,483 +444,483 @@ pub fn split_display_range_by_lines( 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); - } -} +// #[cfg(test)] +// mod tests { +// use super::*; +// use crate::{ +// display_map::Inlay, +// test::{}, +// 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/scroll.rs b/crates/editor2/src/scroll.rs index 4e809dbef4aff350c33e22c286301158d4b4fbd9..2abf80a747773b510b088d40fa16bc3221098a69 100644 --- a/crates/editor2/src/scroll.rs +++ b/crates/editor2/src/scroll.rs @@ -39,7 +39,7 @@ pub struct ScrollAnchor { impl ScrollAnchor { fn new() -> Self { Self { - offset: Point::zero(), + offset: gpui::Point::zero(), anchor: Anchor::min(), } } @@ -48,7 +48,7 @@ impl ScrollAnchor { 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()); + scroll_position.set_y(scroll_top + scroll_position.y); } else { scroll_position.set_y(0.); } @@ -82,7 +82,7 @@ impl OngoingScroll { pub fn filter(&self, delta: &mut gpui::Point) -> Option { const UNLOCK_PERCENT: f32 = 1.9; - const UNLOCK_LOWER_BOUND: f32 = 6.; + const UNLOCK_LOWER_BOUND: Pixels = px(6.); let mut axis = self.axis; let x = delta.x.abs(); @@ -116,10 +116,10 @@ impl OngoingScroll { match axis { Some(Axis::Vertical) => { - *delta = point(pk(0.), delta.y()); + *delta = point(px(0.), delta.y); } Some(Axis::Horizontal) => { - *delta = point(delta.x(), px(0.)); + *delta = point(delta.x, px(0.)); } None => {} } @@ -177,14 +177,14 @@ impl ScrollManager { fn set_scroll_position( &mut self, - scroll_position: gpui::Point, + scroll_position: gpui::Point, map: &DisplaySnapshot, local: bool, autoscroll: bool, workspace_id: Option, cx: &mut ViewContext, ) { - let (new_anchor, top_row) = if scroll_position.y() <= 0. { + let (new_anchor, top_row) = if scroll_position.y <= 0. { ( ScrollAnchor { anchor: Anchor::min(), @@ -194,7 +194,7 @@ impl ScrollManager { ) } else { let scroll_top_buffer_point = - DisplayPoint::new(scroll_position.y() as u32, 0).to_point(&map); + 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); @@ -203,8 +203,8 @@ impl ScrollManager { ScrollAnchor { anchor: top_anchor, offset: point( - scroll_position.x(), - scroll_position.y() - top_anchor.to_display_point(&map).row() as f32, + scroll_position.x, + scroll_position.y - top_anchor.to_display_point(&map).row() as f32, ), }, scroll_top_buffer_point.row, @@ -236,8 +236,8 @@ impl ScrollManager { item_id, workspace_id, top_row, - anchor.offset.x(), - anchor.offset.y(), + anchor.offset.x, + anchor.offset.y, ) .await .log_err() @@ -277,8 +277,8 @@ impl ScrollManager { } pub fn clamp_scroll_left(&mut self, max: f32) -> bool { - if max < self.anchor.offset.x() { - self.anchor.offset.set_x(max); + if max < self.anchor.offset.x { + self.anchor.offset.x = max; true } else { false diff --git a/crates/editor2/src/scroll/autoscroll.rs b/crates/editor2/src/scroll/autoscroll.rs index ffada50179fa233b12e4a02b4fed6e52bcf137ca..5816e5683baa733629e65550b6dcf1f4913ce808 100644 --- a/crates/editor2/src/scroll/autoscroll.rs +++ b/crates/editor2/src/scroll/autoscroll.rs @@ -60,7 +60,7 @@ impl Editor { } else { display_map.max_point().row() as f32 }; - if scroll_position.y() > max_scroll_top { + if scroll_position.y > max_scroll_top { scroll_position.set_y(max_scroll_top); self.set_scroll_position(scroll_position, cx); } @@ -136,7 +136,7 @@ impl Editor { 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 start_row = scroll_position.y; let end_row = start_row + visible_lines; let needs_scroll_up = target_top < start_row; @@ -222,20 +222,15 @@ impl Editor { return false; } - let scroll_left = self.scroll_manager.anchor.offset.x() * max_glyph_width; + 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); + self.scroll_manager.anchor.offset.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); + self.scroll_manager.anchor.offset.x = + ((target_right - viewport_width) / max_glyph_width); true } else { false diff --git a/crates/editor2/src/test/editor_lsp_test_context.rs b/crates/editor2/src/test/editor_lsp_test_context.rs index 6d1662857d641a3f419a7a5aa4316a0cdd443dd4..5a6bdd2723d192486b2fddf4d25cbe3d6818e0af 100644 --- a/crates/editor2/src/test/editor_lsp_test_context.rs +++ b/crates/editor2/src/test/editor_lsp_test_context.rs @@ -1,297 +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, View, ViewContext}; -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 - } -} +// 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, View, ViewContext}; +// 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: View, +// 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 index 0ca043462e6bfb3c9287f8a49cf754e4d2634945..4bf32d061323b9fc6e196dcf7decb1b50777e973 100644 --- a/crates/editor2/src/test/editor_test_context.rs +++ b/crates/editor2/src/test/editor_test_context.rs @@ -315,17 +315,17 @@ use util::{ // } // } // } +// +// impl<'a> Deref for EditorTestContext<'a> { +// type Target = gpui::TestAppContext; -impl<'a> Deref for EditorTestContext<'a> { - type Target = gpui::TestAppContext; - - fn deref(&self) -> &Self::Target { - self.cx - } -} +// fn deref(&self) -> &Self::Target { +// self.cx +// } +// } -impl<'a> DerefMut for EditorTestContext<'a> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.cx - } -} +// impl<'a> DerefMut for EditorTestContext<'a> { +// fn deref_mut(&mut self) -> &mut Self::Target { +// &mut self.cx +// } +// } diff --git a/crates/gpui2/src/geometry.rs b/crates/gpui2/src/geometry.rs index 7d4073144c33281e67d4b1845479162c1712d057..468bc1e5b76809a3c6ce88dbfe2a0b9d79045518 100644 --- a/crates/gpui2/src/geometry.rs +++ b/crates/gpui2/src/geometry.rs @@ -755,6 +755,10 @@ impl Pixels { pub fn pow(&self, exponent: f32) -> Self { Self(self.0.powf(exponent)) } + + pub fn abs(&self) -> Self { + Self(self.0.abs()) + } } impl Mul for Pixels { diff --git a/crates/workspace2/src/item.rs b/crates/workspace2/src/item.rs index c2d5c25781148218aef53fe90d1d659fd84c2c54..309cf96615774a6b7d10e1e83941e561ac2c2f47 100644 --- a/crates/workspace2/src/item.rs +++ b/crates/workspace2/src/item.rs @@ -90,7 +90,7 @@ pub struct BreadcrumbText { pub highlights: Option, HighlightStyle)>>, } -pub trait Item: Render + EventEmitter + Send { +pub trait Item: Render + EventEmitter { fn deactivated(&mut self, _: &mut ViewContext) {} fn workspace_deactivated(&mut self, _: &mut ViewContext) {} fn navigate(&mut self, _: Box, _: &mut ViewContext) -> bool { From a731f8fb1ef0521b85f2a05a6ac0f45c21fccc08 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 2 Nov 2023 21:28:56 -0600 Subject: [PATCH 07/23] WIP --- crates/editor2/src/display_map.rs | 106 ++++++------ crates/editor2/src/display_map/wrap_map.rs | 169 ++++++++++---------- crates/gpui2/src/text_system/line.rs | 5 +- crates/gpui2/src/text_system/line_layout.rs | 22 +++ 4 files changed, 165 insertions(+), 137 deletions(-) diff --git a/crates/editor2/src/display_map.rs b/crates/editor2/src/display_map.rs index 94391f7cb5be070e8131972a37c923f946f3eb3a..0635154910bd770ad19e0bfb4d849aeb8095c10b 100644 --- a/crates/editor2/src/display_map.rs +++ b/crates/editor2/src/display_map.rs @@ -11,7 +11,7 @@ use crate::{ pub use block_map::{BlockMap, BlockPoint}; use collections::{BTreeMap, HashMap, HashSet}; use fold_map::FoldMap; -use gpui::{FontId, HighlightStyle, Hsla, Line, Model, ModelContext}; +use gpui::{Font, FontId, HighlightStyle, Hsla, Line, Model, ModelContext, Pixels}; use inlay_map::InlayMap; use language::{ language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription, @@ -58,8 +58,8 @@ pub struct DisplayMap { impl DisplayMap { pub fn new( buffer: Model, - font_id: FontId, - font_size: f32, + font: Font, + font_size: Pixels, wrap_width: Option, buffer_header_height: u8, excerpt_header_height: u8, @@ -71,7 +71,7 @@ impl DisplayMap { 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 (wrap_map, snapshot) = WrapMap::new(snapshot, font, 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 { @@ -239,7 +239,7 @@ impl DisplayMap { cleared } - pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext) -> bool { + pub fn set_font(&self, font: Font, font_size: Pixels, cx: &mut ModelContext) -> bool { self.wrap_map .update(cx, |map, cx| map.set_font(font_id, font_size, cx)) } @@ -248,7 +248,7 @@ impl DisplayMap { self.fold_map.set_ellipses_color(color) } - pub fn set_wrap_width(&self, width: Option, cx: &mut ModelContext) -> bool { + 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)) } @@ -558,62 +558,62 @@ impl DisplaySnapshot { &self, display_row: u32, TextLayoutDetails { - text_system: font_cache, - text_system: text_layout_cache, + text_system, 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( - todo!(), // len: chunk.chunk.len(), - // 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"); - - todo!(); - // styles.push(RunStyle { - // len: "\n".len(), - // font_id: editor_style.text.font_id, - // color: editor_style.text_color, - // underline: editor_style.text.underline, - // }); - } - - text_layout_cache.layout_text(&line, editor_style.text.font_size, &styles, None) + todo!() + // 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, text_system) + // .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( + // todo!(), // len: chunk.chunk.len(), + // // 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"); + + // todo!(); + // // styles.push(RunStyle { + // // len: "\n".len(), + // // font_id: editor_style.text.font_id, + // // color: editor_style.text_color, + // // underline: editor_style.text.underline, + // // }); + // } + + // text_system.layout_text(&line, editor_style.text.font_size, &styles, None) } pub fn x_for_point( &self, display_point: DisplayPoint, text_layout_details: &TextLayoutDetails, - ) -> f32 { + ) -> Pixels { 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) } diff --git a/crates/editor2/src/display_map/wrap_map.rs b/crates/editor2/src/display_map/wrap_map.rs index 06f06fb5c36dded4cb379db4134017f3fcc95651..c7c55348b11c4959382b99589015dfdbfccd80e4 100644 --- a/crates/editor2/src/display_map/wrap_map.rs +++ b/crates/editor2/src/display_map/wrap_map.rs @@ -4,13 +4,14 @@ use super::{ Highlights, }; use crate::MultiBufferSnapshot; -use gpui::{AppContext, FontId, LineWrapper, Model, ModelContext, Pixels, Task}; +use gpui::{AppContext, Context, Font, LineWrapper, Model, ModelContext, Pixels, 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; +use util::ResultExt; pub use super::tab_map::TextSummary; pub type WrapEdit = text::Edit; @@ -22,7 +23,7 @@ pub struct WrapMap { edits_since_sync: Patch, wrap_width: Option, background_task: Option>, - font: (FontId, Pixels), + font: (Font, Pixels), } #[derive(Clone)] @@ -68,14 +69,14 @@ pub struct WrapBufferRows<'a> { impl WrapMap { pub fn new( tab_snapshot: TabSnapshot, - font_id: FontId, - font_size: f32, - wrap_width: Option, + font: Font, + font_size: Pixels, + wrap_width: Option, cx: &mut AppContext, ) -> (Model, WrapSnapshot) { let handle = cx.build_model(|cx| { let mut this = Self { - font: (font_id, font_size), + font: (font, font_size), wrap_width: None, pending_edits: Default::default(), interpolated_edits: Default::default(), @@ -115,14 +116,9 @@ impl WrapMap { (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); + pub fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut ModelContext) -> bool { + if (font, font_size) != self.font { + self.font = (font, font_size); self.rewrap(cx); true } else { @@ -151,28 +147,32 @@ impl WrapMap { if let Some(wrap_width) = self.wrap_width { let mut new_snapshot = self.snapshot.clone(); - let font_cache = cx.font_cache().clone(); + let mut edits = Patch::default(); + let text_system = cx.text_system().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; + let task = cx.background_executor().spawn(async move { + if let Some(mut line_wrapper) = + text_system.line_wrapper(font_id, font_size).log_err() + { + 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() + .background_executor() .block_with_timeout(Duration::from_millis(5), task) { Ok((snapshot, edits)) => { @@ -235,23 +235,25 @@ impl WrapMap { 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 text_system = cx.text_system().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 update_task = cx.background_executor().spawn(async move { 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); + if let Some(mut line_wrapper) = + text_system.line_wrapper(font_id, font_size).log_err() + { + 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() + .background_executor() .block_with_timeout(Duration::from_millis(1), update_task) { Ok((snapshot, output_edits)) => { @@ -733,48 +735,49 @@ impl WrapSnapshot { } 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 - ); - } - } + // todo!() + // #[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 + // ); + // } + // } } } diff --git a/crates/gpui2/src/text_system/line.rs b/crates/gpui2/src/text_system/line.rs index bfd1487d91eb4bbb576f1b8b88e4b80e8be18920..ad70a79bb1e411fdaa4b0fddc0de39bdbf96e7af 100644 --- a/crates/gpui2/src/text_system/line.rs +++ b/crates/gpui2/src/text_system/line.rs @@ -2,6 +2,7 @@ use crate::{ black, point, px, size, BorrowWindow, Bounds, Hsla, Pixels, Point, Result, Size, UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout, }; +use derive_more::{Deref, DerefMut}; use smallvec::SmallVec; use std::sync::Arc; @@ -12,8 +13,10 @@ pub struct DecorationRun { pub underline: Option, } -#[derive(Clone, Default, Debug)] +#[derive(Clone, Default, Debug, Deref, DerefMut)] pub struct Line { + #[deref] + #[deref_mut] pub(crate) layout: Arc, pub(crate) decorations: SmallVec<[DecorationRun; 32]>, } diff --git a/crates/gpui2/src/text_system/line_layout.rs b/crates/gpui2/src/text_system/line_layout.rs index 8ce859218b35241066f8917d188aaf7c7c279600..7682aaa1b818cac6ed24a3f3d6c02cb2ec490b55 100644 --- a/crates/gpui2/src/text_system/line_layout.rs +++ b/crates/gpui2/src/text_system/line_layout.rs @@ -48,6 +48,28 @@ impl LineLayout { } } + /// closest_index_for_x returns the character boundary closest to the given x coordinate + /// (e.g. to handle aligning up/down arrow keys) + pub fn closest_index_for_x(&self, x: Pixels) -> usize { + let mut prev_index = 0; + let mut prev_x = px(0.); + + for run in self.runs.iter() { + for glyph in run.glyphs.iter() { + if glyph.position.x >= x { + if glyph.position.x - x < x - prev_x { + return glyph.index; + } else { + return prev_index; + } + } + prev_index = glyph.index; + prev_x = glyph.position.x; + } + } + prev_index + } + pub fn x_for_index(&self, index: usize) -> Pixels { for run in &self.runs { for glyph in &run.glyphs { From f3b8a9d8c286f01a5fadf97919677bcafe67ec6f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 2 Nov 2023 22:56:04 -0600 Subject: [PATCH 08/23] WIP --- Cargo.lock | 2 +- crates/editor2/src/display_map.rs | 6 +- crates/editor2/src/editor.rs | 1608 +++++++++-------- crates/editor2/src/element.rs | 27 +- .../editor2/src/highlight_matching_bracket.rs | 43 +- crates/editor2/src/hover_popover.rs | 237 +-- crates/editor2/src/inlay_hint_cache.rs | 568 +++--- crates/editor2/src/items.rs | 149 +- crates/editor2/src/link_go_to_definition.rs | 386 ++-- crates/editor2/src/mouse_context_menu.rs | 37 +- crates/editor2/src/persistence.rs | 3 +- crates/editor2/src/scroll.rs | 339 ++-- crates/editor2/src/scroll/autoscroll.rs | 34 +- crates/editor2/src/scroll/scroll_amount.rs | 27 +- crates/editor2/src/selections_collection.rs | 68 +- crates/gpui2/src/executor.rs | 1 + crates/gpui2/src/geometry.rs | 12 + crates/gpui2/src/platform/mac/text_system.rs | 1 + crates/gpui2/src/style.rs | 9 + crates/gpui2/src/text_system.rs | 2 +- crates/gpui2/src/text_system/line.rs | 4 + crates/gpui2/src/text_system/line_layout.rs | 1 + crates/gpui2/src/window.rs | 1 + crates/text/Cargo.toml | 2 +- crates/text2/src/selection.rs | 6 +- crates/workspace2/src/item.rs | 18 +- 26 files changed, 1854 insertions(+), 1737 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bd4fca5fa531388d556be9ae470ec3be605c74f8..46d481c2eacc607e3f13426568e850cf36a907bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8884,7 +8884,7 @@ dependencies = [ "ctor", "digest 0.9.0", "env_logger 0.9.3", - "gpui", + "gpui2", "lazy_static", "log", "parking_lot 0.11.2", diff --git a/crates/editor2/src/display_map.rs b/crates/editor2/src/display_map.rs index 0635154910bd770ad19e0bfb4d849aeb8095c10b..3cf4d7340f0657cffd1667f7ff4d6e560f0e9af0 100644 --- a/crates/editor2/src/display_map.rs +++ b/crates/editor2/src/display_map.rs @@ -60,7 +60,7 @@ impl DisplayMap { buffer: Model, font: Font, font_size: Pixels, - wrap_width: Option, + wrap_width: Option, buffer_header_height: u8, excerpt_header_height: u8, cx: &mut ModelContext, @@ -241,7 +241,7 @@ impl DisplayMap { pub fn set_font(&self, font: Font, font_size: Pixels, cx: &mut ModelContext) -> bool { self.wrap_map - .update(cx, |map, cx| map.set_font(font_id, font_size, cx)) + .update(cx, |map, cx| map.set_font(font, font_size, cx)) } pub fn set_fold_ellipses_color(&mut self, color: Hsla) -> bool { @@ -621,7 +621,7 @@ impl DisplaySnapshot { pub fn column_for_x( &self, display_row: u32, - x_coordinate: f32, + x_coordinate: Pixels, text_layout_details: &TextLayoutDetails, ) -> u32 { let layout_line = self.lay_out_line_for_row(display_row, text_layout_details); diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index ee552d3dbd7ecf39c4ab04a7416ddec4c2e9da9c..11ad04a6596a70a112c249487825c0f4afd50c91 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -1,3 +1,4 @@ +#![allow(unused)] mod blink_manager; pub mod display_map; mod editor_settings; @@ -20,8 +21,9 @@ mod editor_tests; #[cfg(any(test, feature = "test-support"))] pub mod test; use aho_corasick::AhoCorasick; +use anyhow::Result; use blink_manager::BlinkManager; -use client::{Collaborator, ParticipantIndex}; +use client::{Client, Collaborator, ParticipantIndex}; use clock::ReplicaId; use collections::{HashMap, HashSet, VecDeque}; pub use display_map::DisplayPoint; @@ -33,8 +35,8 @@ pub use element::{ use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - AnyElement, AppContext, Element, EventEmitter, Model, Pixels, Render, Subscription, Task, View, - ViewContext, WeakView, + AnyElement, AppContext, BackgroundExecutor, Element, EventEmitter, Model, Pixels, Render, + Subscription, Task, TextStyle, View, ViewContext, WeakView, WindowContext, }; use hover_popover::HoverState; pub use items::MAX_TAB_TITLE_LEN; @@ -42,27 +44,30 @@ pub use language::{char_kind, CharKind}; use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, AutoindentMode, BracketPair, Buffer, CodeAction, Completion, CursorShape, Diagnostic, Language, - OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId, + LanguageRegistry, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId, }; use link_go_to_definition::LinkGoToDefinitionState; +use lsp::{Documentation, LanguageServerId}; pub use multi_buffer::{ Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; +use ordered_float::OrderedFloat; use parking_lot::RwLock; -use project::Project; +use project::{FormatTrigger, Project}; use rpc::proto::*; use scroll::{autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager}; use selections_collection::SelectionsCollection; use serde::{Deserialize, Serialize}; use settings::Settings; use std::{ + cmp::Reverse, ops::{Deref, DerefMut, Range}, sync::Arc, time::Duration, }; pub use sum_tree::Bias; -use util::TryFutureExt; +use util::{ResultExt, TryFutureExt}; use workspace::{ItemNavHistory, ViewId, Workspace}; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); @@ -569,7 +574,7 @@ pub enum SoftWrap { #[derive(Clone)] pub struct EditorStyle { - // pub text: TextStyle, + pub text: TextStyle, pub line_height_scalar: f32, // pub placeholder_text: Option, // pub theme: theme::Editor, @@ -887,10 +892,11 @@ impl ContextMenu { 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), - } + todo!() + // match self { + // ContextMenu::Completions(menu) => (cursor_position, menu.render(style, workspace, cx)), + // ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx), + // } } } @@ -903,688 +909,715 @@ struct CompletionsMenu { match_candidates: Arc<[StringMatchCandidate]>, matches: Arc<[StringMatch]>, selected_item: usize, - // list: UniformListState, + list: UniformListState, } -// impl CompletionsMenu { -// fn select_first(&mut self, project: Option<&Model>, 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(); -// } +// todo!(this is fake) +#[derive(Clone)] +struct UniformListState; -// fn select_prev(&mut self, project: Option<&Model>, 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(); -// } +// todo!(this is fake) +impl UniformListState { + pub fn scroll_to(&mut self, target: ScrollTarget) {} +} -// fn select_next(&mut self, project: Option<&Model>, 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(); -// } +// todo!(this is somewhat fake) +#[derive(Debug)] +pub enum ScrollTarget { + Show(usize), + Center(usize), +} -// fn select_last(&mut self, project: Option<&Model>, 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(); -// } +impl CompletionsMenu { + fn select_first(&mut self, project: Option<&Model>, 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 pre_resolve_completion_documentation( -// &self, -// project: Option>, -// cx: &mut ViewContext, -// ) { -// let settings = settings::get::(cx); -// if !settings.show_completion_documentation { -// return; -// } + fn select_prev(&mut self, project: Option<&Model>, 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(); + } -// 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; -// } + fn select_next(&mut self, project: Option<&Model>, 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(); + } -// 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; + fn select_last(&mut self, project: Option<&Model>, 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(); + } -// _ = 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; -// } + fn pre_resolve_completion_documentation( + &self, + project: Option>, + cx: &mut ViewContext, + ) { + todo!("implementation below "); + } + // ) { + // let settings = EditorSettings::get_global(cx); + // if !settings.show_completion_documentation { + // return; + // } -// 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; + // let Some(project) = project else { + // return; + // }; + // let client = project.read(cx).client(); + // let language_registry = project.read(cx).languages().clone(); -// _ = this.update(&mut cx, |_, cx| cx.notify()); -// } -// } -// }) -// .detach(); -// } + // let is_remote = project.read(cx).is_remote(); + // let project_id = project.read(cx).remote_id(); -// fn attempt_resolve_selected_completion_documentation( -// &mut self, -// project: Option<&Model>, -// cx: &mut ViewContext, -// ) { -// let settings = settings::get::(cx); -// if !settings.show_completion_documentation { -// return; -// } + // let completions = self.completions.clone(); + // let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect(); -// let completion_index = self.matches[self.selected_item].candidate_id; -// let Some(project) = project else { -// return; -// }; -// let language_registry = project.read(cx).languages().clone(); + // 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; + // }; -// let completions = self.completions.clone(); -// let completions_guard = completions.read(); -// let completion = &completions_guard[completion_index]; -// if completion.documentation.is_some() { -// 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); + // 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; -// if project.read(cx).is_remote() { -// let Some(project_id) = project.read(cx).remote_id() else { -// log::error!("Remote project without remote_id"); -// return; -// }; + // _ = 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 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; -// }; + // let server_id = completion.server_id; + // let completion = completion.lsp_completion.clone(); + // drop(completions_guard); -// 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(); -// } -// } + // let server = project.read_with(&mut cx, |project, _| { + // project.language_server_for_id(server_id) + // }); + // let Some(server) = server else { + // return; + // }; -// 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(), -// }; + // Self::resolve_completion_documentation_local( + // server, + // completions.clone(), + // completion_index, + // completion, + // language_registry.clone(), + // ) + // .await; -// let Some(response) = client -// .request(request) -// .await -// .context("completion documentation resolve proto request") -// .log_err() -// else { -// return; -// }; + // _ = this.update(&mut cx, |_, cx| cx.notify()); + // } + // } + // }) + // .detach(); + // } -// if response.text.is_empty() { -// let mut completions = completions.write(); -// let completion = &mut completions[completion_index]; -// completion.documentation = Some(Documentation::Undocumented); -// } + fn attempt_resolve_selected_completion_documentation( + &mut self, + project: Option<&Model>, + cx: &mut ViewContext, + ) { + let settings = EditorSettings::get_global(cx); + if !settings.show_completion_documentation { + return; + } -// 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 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]; + // todo!() + // 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(); + } + } -// let mut completions = completions.write(); -// let completion = &mut completions[completion_index]; -// completion.documentation = Some(documentation); -// } + 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, + ) { + todo!() + // 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; -// } + async fn resolve_completion_documentation_local( + server: Arc, + completions: Arc>>, + completion_index: usize, + completion: lsp::CompletionItem, + language_registry: Arc, + ) { + todo!() + // 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); + // } + } -// let request = server.request::(completion); -// let Some(completion_item) = request.await.log_err() else { -// return; -// }; + fn visible(&self) -> bool { + !self.matches.is_empty() + } -// 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 render( + &self, + style: EditorStyle, + workspace: Option>, + cx: &mut ViewContext, + ) { + todo!("old implementation below") + } + // ) -> AnyElement { + // enum CompletionTag {} + + // let settings = EditorSettings>(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(); + // } + // } -// fn visible(&self) -> bool { -// !self.matches.is_empty() -// } + // len + // }) + // .map(|(ix, _)| ix); -// 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(); -// } -// } + // let completions = self.completions.clone(); + // let matches = self.matches.clone(); + // let selected_item = self.selected_item; -// 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() -// } + // 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(); -// 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() -// }; + // 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]; -// // 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 documentation = if show_completion_documentation { + // &completion.documentation + // } else { + // &None + // }; -// 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); + // 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 + // }; -// 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; -// } -// } + // 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, + // ), + // ); -// self.matches = matches.into(); -// self.selected_item = 0; -// } -// } + // 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: BackgroundExecutor) { + 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: Model, selected_item: usize, - // list: UniformListState, + 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() -// } +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_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_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 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 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(); + fn render( + &self, + mut cursor_position: DisplayPoint, + style: EditorStyle, + cx: &mut ViewContext, + ) -> (DisplayPoint, AnyElement) { + todo!("old version below") + } + // 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 + // }; -// if self.deployed_from_indicator { -// *cursor_position.column_mut() = 0; -// } + // 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(); -// (cursor_position, element) -// } -// } + // if self.deployed_from_indicator { + // *cursor_position.column_mut() = 0; + // } + + // (cursor_position, element) + // } +} pub struct CopilotState { excerpt_id: Option, @@ -2018,20 +2051,21 @@ impl Editor { // 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 snapshot(&mut self, cx: &mut WindowContext) -> EditorSnapshot { + todo!() + // 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, @@ -2854,7 +2888,7 @@ impl Editor { // 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 !brace_inserted && EditorSettings>(cx).use_on_type_format { // if let Some(on_type_format_task) = // this.trigger_on_type_formatting(text.to_string(), cx) // { @@ -3173,7 +3207,7 @@ impl Editor { // } // fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext) { - // if !settings::get::(cx).show_completions_on_input { + // if !EditorSettings>(cx).show_completions_on_input { // return; // } @@ -3377,16 +3411,16 @@ impl Editor { // } // } - // 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() - // } + 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, @@ -7856,40 +7890,40 @@ impl Editor { // Some(self.perform_format(project, FormatTrigger::Manual, cx)) // } - // fn perform_format( - // &mut self, - // project: Model, - // 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); - // } - // } + fn perform_format( + &mut self, + project: Model, + trigger: FormatTrigger, + cx: &mut ViewContext, + ) -> Task> { + let buffer = self.buffer().clone(); + let buffers = buffer.read(cx).all_buffers(); + + let mut timeout = cx.background_executor().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(); - // }); + cx.notify(); + }); - // Ok(()) - // }) - // } + Ok(()) + }) + } // fn restart_language_server(&mut self, _: &RestartLanguageServer, cx: &mut ViewContext) { // if let Some(project) = self.project.clone() { @@ -8162,42 +8196,42 @@ impl Editor { // 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)); + 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); - // } + if auto_scroll { + self.request_autoscroll(Autoscroll::fit(), cx); + } - // cx.notify(); - // } - // } + 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); - // } + 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(); - // } - // } + cx.notify(); + } + } // pub fn gutter_hover( // &mut self, @@ -8900,59 +8934,61 @@ impl Editor { // 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; + #[cfg(any(test, feature = "test-support"))] + fn report_editor_event( + &self, + _operation: &'static str, + _file_extension: Option, + _cx: &AppContext, + ) { + } - // 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) - // } + #[cfg(not(any(test, feature = "test-support")))] + fn report_editor_event( + &self, + operation: &'static str, + file_extension: Option, + cx: &AppContext, + ) { + todo!("old version below"); + } + // 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. @@ -9190,7 +9226,7 @@ impl EditorSnapshot { self.placeholder_text.as_ref() } - pub fn scroll_position(&self) -> gpui::Point { + pub fn scroll_position(&self) -> gpui::Point { self.scroll_anchor.scroll_position(&self.display_snapshot) } } diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index dc0b9f6751216618f4c6a46f4d905e479e53a2cc..6f36cf8e0648446d60d9f3092d80942fea0ae772 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -5,10 +5,13 @@ use crate::{ display_map::{BlockStyle, DisplaySnapshot}, EditorStyle, }; +use anyhow::Result; use gpui::{ - px, relative, AnyElement, Bounds, Element, Hsla, Line, Pixels, Size, Style, TextRun, TextSystem, + black, px, relative, AnyElement, Bounds, Element, Hsla, Line, Pixels, Size, Style, TextRun, + TextSystem, }; use language::{CursorShape, Selection}; +use smallvec::SmallVec; use std::{ops::Range, sync::Arc}; use sum_tree::Bias; @@ -2700,16 +2703,16 @@ impl PositionMap { let position = position - text_bounds.origin; let y = position.y.max(px(0.)).min(self.size.width); let x = position.x + (scroll_position.x * self.em_width); - let row = (y / self.line_height + scroll_position.y).into(); + let row = (f32::from(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.into()) - .map(|line_with_spaces| &line_with_spaces.line) + .map(|LineWithInvisibles { line, .. }| line) { if let Some(ix) = line.index_for_x(x) { - (ix as u32, 0.0) + (ix as u32, px(0.)) } else { - (line.len() as u32, px(0.).max(x - line.width())) + (line.len as u32, px(0.).max(x - line.width())) } } else { (0, x) @@ -2719,7 +2722,7 @@ impl PositionMap { 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; + let column_overshoot_after_line_end = (x_overshoot_after_line_end / self.em_advance).into(); *exact_unclipped.column_mut() += column_overshoot_after_line_end; PointForPosition { previous_valid, @@ -2740,8 +2743,9 @@ fn layout_line( row: u32, snapshot: &EditorSnapshot, style: &EditorStyle, + rem_size: Pixels, text_system: &TextSystem, -) -> Line { +) -> Result> { let mut line = snapshot.line(row); if line.len() > MAX_LINE_LEN { @@ -2753,15 +2757,16 @@ fn layout_line( line.truncate(len); } - text_system.layout_str( + text_system.layout_text( &line, - style.text.font_size, + style.text.font_size * rem_size, &[TextRun { len: snapshot.line_len(row) as usize, - font: style.text.font.clone(), - color: Hsla::black(), + font: style.text.font(), + color: black(), underline: Default::default(), }], + None, ) } diff --git a/crates/editor2/src/highlight_matching_bracket.rs b/crates/editor2/src/highlight_matching_bracket.rs index e0fc2c0d00a3d15ce3d88c0ac49482aebe19e1da..5074ee4eda495f2f71614c4e7ade328d1a0a6018 100644 --- a/crates/editor2/src/highlight_matching_bracket.rs +++ b/crates/editor2/src/highlight_matching_bracket.rs @@ -5,29 +5,30 @@ use crate::{Editor, RangeToAnchorExt}; enum MatchingBracketHighlight {} pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewContext) { - editor.clear_background_highlights::(cx); + todo!() + // // 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 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, - ) - } + // 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)] diff --git a/crates/editor2/src/hover_popover.rs b/crates/editor2/src/hover_popover.rs index b17c5df28ff5035a6b02790b18e0a687f353095f..9eddd259af022d03da94a0ceec83b052c1ef684c 100644 --- a/crates/editor2/src/hover_popover.rs +++ b/crates/editor2/src/hover_popover.rs @@ -1,17 +1,14 @@ use crate::{ - display_map::{InlayOffset, ToDisplayPoint}, + display_map::InlayOffset, link_go_to_definition::{InlayHighlight, RangeInEditor}, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle, ExcerptId, RangeToAnchorExt, }; use futures::FutureExt; -use gpui::{ - AnyElement, AppContext, CursorStyle, Element, Model, MouseButton, Task, ViewContext, WeakView, -}; -use language::{ - markdown, Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, ParsedMarkdown, -}; +use gpui::{AnyElement, AppContext, Model, Task, ViewContext, WeakView}; +use language::{markdown, Bias, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown}; use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; +use settings::Settings; use std::{ops::Range, sync::Arc, time::Duration}; use util::TryFutureExt; use workspace::Workspace; @@ -77,63 +74,64 @@ pub fn find_hovered_hint_part( } pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext) { - if EditorSettings::get_global(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); - } + todo!() + // if EditorSettings::get_global(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_executor() + // .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. @@ -146,7 +144,8 @@ pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext) -> bool { editor.hover_state.info_task = None; editor.hover_state.triggered_from = None; - editor.clear_background_highlights::(cx); + // todo!() + // editor.clear_background_highlights::(cx); if did_hide { cx.notify(); @@ -237,11 +236,11 @@ fn show_hover( let delay = if !ignore_timeout { // Construct delay task to wait for later let total_delay = Some( - cx.background() + cx.background_executor() .timer(Duration::from_millis(HOVER_DELAY_MILLIS)), ); - cx.background() + cx.background_executor() .timer(Duration::from_millis(HOVER_REQUEST_DELAY_MILLIS)) .await; total_delay @@ -250,11 +249,11 @@ fn show_hover( }; // query the LSP for hover info - let hover_request = cx.update(|cx| { + let hover_request = cx.update(|_, cx| { project.update(cx, |project, cx| { project.hover(&buffer, buffer_position, cx) }) - }); + })?; if let Some(delay) = delay { delay.await; @@ -308,7 +307,8 @@ fn show_hover( anchor..anchor }; - let language_registry = project.update(&mut cx, |p, _| p.languages().clone()); + 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; @@ -325,22 +325,23 @@ fn show_hover( }; 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(); + todo!(); + // 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>(()) @@ -424,38 +425,40 @@ impl HoverState { 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)) + todo!("old version below") } + // // 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)] diff --git a/crates/editor2/src/inlay_hint_cache.rs b/crates/editor2/src/inlay_hint_cache.rs index 74d6092ffa9eda57bef5425c258862ea1358b719..addd3bf3acb74a11b2eaab5cec44fe9a2a68f495 100644 --- a/crates/editor2/src/inlay_hint_cache.rs +++ b/crates/editor2/src/inlay_hint_cache.rs @@ -553,17 +553,18 @@ impl InlayHintCache { 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; - } - } - } + todo!() + // 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; + // } + // } + // } })?; } @@ -584,89 +585,91 @@ fn spawn_new_update_tasks( 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, - }; + todo!("old version below"); +} +// 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 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, - ) - }; +// 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), - )); - } - } - } -} +// 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 { @@ -762,78 +765,79 @@ fn new_update_task( 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); - } - } - }) + todo!() + // 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( @@ -1073,126 +1077,128 @@ fn apply_hint_update( 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), - }; + todo!("old implementation commented below") +} +// 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; +// } +// } - 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); +// 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)); - } +// 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) - } -} +// 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 { diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index f01e6ab2b30a18e5fd0cdfc3c667526bc3d47740..717cdc08a2a721e8a656d8e672dfe8631903ee1d 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -7,7 +7,7 @@ use anyhow::{Context, Result}; use collections::HashSet; use futures::future::try_join_all; use gpui::{ - point, AnyElement, AppContext, AsyncAppContext, Entity, Model, Pixels, SharedString, + point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, Model, Pixels, SharedString, Subscription, Task, View, ViewContext, WeakView, }; use language::{ @@ -26,6 +26,7 @@ use std::{ sync::Arc, }; use text::Selection; +use theme::ThemeVariant; use util::{paths::PathExt, ResultExt, TryFutureExt}; use workspace::item::{BreadcrumbText, FollowableItemHandle}; use workspace::{ @@ -306,12 +307,15 @@ impl FollowableItem for Editor { } } -// async fn update_editor_from_message( -// this: WeakView, -// project: Model, -// message: proto::update_view::Editor, -// cx: &mut AsyncAppContext, -// ) -> Result<()> { +async fn update_editor_from_message( + this: WeakView, + project: Model, + message: proto::update_view::Editor, + cx: &mut AsyncAppContext, +) -> Result<()> { + todo!() +} +// Previous implementation of the above // // Open all of the buffers of which excerpts were added to the editor. // let inserted_excerpt_buffer_ids = message // .inserted_excerpts @@ -512,38 +516,39 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor) 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 - } + todo!(); + // 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 { @@ -563,8 +568,8 @@ impl Item for Editor { 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()), + Cow::Borrowed(path) => Some(path.to_string_lossy().into()), + Cow::Owned(path) => Some(path.to_string_lossy().to_string().into()), } } @@ -590,10 +595,14 @@ impl Item for Editor { // .into_any() } - fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) { + fn for_each_project_item( + &self, + cx: &AppContext, + f: &mut dyn FnMut(EntityId, &dyn project::Item), + ) { self.buffer .read(cx) - .for_each_buffer(|buffer| f(buffer.id(), buffer.read(cx))); + .for_each_buffer(|buffer| f(buffer.entity_id(), buffer.read(cx))); } fn is_singleton(&self, cx: &AppContext) -> bool { @@ -652,20 +661,24 @@ impl Item for Editor { if buffers.len() == 1 { project - .update(&mut cx, |project, cx| project.save_buffers(buffers, cx)) + .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()) + buffer + .update(&mut cx, |buffer, _| { + buffer.is_dirty() || buffer.has_conflict() + }) + .unwrap_or(false) }); project .update(&mut cx, |project, cx| { project.save_buffers(dirty_buffers, cx) - }) + })? .await?; for buffer in clean_buffers { buffer.update(&mut cx, |buffer, cx| { @@ -760,7 +773,7 @@ impl Item for Editor { ToolbarItemLocation::PrimaryLeft { flex: None } } - fn breadcrumbs(&self, cx: &AppContext) -> Option> { + fn breadcrumbs(&self, variant: &ThemeVariant, cx: &AppContext) -> Option> { todo!(); // let cursor = self.selections.newest_anchor().head(); // let multibuffer = &self.buffer().read(cx); @@ -806,7 +819,7 @@ impl Item for Editor { if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) { let path = file.abs_path(cx); - cx.background() + cx.background_executor() .spawn(async move { DB.save_path(item_id, workspace_id, path.clone()) .await @@ -913,15 +926,17 @@ impl SearchableItem for Editor { } fn clear_matches(&mut self, cx: &mut ViewContext) { - self.clear_background_highlights::(cx); + todo!() + // 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, - ); + todo!() + // self.highlight_background::( + // matches, + // |theme| theme.search.match_background, + // cx, + // ); } fn query_suggestion(&mut self, cx: &mut ViewContext) -> String { @@ -952,20 +967,22 @@ impl SearchableItem for Editor { 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]); - }) + todo!() + // 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)); + todo!() + // 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, @@ -1044,7 +1061,7 @@ impl SearchableItem for Editor { cx: &mut ViewContext, ) -> Task>> { let buffer = self.buffer().read(cx).snapshot(cx); - cx.background().spawn(async move { + cx.background_executor().spawn(async move { let mut ranges = Vec::new(); if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() { ranges.extend( diff --git a/crates/editor2/src/link_go_to_definition.rs b/crates/editor2/src/link_go_to_definition.rs index cdbab3b3fc8bfa9c825a61b9e1a30be6eb7089bc..da2b49def63f6a259597474da50c09f8ee3b6083 100644 --- a/crates/editor2/src/link_go_to_definition.rs +++ b/crates/editor2/src/link_go_to_definition.rs @@ -170,170 +170,173 @@ pub fn update_inlay_link_and_hover_points( 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); - } + todo!("old implementation below") } +// ) { +// 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 { @@ -570,34 +573,35 @@ fn go_to_fetched_definition_of_kind( 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), - } - } - } + todo!(); + // 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)] diff --git a/crates/editor2/src/mouse_context_menu.rs b/crates/editor2/src/mouse_context_menu.rs index 84c670c79d1b0297de88ccda35cd86660f8c77df..97787c3d39ce17aab771af2a81135e20dfc4cd72 100644 --- a/crates/editor2/src/mouse_context_menu.rs +++ b/crates/editor2/src/mouse_context_menu.rs @@ -8,27 +8,28 @@ pub fn deploy_context_menu( point: DisplayPoint, cx: &mut ViewContext, ) { - if !editor.focused { - cx.focus_self(); - } + todo!(); - // Don't show context menu for inline editors - if editor.mode() != EditorMode::Full { - return; - } + // if !editor.focused { + // cx.focus_self(); + // } - // Don't show the context menu if there isn't a project associated with this editor - if editor.project.is_none() { - return; - } + // // Don't show context menu for inline editors + // if editor.mode() != EditorMode::Full { + // 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); - }); + // // 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); + // }); - // todo!() // editor.mouse_context_menu.update(cx, |menu, cx| { // menu.show( // position, @@ -50,7 +51,7 @@ pub fn deploy_context_menu( // cx, // ); // }); - cx.notify(); + // cx.notify(); } // #[cfg(test)] diff --git a/crates/editor2/src/persistence.rs b/crates/editor2/src/persistence.rs index 6e37735c1371ff466e345c95d0fa92d6d3c892a6..c1c14550142f8583869edc0a7f4679b84b4ea131 100644 --- a/crates/editor2/src/persistence.rs +++ b/crates/editor2/src/persistence.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; use db::sqlez_macros::sql; use db::{define_connection, query}; +use gpui::EntityId; use workspace::{ItemId, WorkspaceDb, WorkspaceId}; define_connection!( @@ -66,7 +67,7 @@ impl EditorDb { query! { pub async fn save_scroll_position( - item_id: ItemId, + item_id: EntityId, workspace_id: WorkspaceId, top_row: u32, vertical_offset: f32, diff --git a/crates/editor2/src/scroll.rs b/crates/editor2/src/scroll.rs index 2abf80a747773b510b088d40fa16bc3221098a69..3c2a25dd9332b1dbb3bbff1fd72bb532e68d0063 100644 --- a/crates/editor2/src/scroll.rs +++ b/crates/editor2/src/scroll.rs @@ -9,7 +9,7 @@ use crate::{ Anchor, DisplayPoint, Editor, EditorMode, Event, InlayHintRefreshReason, MultiBufferSnapshot, ToPoint, }; -use gpui::{point, px, AppContext, Pixels, Styled, Task, ViewContext}; +use gpui::{point, px, AppContext, Entity, Pixels, Styled, Task, ViewContext}; use language::{Bias, Point}; use std::{ cmp::Ordering, @@ -39,18 +39,18 @@ pub struct ScrollAnchor { impl ScrollAnchor { fn new() -> Self { Self { - offset: gpui::Point::zero(), + offset: gpui::Point::default(), anchor: Anchor::min(), } } - pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point { + pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point { 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); + scroll_position.y = scroll_top + scroll_position.y; } else { - scroll_position.set_y(0.); + scroll_position.y = 0.; } scroll_position } @@ -133,7 +133,7 @@ pub struct ScrollManager { anchor: ScrollAnchor, ongoing: OngoingScroll, autoscroll_request: Option<(Autoscroll, bool)>, - last_autoscroll: Option<(gpui::Point, f32, f32, AutoscrollStrategy)>, + last_autoscroll: Option<(gpui::Point, f32, f32, AutoscrollStrategy)>, show_scrollbars: bool, hide_scrollbar_task: Option>, visible_line_count: Option, @@ -171,7 +171,7 @@ impl ScrollManager { self.ongoing.axis = axis; } - pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point { + pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point { self.anchor.scroll_position(snapshot) } @@ -188,7 +188,7 @@ impl ScrollManager { ( ScrollAnchor { anchor: Anchor::min(), - offset: scroll_position.max(Point::zero()), + offset: scroll_position.max(&gpui::Point::default()), }, 0, ) @@ -228,9 +228,9 @@ impl ScrollManager { self.show_scrollbar(cx); self.autoscroll_request.take(); if let Some(workspace_id) = workspace_id { - let item_id = cx.view_id(); + let item_id = cx.view().entity_id(); - cx.background() + cx.foreground_executor() .spawn(async move { DB.save_scroll_position( item_id, @@ -255,7 +255,9 @@ impl ScrollManager { if cx.default_global::().0 { self.hide_scrollbar_task = Some(cx.spawn(|editor, mut cx| async move { - cx.background().timer(SCROLLBAR_SHOW_INTERVAL).await; + cx.background_executor() + .timer(SCROLLBAR_SHOW_INTERVAL) + .await; editor .update(&mut cx, |editor, cx| { editor.scroll_manager.show_scrollbars = false; @@ -287,160 +289,161 @@ impl ScrollManager { } // todo!() -// 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: gpui::Point, -// cx: &mut ViewContext, -// ) { -// self.set_scroll_position_internal(scroll_position, true, false, cx); -// } - -// pub(crate) fn set_scroll_position_internal( -// &mut self, -// scroll_position: gpui::Point, -// 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) -> gpui::Point { -// 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 + point(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: Point::new(x, y), -// anchor: top_anchor, -// }; -// self.set_scroll_anchor(scroll_anchor, cx); -// } -// } -// } +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: gpui::Point, + cx: &mut ViewContext, + ) { + self.set_scroll_position_internal(scroll_position, true, false, cx); + } + + pub(crate) fn set_scroll_position_internal( + &mut self, + scroll_position: gpui::Point, + 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, + ); + + // todo!() + // self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); + } + + // pub fn scroll_position(&self, cx: &mut ViewContext) -> gpui::Point { + // 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 + point(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: Point::new(x, y), + // anchor: top_anchor, + // }; + // self.set_scroll_anchor(scroll_anchor, cx); + // } + // } +} diff --git a/crates/editor2/src/scroll/autoscroll.rs b/crates/editor2/src/scroll/autoscroll.rs index 5816e5683baa733629e65550b6dcf1f4913ce808..a4c37a258e982b6099611ac548f4802b3437b897 100644 --- a/crates/editor2/src/scroll/autoscroll.rs +++ b/crates/editor2/src/scroll/autoscroll.rs @@ -1,6 +1,6 @@ -use std::cmp; +use std::{cmp, f32}; -use gpui::ViewContext; +use gpui::{px, Pixels, ViewContext}; use language::Point; use crate::{display_map::ToDisplayPoint, Editor, EditorMode, LineWithInvisibles}; @@ -61,7 +61,7 @@ impl Editor { display_map.max_point().row() as f32 }; if scroll_position.y > max_scroll_top { - scroll_position.set_y(max_scroll_top); + scroll_position.y = (max_scroll_top); self.set_scroll_position(scroll_position, cx); } @@ -143,24 +143,24 @@ impl Editor { let needs_scroll_down = target_bottom >= end_row; if needs_scroll_up && !needs_scroll_down { - scroll_position.set_y(target_top); + scroll_position.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); + scroll_position.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)); + scroll_position.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)); + scroll_position.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)); + scroll_position.y = ((target_bottom - visible_lines).max(0.0)); self.set_scroll_position_internal(scroll_position, local, true, cx); } } @@ -178,9 +178,9 @@ impl Editor { pub fn autoscroll_horizontally( &mut self, start_row: u32, - viewport_width: f32, - scroll_width: f32, - max_glyph_width: f32, + viewport_width: Pixels, + scroll_width: Pixels, + max_glyph_width: Pixels, layouts: &[LineWithInvisibles], cx: &mut ViewContext, ) -> bool { @@ -191,11 +191,11 @@ impl Editor { let mut target_right; if self.highlighted_rows.is_some() { - target_left = 0.0_f32; - target_right = 0.0_f32; + target_left = px(0.); + target_right = px(0.); } else { - target_left = std::f32::INFINITY; - target_right = 0.0_f32; + target_left = px(f32::INFINITY); + target_right = px(0.); 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 { @@ -226,11 +226,11 @@ impl Editor { let scroll_right = scroll_left + viewport_width; if target_left < scroll_left { - self.scroll_manager.anchor.offset.x = (target_left / max_glyph_width); + self.scroll_manager.anchor.offset.x = (target_left / max_glyph_width).into(); true } else if target_right > scroll_right { self.scroll_manager.anchor.offset.x = - ((target_right - viewport_width) / max_glyph_width); + ((target_right - viewport_width) / max_glyph_width).into(); true } else { false diff --git a/crates/editor2/src/scroll/scroll_amount.rs b/crates/editor2/src/scroll/scroll_amount.rs index 2cb22d15163323eae5f396e2415b973d099aae74..89d188e324e6aa83cc2bfab3fd0444133636f27b 100644 --- a/crates/editor2/src/scroll/scroll_amount.rs +++ b/crates/editor2/src/scroll/scroll_amount.rs @@ -11,18 +11,19 @@ pub enum ScrollAmount { 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.), - } + todo!() + // 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 index 0d10db7af9983b4c8c300d088e0f08a5a0f2e2cc..2762fd369ea2bf16e687fb61a41d4811973b86fb 100644 --- a/crates/editor2/src/selections_collection.rs +++ b/crates/editor2/src/selections_collection.rs @@ -6,7 +6,7 @@ use std::{ }; use collections::HashMap; -use gpui::{AppContext, Model}; +use gpui::{AppContext, Model, Pixels}; use itertools::Itertools; use language::{Bias, Point, Selection, SelectionGoal, TextDimension, ToPoint}; use util::post_inc; @@ -302,39 +302,39 @@ impl SelectionsCollection { .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 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, diff --git a/crates/gpui2/src/executor.rs b/crates/gpui2/src/executor.rs index 018f575d2d8d6e9c34368d28638df42415dcb4c1..b6fd5b23180bed5bf852ae99e79a6cb071ac7174 100644 --- a/crates/gpui2/src/executor.rs +++ b/crates/gpui2/src/executor.rs @@ -29,6 +29,7 @@ pub struct ForegroundExecutor { } #[must_use] +#[derive(Debug)] pub enum Task { Ready(Option), Spawned(async_task::Task), diff --git a/crates/gpui2/src/geometry.rs b/crates/gpui2/src/geometry.rs index 468bc1e5b76809a3c6ce88dbfe2a0b9d79045518..b2fad4efda9e127ce74c319ac2471cf24798a247 100644 --- a/crates/gpui2/src/geometry.rs +++ b/crates/gpui2/src/geometry.rs @@ -819,6 +819,18 @@ impl From for f64 { } } +impl From for u32 { + fn from(pixels: Pixels) -> Self { + pixels.0 as u32 + } +} + +impl From for usize { + fn from(pixels: Pixels) -> Self { + pixels.0 as usize + } +} + #[derive( Add, AddAssign, Clone, Copy, Default, Div, Eq, Hash, Ord, PartialEq, PartialOrd, Sub, SubAssign, )] diff --git a/crates/gpui2/src/platform/mac/text_system.rs b/crates/gpui2/src/platform/mac/text_system.rs index a4c56c3523f69f09740b1fdf41a3a8ff5e9da6a1..b87db09dc0ac16bee9e76bcf92e00b21761c1143 100644 --- a/crates/gpui2/src/platform/mac/text_system.rs +++ b/crates/gpui2/src/platform/mac/text_system.rs @@ -411,6 +411,7 @@ impl MacTextSystemState { descent: typographic_bounds.descent.into(), runs, font_size, + len: text.len(), } } diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 2544989ebc164d94e52b94bd43b7c3ed5a95c140..b30d4aa00250f981f48301f6e3c8007b55b1bb36 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -167,6 +167,15 @@ impl TextStyle { Ok(self) } + pub fn font(&self) -> Font { + Font { + family: self.font_family.clone(), + features: self.font_features.clone(), + weight: self.font_weight, + style: self.font_style, + } + } + pub fn to_run(&self, len: usize) -> TextRun { TextRun { len, diff --git a/crates/gpui2/src/text_system.rs b/crates/gpui2/src/text_system.rs index ee8c65386691434a6d6fc1728d98cc97c9f01a49..dd0689396e2629985dbedb55bbcb3a72345bd39c 100644 --- a/crates/gpui2/src/text_system.rs +++ b/crates/gpui2/src/text_system.rs @@ -151,7 +151,7 @@ impl TextSystem { pub fn layout_text( &self, - text: &SharedString, + text: &str, font_size: Pixels, runs: &[TextRun], wrap_width: Option, diff --git a/crates/gpui2/src/text_system/line.rs b/crates/gpui2/src/text_system/line.rs index ad70a79bb1e411fdaa4b0fddc0de39bdbf96e7af..63aac96faff9be6f3a452ecf034f501952a4285f 100644 --- a/crates/gpui2/src/text_system/line.rs +++ b/crates/gpui2/src/text_system/line.rs @@ -29,6 +29,10 @@ impl Line { ) } + pub fn width(&self) -> Pixels { + self.layout.width + } + pub fn wrap_count(&self) -> usize { self.layout.wrap_boundaries.len() } diff --git a/crates/gpui2/src/text_system/line_layout.rs b/crates/gpui2/src/text_system/line_layout.rs index 7682aaa1b818cac6ed24a3f3d6c02cb2ec490b55..a310c32cd1f369ed5d6ef3e579ef45c1fdd0d4be 100644 --- a/crates/gpui2/src/text_system/line_layout.rs +++ b/crates/gpui2/src/text_system/line_layout.rs @@ -16,6 +16,7 @@ pub struct LineLayout { pub ascent: Pixels, pub descent: Pixels, pub runs: Vec, + pub len: usize, } #[derive(Debug)] diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index cebf546217e22d3ae7e562cc623a841e69fe51b4..8f9538001de6536f9c487edfaed02244f804b137 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1655,6 +1655,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { } } + // todo!("change this to return a reference"); pub fn view(&self) -> View { self.view.clone() } diff --git a/crates/text/Cargo.toml b/crates/text/Cargo.toml index d1bc6cc8f8e6826c0f50f3f2fa15b28fe3ca8161..5a21500b2ff521182833dadb09ed7b1f8bfc6f8f 100644 --- a/crates/text/Cargo.toml +++ b/crates/text/Cargo.toml @@ -30,7 +30,7 @@ regex.workspace = true [dev-dependencies] collections = { path = "../collections", features = ["test-support"] } -gpui = { path = "../gpui", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } ctor.workspace = true env_logger.workspace = true diff --git a/crates/text2/src/selection.rs b/crates/text2/src/selection.rs index 480cb99d747783b7c7bfc100af8b57401781a984..4bf205dd5d6e379172f7411b97fea3bcecdf3299 100644 --- a/crates/text2/src/selection.rs +++ b/crates/text2/src/selection.rs @@ -1,3 +1,5 @@ +use gpui::Pixels; + use crate::{Anchor, BufferSnapshot, TextDimension}; use std::cmp::Ordering; use std::ops::Range; @@ -5,8 +7,8 @@ use std::ops::Range; #[derive(Copy, Clone, Debug, PartialEq)] pub enum SelectionGoal { None, - HorizontalPosition(f32), - HorizontalRange { start: f32, end: f32 }, + HorizontalPosition(Pixels), + HorizontalRange { start: Pixels, end: Pixels }, WrappedHorizontalPosition((u32, f32)), } diff --git a/crates/workspace2/src/item.rs b/crates/workspace2/src/item.rs index 309cf96615774a6b7d10e1e83941e561ac2c2f47..90ebd16f2a03c7e072615091df957fca9255f712 100644 --- a/crates/workspace2/src/item.rs +++ b/crates/workspace2/src/item.rs @@ -104,7 +104,11 @@ pub trait Item: Render + EventEmitter { } fn tab_content(&self, detail: Option, cx: &AppContext) -> AnyElement; - fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project2::Item)) { + fn for_each_project_item( + &self, + _: &AppContext, + _: &mut dyn FnMut(EntityId, &dyn project2::Item), + ) { } // (model id, Item) fn is_singleton(&self, _cx: &AppContext) -> bool { false @@ -219,8 +223,12 @@ pub trait ItemHandle: 'static + Send { fn dragged_tab_content(&self, detail: Option, cx: &AppContext) -> AnyElement; fn project_path(&self, cx: &AppContext) -> Option; fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>; - fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[usize; 3]>; - fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project2::Item)); + fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[EntityId; 3]>; + fn for_each_project_item( + &self, + _: &AppContext, + _: &mut dyn FnMut(EntityId, &dyn project2::Item), + ); fn is_singleton(&self, cx: &AppContext) -> bool; fn boxed_clone(&self) -> Box; fn clone_on_split( @@ -331,7 +339,7 @@ impl ItemHandle for View { result } - fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[usize; 3]> { + fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[EntityId; 3]> { let mut result = SmallVec::new(); self.read(cx).for_each_project_item(cx, &mut |id, _| { result.push(id); @@ -342,7 +350,7 @@ impl ItemHandle for View { fn for_each_project_item( &self, cx: &AppContext, - f: &mut dyn FnMut(usize, &dyn project2::Item), + f: &mut dyn FnMut(EntityId, &dyn project2::Item), ) { self.read(cx).for_each_project_item(cx, f) } From fbee6b5352c294fac46face17b7fd2927a6918a7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 2 Nov 2023 23:35:20 -0600 Subject: [PATCH 09/23] Get editor2 compiling with a ton of code commented out --- crates/editor2/src/display_map.rs | 4 +- crates/editor2/src/display_map/fold_map.rs | 3 +- crates/editor2/src/display_map/inlay_map.rs | 3 +- crates/editor2/src/display_map/wrap_map.rs | 26 +- crates/editor2/src/editor.rs | 434 +++++++++--------- crates/editor2/src/element.rs | 5 +- crates/editor2/src/items.rs | 34 +- crates/editor2/src/link_go_to_definition.rs | 475 ++++++++++---------- crates/editor2/src/movement.rs | 27 +- crates/editor2/src/persistence.rs | 3 +- crates/editor2/src/scroll.rs | 92 ++-- crates/text2/src/selection.rs | 6 +- 12 files changed, 571 insertions(+), 541 deletions(-) diff --git a/crates/editor2/src/display_map.rs b/crates/editor2/src/display_map.rs index 3cf4d7340f0657cffd1667f7ff4d6e560f0e9af0..2955a929f8eacd8c956462627b338eaa19159864 100644 --- a/crates/editor2/src/display_map.rs +++ b/crates/editor2/src/display_map.rs @@ -241,7 +241,7 @@ impl DisplayMap { pub fn set_font(&self, font: Font, font_size: Pixels, cx: &mut ModelContext) -> bool { self.wrap_map - .update(cx, |map, cx| map.set_font(font, font_size, cx)) + .update(cx, |map, cx| map.set_font_with_size(font, font_size, cx)) } pub fn set_fold_ellipses_color(&mut self, color: Hsla) -> bool { @@ -1388,7 +1388,7 @@ pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterat // ); // // Re-wrap on font size changes -// map.update(cx, |map, cx| map.set_font(font_id, font_size + 3., cx)); +// map.update(cx, |map, cx| map.set_font_with_size(font_id, font_size + 3., cx)); // let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); // assert_eq!( diff --git a/crates/editor2/src/display_map/fold_map.rs b/crates/editor2/src/display_map/fold_map.rs index 61047043dffbcae84aa418dbc1eda0c8ccd9e8bd..88cd202b0801a4ca875122cece06c1c7ba0ab5a0 100644 --- a/crates/editor2/src/display_map/fold_map.rs +++ b/crates/editor2/src/display_map/fold_map.rs @@ -1629,7 +1629,8 @@ mod tests { } fn init_test(cx: &mut gpui::AppContext) { - cx.set_global(SettingsStore::test(cx)); + let store = SettingsStore::test(cx); + cx.set_global(store); } impl FoldMap { diff --git a/crates/editor2/src/display_map/inlay_map.rs b/crates/editor2/src/display_map/inlay_map.rs index 0afed4028d117708825080b2ff1bc479c2b0fa3b..a6bc6343f4fc58e7fc7240fc186845edbdb6a115 100644 --- a/crates/editor2/src/display_map/inlay_map.rs +++ b/crates/editor2/src/display_map/inlay_map.rs @@ -1889,7 +1889,8 @@ mod tests { } fn init_test(cx: &mut AppContext) { - cx.set_global(SettingsStore::test(cx)); + let store = SettingsStore::test(cx); + cx.set_global(store); theme::init(cx); } } diff --git a/crates/editor2/src/display_map/wrap_map.rs b/crates/editor2/src/display_map/wrap_map.rs index c7c55348b11c4959382b99589015dfdbfccd80e4..408142ae07d453ba7de30604735f91295ee0217b 100644 --- a/crates/editor2/src/display_map/wrap_map.rs +++ b/crates/editor2/src/display_map/wrap_map.rs @@ -23,7 +23,7 @@ pub struct WrapMap { edits_since_sync: Patch, wrap_width: Option, background_task: Option>, - font: (Font, Pixels), + font_with_size: (Font, Pixels), } #[derive(Clone)] @@ -76,7 +76,7 @@ impl WrapMap { ) -> (Model, WrapSnapshot) { let handle = cx.build_model(|cx| { let mut this = Self { - font: (font, font_size), + font_with_size: (font, font_size), wrap_width: None, pending_edits: Default::default(), interpolated_edits: Default::default(), @@ -116,9 +116,16 @@ impl WrapMap { (self.snapshot.clone(), mem::take(&mut self.edits_since_sync)) } - pub fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut ModelContext) -> bool { - if (font, font_size) != self.font { - self.font = (font, font_size); + pub fn set_font_with_size( + &mut self, + font: Font, + font_size: Pixels, + cx: &mut ModelContext, + ) -> bool { + let font_with_size = (font, font_size); + + if font_with_size != self.font_with_size { + self.font_with_size = font_with_size; self.rewrap(cx); true } else { @@ -149,10 +156,9 @@ impl WrapMap { let mut new_snapshot = self.snapshot.clone(); let mut edits = Patch::default(); let text_system = cx.text_system().clone(); - let (font_id, font_size) = self.font; + let (font, font_size) = self.font_with_size.clone(); let task = cx.background_executor().spawn(async move { - if let Some(mut line_wrapper) = - text_system.line_wrapper(font_id, font_size).log_err() + if let Some(mut line_wrapper) = text_system.line_wrapper(font, font_size).log_err() { let tab_snapshot = new_snapshot.tab_snapshot.clone(); let range = TabPoint::zero()..tab_snapshot.max_point(); @@ -236,11 +242,11 @@ impl WrapMap { let pending_edits = self.pending_edits.clone(); let mut snapshot = self.snapshot.clone(); let text_system = cx.text_system().clone(); - let (font_id, font_size) = self.font; + let (font, font_size) = self.font_with_size.clone(); let update_task = cx.background_executor().spawn(async move { let mut edits = Patch::default(); if let Some(mut line_wrapper) = - text_system.line_wrapper(font_id, font_size).log_err() + text_system.line_wrapper(font, font_size).log_err() { for (tab_snapshot, tab_edits) in pending_edits { let wrap_edits = snapshot diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 11ad04a6596a70a112c249487825c0f4afd50c91..e2d923c3395cb269bd3566483123fa4760301c09 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -35,8 +35,8 @@ pub use element::{ use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - AnyElement, AppContext, BackgroundExecutor, Element, EventEmitter, Model, Pixels, Render, - Subscription, Task, TextStyle, View, ViewContext, WeakView, WindowContext, + AnyElement, AppContext, BackgroundExecutor, Context, Element, EventEmitter, Model, Pixels, + Render, Subscription, Task, TextStyle, View, ViewContext, WeakView, WindowContext, }; use hover_popover::HoverState; pub use items::MAX_TAB_TITLE_LEN; @@ -64,7 +64,7 @@ use std::{ cmp::Reverse, ops::{Deref, DerefMut, Range}, sync::Arc, - time::Duration, + time::{Duration, Instant}, }; pub use sum_tree::Bias; use util::{ResultExt, TryFutureExt}; @@ -1810,14 +1810,14 @@ impl Editor { // ) // } - // pub fn for_buffer( - // buffer: Model, - // 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_buffer( + buffer: Model, + project: Option>, + cx: &mut ViewContext, + ) -> Self { + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new(EditorMode::Full, buffer, project, cx) + } // pub fn for_multibuffer( // buffer: Model, @@ -1827,171 +1827,175 @@ impl Editor { // 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 - // } + pub fn clone(&self, cx: &mut ViewContext) -> Self { + let mut clone = Self::new( + self.mode, + self.buffer.clone(), + self.project.clone(), + // todo! + // 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: Model, - // 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, - // ) - // }); + fn new( + mode: EditorMode, + buffer: Model, + project: Option>, + // todo!() + // get_field_editor_theme: Option>, + cx: &mut ViewContext, + ) -> Self { + todo!("old version below") + } + // 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 selections = SelectionsCollection::new(display_map.clone(), buffer.clone()); - // let blink_manager = cx.add_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx)); + // 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 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 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); + // 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.end_selection(cx); - // this.scroll_manager.show_scrollbar(cx); + // this._subscriptions.extend(project_subscriptions); - // let editor_created_event = EditorCreated(cx.handle()); - // cx.emit_global(editor_created_event); + // this.end_selection(cx); + // this.scroll_manager.show_scrollbar(cx); - // if mode == EditorMode::Full { - // let should_auto_hide_scrollbars = cx.platform().should_auto_hide_scrollbars(); - // cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars)); - // } + // let editor_created_event = EditorCreated(cx.handle()); + // cx.emit_global(editor_created_event); - // this.report_editor_event("open", None, cx); - // this + // 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, @@ -2316,19 +2320,19 @@ impl Editor { // result // } - // pub fn edit(&mut self, edits: I, cx: &mut ViewContext) - // where - // I: IntoIterator, T)>, - // S: ToOffset, - // T: Into>, - // { - // if self.read_only { - // return; - // } + 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)); - // } + self.buffer + .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); + } // pub fn edit_with_autoindent(&mut self, edits: I, cx: &mut ViewContext) // where @@ -8058,48 +8062,50 @@ impl Editor { // }); // } - // 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()); - // } - // } + 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 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"); - // } + fn start_transaction_at(&mut self, now: Instant, cx: &mut ViewContext) { + todo!() + // 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()); + // } + } - // cx.emit(Event::Edited); - // Some(tx_id) - // } else { - // None - // } - // } + fn end_transaction_at( + &mut self, + now: Instant, + cx: &mut ViewContext, + ) -> Option { + todo!() + // 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(); @@ -9304,7 +9310,7 @@ impl Render for Editor { // 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) +// map.set_font_with_size(style.text.font_id, style.text.font_size, cx) // }); // if font_changed { diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 6f36cf8e0648446d60d9f3092d80942fea0ae772..645cdc76469e1db52c898036a49f960aa0bfee67 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -2704,10 +2704,11 @@ impl PositionMap { let y = position.y.max(px(0.)).min(self.size.width); let x = position.x + (scroll_position.x * self.em_width); let row = (f32::from(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.into()) - .map(|LineWithInvisibles { line, .. }| line) + .get(row as usize - scroll_position.y as usize) + .map(|&LineWithInvisibles { ref line, .. }| line) { if let Some(ix) = line.index_for_x(x) { (ix as u32, px(0.)) diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 717cdc08a2a721e8a656d8e672dfe8631903ee1d..b0b344a868354f79a982328116e4095be86743cc 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -3,12 +3,12 @@ use crate::{ movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, }; -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; use collections::HashSet; use futures::future::try_join_all; use gpui::{ point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, Model, Pixels, SharedString, - Subscription, Task, View, ViewContext, WeakView, + Subscription, Task, View, ViewContext, VisualContext, WeakView, }; use language::{ proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point, @@ -566,11 +566,9 @@ impl Item for Editor { 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().into()), - Cow::Owned(path) => Some(path.to_string_lossy().to_string().into()), - } + fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option { + let path = path_for_buffer(&self.buffer, detail, true, cx)?; + Some(path.to_string_lossy().to_string().into()) } fn tab_content(&self, detail: Option, cx: &AppContext) -> AnyElement { @@ -613,11 +611,11 @@ impl Item for Editor { &self, _workspace_id: WorkspaceId, cx: &mut ViewContext, - ) -> Option> + ) -> Option> where Self: Sized, { - Some(self.clone()) + Some(cx.build_view(|cx| self.clone(cx))) } fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { @@ -706,7 +704,9 @@ impl Item for Editor { .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()); + 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| { @@ -807,7 +807,7 @@ impl Item for Editor { fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { let workspace_id = workspace.database_id(); - let item_id = cx.view_id(); + let item_id = cx.view().entity_id().as_u64() as ItemId; self.workspace = Some((workspace.weak_handle(), workspace.database_id())); fn serialize( @@ -835,7 +835,12 @@ impl Item for Editor { 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); + serialize( + buffer, + *workspace_id, + cx.view().entity_id().as_u64() as ItemId, + cx, + ); } } }) @@ -877,10 +882,11 @@ impl Item for Editor { let (_, project_item) = project_item.await?; let buffer = project_item .downcast::() - .context("Project item at stored path was not a buffer")?; + .map_err(|_| anyhow!("Project item at stored path was not a buffer"))?; Ok(pane.update(&mut cx, |_, cx| { - cx.add_view(|cx| { + cx.build_view(|cx| { let mut editor = Editor::for_buffer(buffer, Some(project), cx); + editor.read_scroll_position_from_db(item_id, workspace_id, cx); editor }) diff --git a/crates/editor2/src/link_go_to_definition.rs b/crates/editor2/src/link_go_to_definition.rs index da2b49def63f6a259597474da50c09f8ee3b6083..f9a54978464ac859d6502dd44ffde4cde1f7a075 100644 --- a/crates/editor2/src/link_go_to_definition.rs +++ b/crates/editor2/src/link_go_to_definition.rs @@ -108,59 +108,61 @@ pub fn update_go_to_definition_link( 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; - } - } - _ => {} - } - } + todo!("old version below"); +} +// 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(); +// editor.link_go_to_definition_state.last_trigger_point = trigger_point.clone(); - if pending_nonempty_selection { - hide_link_definition(editor, cx); - return; - } +// 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; - } - } +// 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); -} +// hide_link_definition(editor, cx); +// } pub fn update_inlay_link_and_hover_points( snapshot: &DisplaySnapshot, @@ -351,201 +353,204 @@ pub fn show_link_definition( 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); - } + todo!("old implementation below") +} +// 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; - } +// 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 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 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 { - // todo!() - // // 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); - } - } - })?; +// let excerpt_id = if let Some((excerpt_id, _, _)) = editor +// .buffer() +// .read(cx) +// .excerpt_containing(trigger_anchor.clone(), cx) +// { +// excerpt_id +// } else { +// return; +// }; - Ok::<_, anyhow::Error>(()) - } - .log_err() - }); +// let project = if let Some(project) = editor.project.clone() { +// project +// } else { +// return; +// }; - editor.link_go_to_definition_state.task = Some(task); -} +// // 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 { +// // todo!() +// // // 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(); - } + todo!() + // 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.link_go_to_definition_state.task = None; - editor.clear_highlights::(cx); + // editor.clear_highlights::(cx); } pub fn go_to_fetched_definition( diff --git a/crates/editor2/src/movement.rs b/crates/editor2/src/movement.rs index 0749c3f17855f5a8b8fdd8b4c108e944cb3181ac..962f4f60e643bbc1646c76e0a71bf27223b27b66 100644 --- a/crates/editor2/src/movement.rs +++ b/crates/editor2/src/movement.rs @@ -1,7 +1,8 @@ use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint}; -use gpui::TextSystem; +use gpui::{px, TextSystem}; use language::Point; +use serde::de::IntoDeserializer; use std::ops::Range; #[derive(Debug, PartialEq)] @@ -93,9 +94,9 @@ pub fn up_by_rows( text_layout_details: &TextLayoutDetails, ) -> (DisplayPoint, SelectionGoal) { let mut goal_x = match goal { - SelectionGoal::HorizontalPosition(x) => x, - SelectionGoal::WrappedHorizontalPosition((_, x)) => x, - SelectionGoal::HorizontalRange { end, .. } => end, + SelectionGoal::HorizontalPosition(x) => x.into(), // todo!("Can the fields in SelectionGoal by Pixels? We should extract a geometry crate and depend on that.") + SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(), + SelectionGoal::HorizontalRange { end, .. } => end.into(), _ => map.x_for_point(start, text_layout_details), }; @@ -110,14 +111,17 @@ pub fn up_by_rows( return (start, goal); } else { point = DisplayPoint::new(0, 0); - goal_x = 0.0; + goal_x = px(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)) + ( + clipped_point, + SelectionGoal::HorizontalPosition(goal_x.into()), + ) } pub fn down_by_rows( @@ -129,9 +133,9 @@ pub fn down_by_rows( text_layout_details: &TextLayoutDetails, ) -> (DisplayPoint, SelectionGoal) { let mut goal_x = match goal { - SelectionGoal::HorizontalPosition(x) => x, - SelectionGoal::WrappedHorizontalPosition((_, x)) => x, - SelectionGoal::HorizontalRange { end, .. } => end, + SelectionGoal::HorizontalPosition(x) => x.into(), + SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(), + SelectionGoal::HorizontalRange { end, .. } => end.into(), _ => map.x_for_point(start, text_layout_details), }; @@ -150,7 +154,10 @@ pub fn down_by_rows( if clipped_point.row() > point.row() { clipped_point = map.clip_point(point, Bias::Left); } - (clipped_point, SelectionGoal::HorizontalPosition(goal_x)) + ( + clipped_point, + SelectionGoal::HorizontalPosition(goal_x.into()), + ) } pub fn line_beginning( diff --git a/crates/editor2/src/persistence.rs b/crates/editor2/src/persistence.rs index c1c14550142f8583869edc0a7f4679b84b4ea131..6e37735c1371ff466e345c95d0fa92d6d3c892a6 100644 --- a/crates/editor2/src/persistence.rs +++ b/crates/editor2/src/persistence.rs @@ -3,7 +3,6 @@ use std::path::PathBuf; use db::sqlez_macros::sql; use db::{define_connection, query}; -use gpui::EntityId; use workspace::{ItemId, WorkspaceDb, WorkspaceId}; define_connection!( @@ -67,7 +66,7 @@ impl EditorDb { query! { pub async fn save_scroll_position( - item_id: EntityId, + item_id: ItemId, workspace_id: WorkspaceId, top_row: u32, vertical_offset: f32, diff --git a/crates/editor2/src/scroll.rs b/crates/editor2/src/scroll.rs index 3c2a25dd9332b1dbb3bbff1fd72bb532e68d0063..0c6829fbcb3cd8d82acb4040c719507f5799ba8e 100644 --- a/crates/editor2/src/scroll.rs +++ b/crates/editor2/src/scroll.rs @@ -16,7 +16,7 @@ use std::{ time::{Duration, Instant}, }; use util::ResultExt; -use workspace::WorkspaceId; +use workspace::{ItemId, WorkspaceId}; use self::{ autoscroll::{Autoscroll, AutoscrollStrategy}, @@ -228,7 +228,7 @@ impl ScrollManager { self.show_scrollbar(cx); self.autoscroll_request.take(); if let Some(workspace_id) = workspace_id { - let item_id = cx.view().entity_id(); + let item_id = cx.view().entity_id().as_u64() as ItemId; cx.foreground_executor() .spawn(async move { @@ -355,31 +355,31 @@ impl Editor { // 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 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(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) { @@ -426,24 +426,24 @@ impl Editor { // 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: Point::new(x, y), - // anchor: top_anchor, - // }; - // self.set_scroll_anchor(scroll_anchor, cx); - // } - // } + 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: gpui::Point::new(x, y), + anchor: top_anchor, + }; + self.set_scroll_anchor(scroll_anchor, cx); + } + } } diff --git a/crates/text2/src/selection.rs b/crates/text2/src/selection.rs index 4bf205dd5d6e379172f7411b97fea3bcecdf3299..4f1f9a29223b927cd993d53a9df27488c9b37ad0 100644 --- a/crates/text2/src/selection.rs +++ b/crates/text2/src/selection.rs @@ -1,5 +1,3 @@ -use gpui::Pixels; - use crate::{Anchor, BufferSnapshot, TextDimension}; use std::cmp::Ordering; use std::ops::Range; @@ -7,8 +5,8 @@ use std::ops::Range; #[derive(Copy, Clone, Debug, PartialEq)] pub enum SelectionGoal { None, - HorizontalPosition(Pixels), - HorizontalRange { start: Pixels, end: Pixels }, + HorizontalPosition(f32), // todo!("Can we use pixels here without adding a runtime gpui dependency?") + HorizontalRange { start: f32, end: f32 }, WrappedHorizontalPosition((u32, f32)), } From 800c2685ea4315d8749019b4530c067d66a6975e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Nov 2023 09:05:58 +0100 Subject: [PATCH 10/23] Remove dependency from gpui from editor2 --- Cargo.lock | 5 +---- crates/copilot2/Cargo.toml | 2 +- crates/editor2/Cargo.toml | 6 +++--- crates/editor2/src/editor.rs | 2 +- crates/editor2/src/mouse_context_menu.rs | 1 - crates/gpui2/Cargo.toml | 1 - 6 files changed, 6 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 46d481c2eacc607e3f13426568e850cf36a907bf..135e8a7ca94454a72dd59ece021db3e6b2bfd06e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1866,7 +1866,6 @@ dependencies = [ "async-tar", "clock", "collections", - "context_menu", "fs", "futures 0.3.28", "gpui2", @@ -2631,7 +2630,6 @@ dependencies = [ "client2", "clock", "collections", - "context_menu", "convert_case 0.6.0", "copilot2", "ctor", @@ -2654,7 +2652,7 @@ dependencies = [ "postage", "project2", "rand 0.8.5", - "rich_text", + "rich_text2", "rpc2", "schemars", "serde", @@ -3578,7 +3576,6 @@ dependencies = [ "foreign-types", "futures 0.3.28", "gpui2_macros", - "gpui_macros", "image", "itertools 0.10.5", "lazy_static", diff --git a/crates/copilot2/Cargo.toml b/crates/copilot2/Cargo.toml index 20211946071b3284d2ba70a2a497e9c8f75b75c7..2ce432d9fc2ff877e5982aa7d2a3d83e61cc9491 100644 --- a/crates/copilot2/Cargo.toml +++ b/crates/copilot2/Cargo.toml @@ -20,7 +20,7 @@ test-support = [ [dependencies] collections = { path = "../collections" } -context_menu = { path = "../context_menu" } +# context_menu = { path = "../context_menu" } gpui = { package = "gpui2", path = "../gpui2" } language = { package = "language2", path = "../language2" } settings = { package = "settings2", path = "../settings2" } diff --git a/crates/editor2/Cargo.toml b/crates/editor2/Cargo.toml index f0002787f3d7806c0c46865160a9e3c6860acde8..e356a3515530eb1f92a0fa2ed7c38ec8b7f1768b 100644 --- a/crates/editor2/Cargo.toml +++ b/crates/editor2/Cargo.toml @@ -29,7 +29,7 @@ copilot = { package="copilot2", path = "../copilot2" } db = { package="db2", path = "../db2" } drag_and_drop = { path = "../drag_and_drop" } collections = { path = "../collections" } -context_menu = { path = "../context_menu" } +# context_menu = { path = "../context_menu" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" } git = { path = "../git" } gpui = { package = "gpui2", path = "../gpui2" } @@ -38,7 +38,7 @@ lsp = { package = "lsp2", path = "../lsp2" } multi_buffer = { package = "multi_buffer2", path = "../multi_buffer2" } project = { package = "project2", path = "../project2" } rpc = { package = "rpc2", path = "../rpc2" } -rich_text = { path = "../rich_text" } +rich_text = { package = "rich_text2", path = "../rich_text2" } settings = { package="settings2", path = "../settings2" } snippet = { path = "../snippet" } sum_tree = { path = "../sum_tree" } @@ -46,7 +46,7 @@ text = { package="text2", path = "../text2" } theme = { package="theme2", path = "../theme2" } util = { path = "../util" } sqlez = { path = "../sqlez" } -workspace = { package="workspace2", path = "../workspace2" } +workspace = { package = "workspace2", path = "../workspace2" } aho-corasick = "1.1" anyhow.workspace = true diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index e2d923c3395cb269bd3566483123fa4760301c09..16997fcacca87a3378288e02c83779ea63caf8c1 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -622,7 +622,7 @@ pub struct Editor { // inlay_background_highlights: TreeMap, InlayBackgroundHighlight>, nav_history: Option, context_menu: RwLock>, - mouse_context_menu: View, + // mouse_context_menu: View, completion_tasks: Vec<(CompletionId, Task>)>, next_completion_id: CompletionId, available_code_actions: Option<(Model, Arc<[CodeAction]>)>, diff --git a/crates/editor2/src/mouse_context_menu.rs b/crates/editor2/src/mouse_context_menu.rs index 97787c3d39ce17aab771af2a81135e20dfc4cd72..b70a826bf8cf3975bdd5c93df2ba67c31c0fb672 100644 --- a/crates/editor2/src/mouse_context_menu.rs +++ b/crates/editor2/src/mouse_context_menu.rs @@ -1,5 +1,4 @@ use crate::{DisplayPoint, Editor, EditorMode, SelectMode}; -use context_menu::ContextMenuItem; use gpui::{Pixels, Point, ViewContext}; pub fn deploy_context_menu( diff --git a/crates/gpui2/Cargo.toml b/crates/gpui2/Cargo.toml index fa072dadc3c064a493e135451c67cda7e822c93c..df461af7b87c40044f3ad8cc8b4c198e222c0263 100644 --- a/crates/gpui2/Cargo.toml +++ b/crates/gpui2/Cargo.toml @@ -15,7 +15,6 @@ doctest = false [dependencies] collections = { path = "../collections" } -gpui_macros = { path = "../gpui_macros" } gpui2_macros = { path = "../gpui2_macros" } util = { path = "../util" } sum_tree = { path = "../sum_tree" } From cdc82d01f775f5ebad0734cd4e099a1e4e573727 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Nov 2023 09:06:25 +0100 Subject: [PATCH 11/23] Call editor::init --- crates/workspace2/src/workspace2.rs | 25 ++++++++++++------------- crates/zed2/src/main.rs | 2 +- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 8a80ef328aee8154e8d446576f1faf02fcc7541b..f5f507f3b15dc3d471347c734ccfc236898d1487 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -414,18 +414,17 @@ type ItemDeserializers = HashMap< ) -> Task>>, >; pub fn register_deserializable_item(cx: &mut AppContext) { - cx.update_global(|deserializers: &mut ItemDeserializers, _cx| { - if let Some(serialized_item_kind) = I::serialized_item_kind() { - deserializers.insert( - Arc::from(serialized_item_kind), - |project, workspace, workspace_id, item_id, cx| { - let task = I::deserialize(project, workspace, workspace_id, item_id, cx); - cx.foreground_executor() - .spawn(async { Ok(Box::new(task.await?) as Box<_>) }) - }, - ); - } - }); + if let Some(serialized_item_kind) = I::serialized_item_kind() { + let deserializers = cx.default_global::(); + deserializers.insert( + Arc::from(serialized_item_kind), + |project, workspace, workspace_id, item_id, cx| { + let task = I::deserialize(project, workspace, workspace_id, item_id, cx); + cx.foreground_executor() + .spawn(async { Ok(Box::new(task.await?) as Box<_>) }) + }, + ); + } } pub struct AppState { @@ -627,7 +626,7 @@ impl Workspace { } project2::Event::Closed => { - // cx.remove_window(); + cx.remove_window(); } project2::Event::DeletedEntry(entry_id) => { diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 1e3b8d0d2bba686deec67a6555a7bfd46066dc98..3033e93dd622cc05899a067fbc8c2a74c512c1cb 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -140,7 +140,7 @@ fn main() { client::init(&client, cx); // command_palette::init(cx); language::init(cx); - // editor::init(cx); + editor::init(cx); // go_to_line::init(cx); // file_finder::init(cx); // outline::init(cx); From efa27cf9b8743307724b2c1924db298531bf6944 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Nov 2023 10:09:00 +0100 Subject: [PATCH 12/23] Uncomment more editor code --- crates/copilot2/src/copilot2.rs | 5 + crates/editor2/src/editor.rs | 1599 +++++++++++++++--------------- crates/workspace2/src/item.rs | 4 +- crates/zed2/src/main.rs | 45 +- crates/zed2/src/open_listener.rs | 216 +++- crates/zed2/src/zed2.rs | 208 +--- 6 files changed, 1059 insertions(+), 1018 deletions(-) diff --git a/crates/copilot2/src/copilot2.rs b/crates/copilot2/src/copilot2.rs index 6b1190a5bff7b47fab19c3adc3159244d6433c48..ac52bf156d66fe52c1e1625b56195555a4db5a21 100644 --- a/crates/copilot2/src/copilot2.rs +++ b/crates/copilot2/src/copilot2.rs @@ -42,6 +42,11 @@ use util::{ // copilot, // [Suggest, NextSuggestion, PreviousSuggestion, Reinstall] // ); +// +pub struct Suggest; +pub struct NextSuggestion; +pub struct PreviousSuggestion; +pub struct Reinstall; pub fn init( new_server_id: LanguageServerId, diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 16997fcacca87a3378288e02c83779ea63caf8c1..b6d70a1fe9eff776c34ab7bc6c0d112d2f0aee57 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -23,9 +23,10 @@ pub mod test; use aho_corasick::AhoCorasick; use anyhow::Result; use blink_manager::BlinkManager; -use client::{Client, Collaborator, ParticipantIndex}; +use client::{ClickhouseEvent, Client, Collaborator, ParticipantIndex, TelemetrySettings}; use clock::ReplicaId; -use collections::{HashMap, HashSet, VecDeque}; +use collections::{BTreeMap, HashMap, HashSet, VecDeque}; +use copilot::Copilot; pub use display_map::DisplayPoint; use display_map::*; pub use editor_settings::EditorSettings; @@ -35,18 +36,21 @@ pub use element::{ use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - AnyElement, AppContext, BackgroundExecutor, Context, Element, EventEmitter, Model, Pixels, - Render, Subscription, Task, TextStyle, View, ViewContext, WeakView, WindowContext, + AnyElement, AppContext, BackgroundExecutor, Context, Element, EventEmitter, Hsla, Model, + Pixels, Render, Subscription, Task, TextStyle, View, ViewContext, WeakView, WindowContext, }; -use hover_popover::HoverState; +use highlight_matching_bracket::refresh_matching_bracket_highlights; +use hover_popover::{hide_hover, HoverState}; +use inlay_hint_cache::InlayHintCache; 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}, AutoindentMode, BracketPair, Buffer, CodeAction, Completion, CursorShape, Diagnostic, Language, LanguageRegistry, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId, }; -use link_go_to_definition::LinkGoToDefinitionState; +use link_go_to_definition::{InlayHighlight, LinkGoToDefinitionState}; use lsp::{Documentation, LanguageServerId}; pub use multi_buffer::{ Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset, @@ -57,17 +61,22 @@ use parking_lot::RwLock; use project::{FormatTrigger, Project}; use rpc::proto::*; use scroll::{autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager}; -use selections_collection::SelectionsCollection; +use selections_collection::{MutableSelectionsCollection, SelectionsCollection}; use serde::{Deserialize, Serialize}; use settings::Settings; use std::{ + any::TypeId, cmp::Reverse, ops::{Deref, DerefMut, Range}, + path::Path, sync::Arc, time::{Duration, Instant}, }; pub use sum_tree::Bias; -use util::{ResultExt, TryFutureExt}; +use sum_tree::TreeMap; +use text::Rope; +use theme::ThemeColors; +use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::{ItemNavHistory, ViewId, Workspace}; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); @@ -374,6 +383,10 @@ impl InlayId { // ] // ); +// todo!(revisit these actions) +pub struct ShowCompletions; +pub struct Rename; + enum DocumentHighlightRead {} enum DocumentHighlightWrite {} enum InputComposition {} @@ -586,8 +599,8 @@ type CompletionId = usize; // type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor; // type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option; -// type BackgroundHighlight = (fn(&Theme) -> Hsla, Vec>); -// type InlayBackgroundHighlight = (fn(&Theme) -> Hsla, Vec); +type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Vec>); +type InlayBackgroundHighlight = (fn(&ThemeColors) -> Hsla, Vec); pub struct Editor { handle: WeakView, @@ -618,8 +631,8 @@ pub struct Editor { show_wrap_guides: Option, placeholder_text: Option>, highlighted_rows: Option>, - // background_highlights: BTreeMap, - // inlay_background_highlights: TreeMap, InlayBackgroundHighlight>, + background_highlights: BTreeMap, + inlay_background_highlights: TreeMap, InlayBackgroundHighlight>, nav_history: Option, context_menu: RwLock>, // mouse_context_menu: View, @@ -643,7 +656,7 @@ pub struct Editor { gutter_hovered: bool, link_go_to_definition_state: LinkGoToDefinitionState, copilot_state: CopilotState, - // inlay_hint_cache: InlayHintCache, + inlay_hint_cache: InlayHintCache, next_inlay_id: usize, _subscriptions: Vec, pixel_position_of_newest_cursor: Option>, @@ -913,7 +926,7 @@ struct CompletionsMenu { } // todo!(this is fake) -#[derive(Clone)] +#[derive(Clone, Default)] struct UniformListState; // todo!(this is fake) @@ -2189,136 +2202,136 @@ impl Editor { // 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), + 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, + ) + }); + } - // _ => { - // *context_menu = None; - // None - // } - // }; + 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 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 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), - // if menu.id > completion_menu.id { - // return; - // } + _ => { + *context_menu = None; + None + } + }; - // *context_menu = Some(ContextMenu::Completions(completion_menu)); - // drop(context_menu); - // cx.notify(); - // }) - // }) - // .detach(); + 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_executor().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); - // } + self.show_completions(&ShowCompletions, cx); + } else { + drop(context_menu); + self.hide_context_menu(cx); + } + } else { + drop(context_menu); + } - // hide_hover(self, cx); + 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); - // } + 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(); - // } + 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(); + 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); + 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); - // } + if changed { + if let Some(autoscroll) = autoscroll { + self.request_autoscroll(autoscroll, cx); + } + self.selections_did_change(true, &old_cursor_position, cx); + } - // result - // } + result + } pub fn edit(&mut self, edits: I, cx: &mut ViewContext) where @@ -3170,45 +3183,45 @@ impl Editor { // ); // } - // fn insert_with_autoindent_mode( - // &mut self, - // text: &str, - // autoindent_mode: Option, - // cx: &mut ViewContext, - // ) { - // if self.read_only { - // return; - // } + 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 - // }); + 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); - // }) - // }); - // } + 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 !EditorSettings>(cx).show_completions_on_input { @@ -3288,45 +3301,45 @@ impl Editor { // }) // } - // /// 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 - // }); - // } + /// 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 - // } - // } + 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) { // todo!(); @@ -3534,109 +3547,110 @@ impl Editor { // })) // } - // fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext) { - // if self.pending_rename.is_some() { - // return; - // } + 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 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 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 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 - // }; + 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_executor().clone()) + .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); + 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 => {} + 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; - // } - // } + Some(ContextMenu::Completions(prev_menu)) => { + if prev_menu.id > id { + return; + } + } - // _ => 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); - // } - // } - // })?; + 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)); - // } + Ok::<_, anyhow::Error>(()) + } + .log_err() + }); + self.completion_tasks.push((id, task)); + } // pub fn confirm_completion( // &mut self, @@ -3919,385 +3933,400 @@ impl Editor { // 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; + 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; + } - // 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 - // } + self.code_actions_task = Some(cx.spawn(|this, mut cx| async move { + cx.background_executor() + .timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT) + .await; - // fn refresh_document_highlights(&mut self, cx: &mut ViewContext) -> Option<()> { - // if self.pending_rename.is_some() { - // return None; - // } + let actions = if let Ok(code_actions) = project.update(&mut cx, |project, cx| { + project.code_actions(&start_buffer, start..end, cx) + }) { + code_actions.await.log_err() + } else { + 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; - // } + this.update(&mut cx, |this, cx| { + this.available_code_actions = actions.and_then(|actions| { + if actions.is_empty() { + None + } else { + Some((start_buffer, actions.into())) + } + }); + cx.notify(); + }) + .log_err(); + })); + None + } - // self.document_highlights_task = Some(cx.spawn(|this, mut cx| async move { - // cx.background() - // .timer(DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT) - // .await; + fn refresh_document_highlights(&mut self, cx: &mut ViewContext) -> Option<()> { + if self.pending_rename.is_some() { + return None; + } - // let highlights = project - // .update(&mut cx, |project, cx| { - // project.document_highlights(&cursor_buffer, cursor_buffer_position, cx) - // }) - // .await - // .log_err(); + 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; + } - // if let Some(highlights) = highlights { - // this.update(&mut cx, |this, cx| { - // if this.pending_rename.is_some() { - // return; - // } + self.document_highlights_task = Some(cx.spawn(|this, mut cx| async move { + cx.background_executor() + .timer(DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT) + .await; - // 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 highlights = if let Some(highlights) = project + .update(&mut cx, |project, cx| { + project.document_highlights(&cursor_buffer, cursor_buffer_position, cx) + }) + .log_err() + { + highlights.await.log_err() + } else { + None + }; - // 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; - // } + if let Some(highlights) = highlights { + this.update(&mut cx, |this, cx| { + if this.pending_rename.is_some() { + return; + } - // 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); - // } - // } - // } + 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; + } - // 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 - // } + 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); + } + } + } - // 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); + this.highlight_background::( + read_ranges, + |theme| todo!("theme.editor.document_highlight_read_background"), + cx, + ); + this.highlight_background::( + write_ranges, + |theme| todo!("theme.editor.document_highlight_write_background"), + cx, + ); + cx.notify(); + }) + .log_err(); + } + })); + None + } - // 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; - // } + 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 (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 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 completions = copilot - // .update(&mut cx, |copilot, cx| { - // copilot.completions(&buffer, buffer_position, cx) - // }) - // .await - // .log_err() - // .into_iter() - // .flatten() - // .collect_vec(); + 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_executor() + .timer(COPILOT_DEBOUNCE_TIMEOUT) + .await; + } - // 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(()) - // }); + let completions = copilot + .update(&mut cx, |copilot, cx| { + copilot.completions(&buffer, buffer_position, cx) + }) + .log_err() + .unwrap_or(Task::ready(Ok(Vec::new()))) + .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(()) - // } + 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; - // } + 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; + 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) + }) + .log_err()? + .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()?; + 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(()) + }); + } - // 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; - // } + 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); - // } + 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 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 { + todo!(); + // cx.propagate(); + } + } + } - // 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 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 { + todo!(); + // 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); + 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 - // } - // } + 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); + 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.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 - // } - // } + 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 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 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); + 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 - // } - // } + 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(); + 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); - // } + 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); - // } - // } + 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); - // } + 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, @@ -4422,15 +4451,15 @@ impl Editor { // }) // } - // 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 - // } + 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, @@ -6277,37 +6306,37 @@ impl Editor { // 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; - // } - // } + 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, - // ); - // } - // } + 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); @@ -7843,47 +7872,47 @@ impl Editor { // })) // } - // 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); - // } + 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) - // } + Some(rename) + } - // #[cfg(any(test, feature = "test-support"))] - // pub fn pending_rename(&self) -> Option<&RenameState> { - // self.pending_rename.as_ref() - // } + #[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 { @@ -8053,14 +8082,14 @@ impl Editor { // 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(), - // }); - // } + 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, @@ -8276,19 +8305,19 @@ impl Editor { // } // } - // 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 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 @@ -8427,16 +8456,16 @@ impl Editor { // 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_background( + &mut self, + ranges: Vec>, + color_fetcher: fn(&ThemeColors) -> Hsla, + cx: &mut ViewContext, + ) { + self.background_highlights + .insert(TypeId::of::(), (color_fetcher, ranges)); + cx.notify(); + } // pub fn highlight_inlay_background( // &mut self, @@ -8658,14 +8687,14 @@ impl Editor { // 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 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 @@ -8911,34 +8940,34 @@ impl Editor { // .collect() // } - // fn report_copilot_event( - // &self, - // suggestion_id: Option, - // suggestion_accepted: bool, - // cx: &AppContext, - // ) { - // let Some(project) = &self.project else { return }; + 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); - // } + // 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 = *TelemetrySettings::get_global(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( diff --git a/crates/workspace2/src/item.rs b/crates/workspace2/src/item.rs index 90ebd16f2a03c7e072615091df957fca9255f712..5d02f5d6fdcc194c0bef7fc57125d3673520d3ff 100644 --- a/crates/workspace2/src/item.rs +++ b/crates/workspace2/src/item.rs @@ -261,7 +261,7 @@ pub trait ItemHandle: 'static + Send { fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option; fn to_followable_item_handle(&self, cx: &AppContext) -> Option>; fn on_release( - &mut self, + &self, cx: &mut AppContext, callback: Box, ) -> gpui2::Subscription; @@ -578,7 +578,7 @@ impl ItemHandle for View { } fn on_release( - &mut self, + &self, cx: &mut AppContext, callback: Box, ) -> gpui2::Subscription { diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 3033e93dd622cc05899a067fbc8c2a74c512c1cb..31bfb1c99a506737604c757e7a27cd39d7ccfd75 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -256,6 +256,7 @@ fn main() { .detach(); } Ok(Some(OpenRequest::JoinChannel { channel_id: _ })) => { + todo!() // triggered_authentication = true; // let app_state = app_state.clone(); // let client = client.clone(); @@ -267,6 +268,9 @@ fn main() { // }) // .detach_and_log_err(cx) } + Ok(Some(OpenRequest::OpenChannelNotes { channel_id: _ })) => { + todo!() + } Ok(None) | Err(_) => cx .spawn({ let app_state = app_state.clone(); @@ -276,28 +280,25 @@ fn main() { } let app_state = app_state.clone(); - cx.spawn(|cx| { - async move { - while let Some(request) = open_rx.next().await { - match request { - OpenRequest::Paths { paths } => { - cx.update(|cx| open_paths_and_log_errs(&paths, &app_state, cx)) - .ok(); - } - OpenRequest::CliConnection { connection } => { - let app_state = app_state.clone(); - cx.spawn(move |cx| { - handle_cli_connection(connection, app_state.clone(), cx) - }) - .detach(); - } - OpenRequest::JoinChannel { channel_id: _ } => { - // cx - // .update(|cx| { - // workspace::join_channel(channel_id, app_state.clone(), None, cx) - // }) - // .detach() - } + cx.spawn(|cx| async move { + while let Some(request) = open_rx.next().await { + match request { + OpenRequest::Paths { paths } => { + cx.update(|cx| open_paths_and_log_errs(&paths, &app_state, cx)) + .ok(); + } + OpenRequest::CliConnection { connection } => { + let app_state = app_state.clone(); + cx.spawn(move |cx| { + handle_cli_connection(connection, app_state.clone(), cx) + }) + .detach(); + } + OpenRequest::JoinChannel { channel_id: _ } => { + todo!() + } + OpenRequest::OpenChannelNotes { channel_id: _ } => { + todo!() } } } diff --git a/crates/zed2/src/open_listener.rs b/crates/zed2/src/open_listener.rs index 9b416e14be4b601cad8c6b4555a9c9459757de84..f4219f199dca3bfb4bbc01ea27a07cd5aa84c13d 100644 --- a/crates/zed2/src/open_listener.rs +++ b/crates/zed2/src/open_listener.rs @@ -1,15 +1,26 @@ -use anyhow::anyhow; +use anyhow::{anyhow, Context, Result}; +use cli::{ipc, IpcHandshake}; use cli::{ipc::IpcSender, CliRequest, CliResponse}; -use futures::channel::mpsc; +use editor::scroll::autoscroll::Autoscroll; +use editor::Editor; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; +use futures::channel::{mpsc, oneshot}; +use futures::{FutureExt, SinkExt, StreamExt}; +use gpui::AsyncAppContext; +use language::{Bias, Point}; +use std::collections::HashMap; use std::ffi::OsStr; use std::os::unix::prelude::OsStrExt; +use std::path::Path; use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::thread; +use std::time::Duration; use std::{path::PathBuf, sync::atomic::AtomicBool}; use util::channel::parse_zed_link; +use util::paths::PathLikeWithPosition; use util::ResultExt; - -use crate::connect_to_cli; +use workspace::AppState; pub enum OpenRequest { Paths { @@ -21,6 +32,9 @@ pub enum OpenRequest { JoinChannel { channel_id: u64, }, + OpenChannelNotes { + channel_id: u64, + }, } pub struct OpenListener { @@ -74,7 +88,11 @@ impl OpenListener { if let Some(slug) = parts.next() { if let Some(id_str) = slug.split("-").last() { if let Ok(channel_id) = id_str.parse::() { - return Some(OpenRequest::JoinChannel { channel_id }); + if Some("notes") == parts.next() { + return Some(OpenRequest::OpenChannelNotes { channel_id }); + } else { + return Some(OpenRequest::JoinChannel { channel_id }); + } } } } @@ -96,3 +114,191 @@ impl OpenListener { Some(OpenRequest::Paths { paths }) } } + +fn connect_to_cli( + server_name: &str, +) -> Result<(mpsc::Receiver, IpcSender)> { + let handshake_tx = cli::ipc::IpcSender::::connect(server_name.to_string()) + .context("error connecting to cli")?; + let (request_tx, request_rx) = ipc::channel::()?; + let (response_tx, response_rx) = ipc::channel::()?; + + handshake_tx + .send(IpcHandshake { + requests: request_tx, + responses: response_rx, + }) + .context("error sending ipc handshake")?; + + let (mut async_request_tx, async_request_rx) = + futures::channel::mpsc::channel::(16); + thread::spawn(move || { + while let Ok(cli_request) = request_rx.recv() { + if smol::block_on(async_request_tx.send(cli_request)).is_err() { + break; + } + } + Ok::<_, anyhow::Error>(()) + }); + + Ok((async_request_rx, response_tx)) +} + +pub async fn handle_cli_connection( + (mut requests, responses): (mpsc::Receiver, IpcSender), + app_state: Arc, + mut cx: AsyncAppContext, +) { + if let Some(request) = requests.next().await { + match request { + CliRequest::Open { paths, wait } => { + let mut caret_positions = HashMap::new(); + + let paths = if paths.is_empty() { + workspace::last_opened_workspace_paths() + .await + .map(|location| location.paths().to_vec()) + .unwrap_or_default() + } else { + paths + .into_iter() + .filter_map(|path_with_position_string| { + let path_with_position = PathLikeWithPosition::parse_str( + &path_with_position_string, + |path_str| { + Ok::<_, std::convert::Infallible>( + Path::new(path_str).to_path_buf(), + ) + }, + ) + .expect("Infallible"); + let path = path_with_position.path_like; + if let Some(row) = path_with_position.row { + if path.is_file() { + let row = row.saturating_sub(1); + let col = + path_with_position.column.unwrap_or(0).saturating_sub(1); + caret_positions.insert(path.clone(), Point::new(row, col)); + } + } + Some(path) + }) + .collect() + }; + + let mut errored = false; + + match cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) { + Ok(task) => match task.await { + Ok((workspace, items)) => { + let mut item_release_futures = Vec::new(); + + for (item, path) in items.into_iter().zip(&paths) { + match item { + Some(Ok(item)) => { + if let Some(point) = caret_positions.remove(path) { + if let Some(active_editor) = item.downcast::() { + workspace + .update(&mut cx, |_, cx| { + active_editor.update(cx, |editor, cx| { + let snapshot = editor + .snapshot(cx) + .display_snapshot; + let point = snapshot + .buffer_snapshot + .clip_point(point, Bias::Left); + editor.change_selections( + Some(Autoscroll::center()), + cx, + |s| s.select_ranges([point..point]), + ); + }); + }) + .log_err(); + } + } + + cx.update(|cx| { + let released = oneshot::channel(); + item.on_release( + cx, + Box::new(move |_| { + let _ = released.0.send(()); + }), + ) + .detach(); + item_release_futures.push(released.1); + }) + .log_err(); + } + Some(Err(err)) => { + responses + .send(CliResponse::Stderr { + message: format!( + "error opening {:?}: {}", + path, err + ), + }) + .log_err(); + errored = true; + } + None => {} + } + } + + if wait { + let background = cx.background_executor().clone(); + let wait = async move { + if paths.is_empty() { + let (done_tx, done_rx) = oneshot::channel(); + let _subscription = + workspace.update(&mut cx, |workspace, cx| { + cx.on_release(move |_, _| { + let _ = done_tx.send(()); + }) + }); + let _ = done_rx.await; + } else { + let _ = futures::future::try_join_all(item_release_futures) + .await; + }; + } + .fuse(); + futures::pin_mut!(wait); + + loop { + // Repeatedly check if CLI is still open to avoid wasting resources + // waiting for files or workspaces to close. + let mut timer = background.timer(Duration::from_secs(1)).fuse(); + futures::select_biased! { + _ = wait => break, + _ = timer => { + if responses.send(CliResponse::Ping).is_err() { + break; + } + } + } + } + } + } + Err(error) => { + errored = true; + responses + .send(CliResponse::Stderr { + message: format!("error opening {:?}: {}", paths, error), + }) + .log_err(); + } + }, + Err(_) => errored = true, + } + + responses + .send(CliResponse::Exit { + status: i32::from(errored), + }) + .log_err(); + } + } + } +} diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 4224baadb881d8316035fc01b19f4961541d32a3..e3b54e6e62c1a03ffa7d8dff171bfa5ce78af923 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -7,218 +7,18 @@ mod only_instance; mod open_listener; pub use assets::*; -use collections::HashMap; use gpui::{ - point, px, AppContext, AsyncAppContext, AsyncWindowContext, Point, Task, TitlebarOptions, - WeakView, WindowBounds, WindowKind, WindowOptions, + point, px, AppContext, AsyncWindowContext, Task, TitlebarOptions, WeakView, WindowBounds, + WindowKind, WindowOptions, }; pub use only_instance::*; pub use open_listener::*; -use anyhow::{Context, Result}; -use cli::{ - ipc::{self, IpcSender}, - CliRequest, CliResponse, IpcHandshake, -}; -use futures::{ - channel::{mpsc, oneshot}, - FutureExt, SinkExt, StreamExt, -}; -use std::{path::Path, sync::Arc, thread, time::Duration}; -use util::{paths::PathLikeWithPosition, ResultExt}; +use anyhow::Result; +use std::sync::Arc; use uuid::Uuid; use workspace::{AppState, Workspace}; -pub fn connect_to_cli( - server_name: &str, -) -> Result<(mpsc::Receiver, IpcSender)> { - let handshake_tx = cli::ipc::IpcSender::::connect(server_name.to_string()) - .context("error connecting to cli")?; - let (request_tx, request_rx) = ipc::channel::()?; - let (response_tx, response_rx) = ipc::channel::()?; - - handshake_tx - .send(IpcHandshake { - requests: request_tx, - responses: response_rx, - }) - .context("error sending ipc handshake")?; - - let (mut async_request_tx, async_request_rx) = - futures::channel::mpsc::channel::(16); - thread::spawn(move || { - while let Ok(cli_request) = request_rx.recv() { - if smol::block_on(async_request_tx.send(cli_request)).is_err() { - break; - } - } - Ok::<_, anyhow::Error>(()) - }); - - Ok((async_request_rx, response_tx)) -} - -pub async fn handle_cli_connection( - (mut requests, responses): (mpsc::Receiver, IpcSender), - app_state: Arc, - mut cx: AsyncAppContext, -) { - if let Some(request) = requests.next().await { - match request { - CliRequest::Open { paths, wait } => { - let mut caret_positions = HashMap::default(); - - let paths = if paths.is_empty() { - todo!() - // workspace::last_opened_workspace_paths() - // .await - // .map(|location| location.paths().to_vec()) - // .unwrap_or_default() - } else { - paths - .into_iter() - .filter_map(|path_with_position_string| { - let path_with_position = PathLikeWithPosition::parse_str( - &path_with_position_string, - |path_str| { - Ok::<_, std::convert::Infallible>( - Path::new(path_str).to_path_buf(), - ) - }, - ) - .expect("Infallible"); - let path = path_with_position.path_like; - if let Some(row) = path_with_position.row { - if path.is_file() { - let row = row.saturating_sub(1); - let col = - path_with_position.column.unwrap_or(0).saturating_sub(1); - caret_positions.insert(path.clone(), Point::new(row, col)); - } - } - Some(path) - }) - .collect::>() - }; - - let mut errored = false; - - if let Some(open_paths_task) = cx - .update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) - .log_err() - { - match open_paths_task.await { - Ok((workspace, items)) => { - let mut item_release_futures = Vec::new(); - - for (item, path) in items.into_iter().zip(&paths) { - match item { - Some(Ok(mut item)) => { - if let Some(point) = caret_positions.remove(path) { - todo!() - // if let Some(active_editor) = item.downcast::() { - // active_editor - // .downgrade() - // .update(&mut cx, |editor, cx| { - // let snapshot = - // editor.snapshot(cx).display_snapshot; - // let point = snapshot - // .buffer_snapshot - // .clip_point(point, Bias::Left); - // editor.change_selections( - // Some(Autoscroll::center()), - // cx, - // |s| s.select_ranges([point..point]), - // ); - // }) - // .log_err(); - // } - } - - let released = oneshot::channel(); - cx.update(move |cx| { - item.on_release( - cx, - Box::new(move |_| { - let _ = released.0.send(()); - }), - ) - .detach(); - }) - .ok(); - item_release_futures.push(released.1); - } - Some(Err(err)) => { - responses - .send(CliResponse::Stderr { - message: format!( - "error opening {:?}: {}", - path, err - ), - }) - .log_err(); - errored = true; - } - None => {} - } - } - - if wait { - let executor = cx.background_executor().clone(); - let wait = async move { - if paths.is_empty() { - let (done_tx, done_rx) = oneshot::channel(); - let _subscription = - workspace.update(&mut cx, move |_, cx| { - cx.on_release(|_, _| { - let _ = done_tx.send(()); - }) - }); - let _ = done_rx.await; - } else { - let _ = futures::future::try_join_all(item_release_futures) - .await; - }; - } - .fuse(); - futures::pin_mut!(wait); - - loop { - // Repeatedly check if CLI is still open to avoid wasting resources - // waiting for files or workspaces to close. - let mut timer = executor.timer(Duration::from_secs(1)).fuse(); - futures::select_biased! { - _ = wait => break, - _ = timer => { - if responses.send(CliResponse::Ping).is_err() { - break; - } - } - } - } - } - } - Err(error) => { - errored = true; - responses - .send(CliResponse::Stderr { - message: format!("error opening {:?}: {}", paths, error), - }) - .log_err(); - } - } - - responses - .send(CliResponse::Exit { - status: i32::from(errored), - }) - .log_err(); - } - } - } - } -} - pub fn build_window_options( bounds: Option, display_uuid: Option, From 2fccde5ab692d402dd55aadd51d397777e65d20b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Nov 2023 10:11:13 +0100 Subject: [PATCH 13/23] Remove unused code --- crates/zed2/src/main.rs | 198 ++-------------------------------------- 1 file changed, 6 insertions(+), 192 deletions(-) diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 31bfb1c99a506737604c757e7a27cd39d7ccfd75..79ba132e4ffedaa275d1c3cbdb64cf3692970b13 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -4,17 +4,13 @@ // Allow binary to be called Zed for a nice application menu when running executable directly #![allow(non_snake_case)] -use crate::open_listener::{OpenListener, OpenRequest}; use anyhow::{anyhow, Context as _, Result}; use backtrace::Backtrace; -use cli::{ - ipc::{self, IpcSender}, - CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME, -}; +use cli::FORCE_CLI_MODE_ENV_VAR_NAME; use client::UserStore; use db::kvp::KEY_VALUE_STORE; use fs::RealFs; -use futures::{channel::mpsc, SinkExt, StreamExt}; +use futures::StreamExt; use gpui::{Action, App, AppContext, AsyncAppContext, Context, SemanticVersion, Task}; use isahc::{prelude::Configurable, Request}; use language::LanguageRegistry; @@ -50,8 +46,10 @@ use util::{ }; use uuid::Uuid; use workspace::{AppState, WorkspaceStore}; -use zed2::{build_window_options, initialize_workspace, languages}; -use zed2::{ensure_only_instance, Assets, IsOnlyInstance}; +use zed2::{ + build_window_options, ensure_only_instance, handle_cli_connection, initialize_workspace, + languages, Assets, IsOnlyInstance, OpenListener, OpenRequest, +}; mod open_listener; @@ -762,190 +760,6 @@ fn load_embedded_fonts(cx: &AppContext) { // #[cfg(not(debug_assertions))] // fn watch_file_types(_fs: Arc, _cx: &mut AppContext) {} -fn connect_to_cli( - server_name: &str, -) -> Result<(mpsc::Receiver, IpcSender)> { - let handshake_tx = cli::ipc::IpcSender::::connect(server_name.to_string()) - .context("error connecting to cli")?; - let (request_tx, request_rx) = ipc::channel::()?; - let (response_tx, response_rx) = ipc::channel::()?; - - handshake_tx - .send(IpcHandshake { - requests: request_tx, - responses: response_rx, - }) - .context("error sending ipc handshake")?; - - let (mut async_request_tx, async_request_rx) = - futures::channel::mpsc::channel::(16); - thread::spawn(move || { - while let Ok(cli_request) = request_rx.recv() { - if smol::block_on(async_request_tx.send(cli_request)).is_err() { - break; - } - } - Ok::<_, anyhow::Error>(()) - }); - - Ok((async_request_rx, response_tx)) -} - -async fn handle_cli_connection( - (mut requests, _responses): (mpsc::Receiver, IpcSender), - _app_state: Arc, - mut _cx: AsyncAppContext, -) { - if let Some(request) = requests.next().await { - match request { - CliRequest::Open { paths: _, wait: _ } => { - // let mut caret_positions = HashMap::new(); - - // todo!("workspace") - // let paths = if paths.is_empty() { - // workspace::last_opened_workspace_paths() - // .await - // .map(|location| location.paths().to_vec()) - // .unwrap_or_default() - // } else { - // paths - // .into_iter() - // .filter_map(|path_with_position_string| { - // let path_with_position = PathLikeWithPosition::parse_str( - // &path_with_position_string, - // |path_str| { - // Ok::<_, std::convert::Infallible>( - // Path::new(path_str).to_path_buf(), - // ) - // }, - // ) - // .expect("Infallible"); - // let path = path_with_position.path_like; - // if let Some(row) = path_with_position.row { - // if path.is_file() { - // let row = row.saturating_sub(1); - // let col = - // path_with_position.column.unwrap_or(0).saturating_sub(1); - // caret_positions.insert(path.clone(), Point::new(row, col)); - // } - // } - // Some(path) - // }) - // .collect() - // }; - - // let mut errored = false; - // match cx - // .update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) - // .await - // { - // Ok((workspace, items)) => { - // let mut item_release_futures = Vec::new(); - - // for (item, path) in items.into_iter().zip(&paths) { - // match item { - // Some(Ok(item)) => { - // if let Some(point) = caret_positions.remove(path) { - // if let Some(active_editor) = item.downcast::() { - // active_editor - // .downgrade() - // .update(&mut cx, |editor, cx| { - // let snapshot = - // editor.snapshot(cx).display_snapshot; - // let point = snapshot - // .buffer_snapshot - // .clip_point(point, Bias::Left); - // editor.change_selections( - // Some(Autoscroll::center()), - // cx, - // |s| s.select_ranges([point..point]), - // ); - // }) - // .log_err(); - // } - // } - - // let released = oneshot::channel(); - // cx.update(|cx| { - // item.on_release( - // cx, - // Box::new(move |_| { - // let _ = released.0.send(()); - // }), - // ) - // .detach(); - // }); - // item_release_futures.push(released.1); - // } - // Some(Err(err)) => { - // responses - // .send(CliResponse::Stderr { - // message: format!("error opening {:?}: {}", path, err), - // }) - // .log_err(); - // errored = true; - // } - // None => {} - // } - // } - - // if wait { - // let background = cx.background(); - // let wait = async move { - // if paths.is_empty() { - // let (done_tx, done_rx) = oneshot::channel(); - // if let Some(workspace) = workspace.upgrade(&cx) { - // let _subscription = cx.update(|cx| { - // cx.observe_release(&workspace, move |_, _| { - // let _ = done_tx.send(()); - // }) - // }); - // drop(workspace); - // let _ = done_rx.await; - // } - // } else { - // let _ = - // futures::future::try_join_all(item_release_futures).await; - // }; - // } - // .fuse(); - // futures::pin_mut!(wait); - - // loop { - // // Repeatedly check if CLI is still open to avoid wasting resources - // // waiting for files or workspaces to close. - // let mut timer = background.timer(Duration::from_secs(1)).fuse(); - // futures::select_biased! { - // _ = wait => break, - // _ = timer => { - // if responses.send(CliResponse::Ping).is_err() { - // break; - // } - // } - // } - // } - // } - // } - // Err(error) => { - // errored = true; - // responses - // .send(CliResponse::Stderr { - // message: format!("error opening {:?}: {}", paths, error), - // }) - // .log_err(); - // } - // } - - // responses - // .send(CliResponse::Exit { - // status: i32::from(errored), - // }) - // .log_err(); - } - } - } -} - pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] { // &[ // ("Go to file", &file_finder::Toggle), From 11feda01e31886cdc28348a7d0b8c4a6f26749eb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Nov 2023 10:38:28 +0100 Subject: [PATCH 14/23] Uncomment Editor::new --- crates/editor2/src/editor.rs | 984 +++++++++++++++++------------------ crates/editor2/src/scroll.rs | 6 +- crates/gpui2/src/app.rs | 4 + 3 files changed, 498 insertions(+), 496 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index b6d70a1fe9eff776c34ab7bc6c0d112d2f0aee57..c3aa26cb0cb53a0e08624f71e6fa757c8fde1474 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -41,7 +41,7 @@ use gpui::{ }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; -use inlay_hint_cache::InlayHintCache; +use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; pub use items::MAX_TAB_TITLE_LEN; use itertools::Itertools; pub use language::{char_kind, CharKind}; @@ -60,14 +60,16 @@ use ordered_float::OrderedFloat; use parking_lot::RwLock; use project::{FormatTrigger, Project}; use rpc::proto::*; -use scroll::{autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager}; +use scroll::{ + autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide, +}; use selections_collection::{MutableSelectionsCollection, SelectionsCollection}; use serde::{Deserialize, Serialize}; -use settings::Settings; +use settings::{Settings, SettingsStore}; use std::{ any::TypeId, - cmp::Reverse, - ops::{Deref, DerefMut, Range}, + cmp::{self, Reverse}, + ops::{ControlFlow, Deref, DerefMut, Range}, path::Path, sync::Arc, time::{Duration, Instant}, @@ -1793,8 +1795,8 @@ impl Editor { // 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)); + // let buffer = cx.build_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new())); + // let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); // Self::new(EditorMode::SingleLine, buffer, None, field_editor_style, cx) // } @@ -1802,8 +1804,8 @@ impl Editor { // 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)); + // let buffer = cx.build_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new())); + // let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); // Self::new(EditorMode::Full, buffer, None, field_editor_style, cx) // } @@ -1812,8 +1814,8 @@ impl Editor { // 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)); + // let buffer = cx.build_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new())); + // let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); // Self::new( // EditorMode::AutoHeight { max_lines }, // buffer, @@ -1869,145 +1871,140 @@ impl Editor { // get_field_editor_theme: Option>, cx: &mut ViewContext, ) -> Self { - todo!("old version below") - } - // 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 editor_view_id = cx.view_id(); + let style = cx.text_style(); + let font_size = style.font_size * cx.rem_size(); + let display_map = cx.build_model(|cx| { + // todo!() + // let settings = settings::get::(cx); + // let style = build_style(settings, get_field_editor_theme.as_deref(), None, cx); + DisplayMap::new(buffer.clone(), style.font(), font_size, None, 2, 1, cx) + }); - // let selections = SelectionsCollection::new(display_map.clone(), buffer.clone()); + let selections = SelectionsCollection::new(display_map.clone(), buffer.clone()); - // let blink_manager = cx.add_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx)); + let blink_manager = cx.build_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx)); - // let soft_wrap_mode_override = - // (mode == EditorMode::SingleLine).then(|| language_settings::SoftWrap::None); + 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 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); - // } - // }); - // }), - // ], - // }; + let inlay_hint_settings = inlay_hint_settings( + selections.newest_anchor().head(), + &buffer.read(cx).snapshot(cx), + cx, + ); - // this._subscriptions.extend(project_subscriptions); + let mut this = Self { + handle: cx.view().downgrade(), + 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, cx| { + let active = cx.is_window_active(); + 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.end_selection(cx); - // this.scroll_manager.show_scrollbar(cx); + this._subscriptions.extend(project_subscriptions); - // let editor_created_event = EditorCreated(cx.handle()); - // cx.emit_global(editor_created_event); + this.end_selection(cx); + this.scroll_manager.show_scrollbar(cx); - // if mode == EditorMode::Full { - // let should_auto_hide_scrollbars = cx.platform().should_auto_hide_scrollbars(); - // cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars)); - // } + // todo!("use a different mechanism") + // let editor_created_event = EditorCreated(cx.handle()); + // cx.emit_global(editor_created_event); - // this.report_editor_event("open", None, cx); - // this - // } + if mode == EditorMode::Full { + let should_auto_hide_scrollbars = cx.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, @@ -2635,68 +2632,68 @@ impl Editor { // 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 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::>(); + 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(); - // } + 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_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 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() { @@ -3354,79 +3351,79 @@ impl Editor { // 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; - // } + 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) - // } - // }; + 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); - // } - // } + 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 @@ -3439,47 +3436,47 @@ impl Editor { .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 excerpt_visible_offsets( + &self, + restrict_to_languages: Option<&HashSet>>, + cx: &mut ViewContext, + ) -> HashMap, clock::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 { @@ -3489,17 +3486,17 @@ impl Editor { // } // } - // 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 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, @@ -3897,7 +3894,7 @@ impl Editor { // } // let mut ranges_to_highlight = Vec::new(); - // let excerpt_buffer = cx.add_model(|cx| { + // let excerpt_buffer = cx.build_model(|cx| { // let mut multibuffer = MultiBuffer::new(replica_id).with_title(title); // for (buffer_handle, transaction) in &entries { // let buffer = buffer_handle.read(cx); @@ -7647,7 +7644,7 @@ impl Editor { // let mut locations = locations.into_iter().peekable(); // let mut ranges_to_highlight = Vec::new(); - // let excerpt_buffer = cx.add_model(|cx| { + // let excerpt_buffer = cx.build_model(|cx| { // let mut multibuffer = MultiBuffer::new(replica_id); // while let Some(location) = locations.next() { // let buffer = location.buffer.read(cx); @@ -7972,33 +7969,33 @@ impl Editor { // 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 - // }); + 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)); - // } - // } - // } + 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); @@ -8700,101 +8697,101 @@ impl Editor { // self.blink_manager.read(cx).visible() && self.focused // } - // fn on_buffer_changed(&mut self, _: Model, cx: &mut ViewContext) { - // cx.notify(); - // } + fn on_buffer_changed(&mut self, _: Model, cx: &mut ViewContext) { + cx.notify(); + } - // fn on_buffer_event( - // &mut self, - // multibuffer: Model, - // 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_buffer_event( + &mut self, + multibuffer: Model, + 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, _: Model, cx: &mut ViewContext) { - // cx.notify(); - // } + fn on_display_map_changed(&mut self, _: Model, 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, - // ); - // } + 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; @@ -9871,60 +9868,61 @@ impl InvalidationRegion for SnippetState { // } // } -// pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> RenderBlock { -// let mut highlighted_lines = Vec::new(); +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}")) -// } + 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 = ThemeSettings::get_global(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() -// }) -// } + _ => highlight_diagnostic_message(Vec::new(), line), + }; + highlighted_lines.push(line); + } + let message = diagnostic.message; + Arc::new(move |cx: &mut BlockContext| { + todo!() + // let message = message.clone(); + // let settings = ThemeSettings::get_global(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, diff --git a/crates/editor2/src/scroll.rs b/crates/editor2/src/scroll.rs index 0c6829fbcb3cd8d82acb4040c719507f5799ba8e..d81d33ff23a71f56b86d71588bb85f1ee07523ea 100644 --- a/crates/editor2/src/scroll.rs +++ b/crates/editor2/src/scroll.rs @@ -299,9 +299,9 @@ impl Editor { // cx.notify(); // } - // pub fn visible_line_count(&self) -> Option { - // self.scroll_manager.visible_line_count - // } + 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(); diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index da4d59daed2da6cbc364963e765435f2e6ac8a97..9ec76a86d4e7bd0726cc1a38f98a942728c7e43d 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -373,6 +373,10 @@ impl AppContext { self.platform.reveal_path(path) } + pub fn should_auto_hide_scrollbars(&self) -> bool { + self.platform.should_auto_hide_scrollbars() + } + pub(crate) fn push_effect(&mut self, effect: Effect) { match &effect { Effect::Notify { emitter } => { From 0dfa3c60b726b7736bb759a4e8d2890230a6528e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Nov 2023 10:50:30 +0100 Subject: [PATCH 15/23] Remove some todos --- crates/editor2/src/editor.rs | 29 ++++++------- .../editor2/src/highlight_matching_bracket.rs | 43 +++++++++---------- 2 files changed, 35 insertions(+), 37 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index c3aa26cb0cb53a0e08624f71e6fa757c8fde1474..4bd1a5153860a26d1bd720f1600d17ae36af95df 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -36,8 +36,9 @@ pub use element::{ use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - AnyElement, AppContext, BackgroundExecutor, Context, Element, EventEmitter, Hsla, Model, - Pixels, Render, Subscription, Task, TextStyle, View, ViewContext, WeakView, WindowContext, + AnyElement, AppContext, BackgroundExecutor, Context, Element, EventEmitter, FocusHandle, Hsla, + Model, Pixels, Render, Subscription, Task, TextStyle, View, ViewContext, WeakView, + WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -606,6 +607,7 @@ type InlayBackgroundHighlight = (fn(&ThemeColors) -> Hsla, Vec); pub struct Editor { handle: WeakView, + focus_handle: FocusHandle, buffer: Model, display_map: Model, pub selections: SelectionsCollection, @@ -1912,6 +1914,7 @@ impl Editor { let mut this = Self { handle: cx.view().downgrade(), + focus_handle: cx.focus_handle(), buffer: buffer.clone(), display_map: display_map.clone(), selections, @@ -2066,19 +2069,15 @@ impl Editor { // } pub fn snapshot(&mut self, cx: &mut WindowContext) -> EditorSnapshot { - todo!() - // 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)), - // } + 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.focus_handle.is_focused(cx), + } } // pub fn language_at<'a, T: ToOffset>( diff --git a/crates/editor2/src/highlight_matching_bracket.rs b/crates/editor2/src/highlight_matching_bracket.rs index 5074ee4eda495f2f71614c4e7ade328d1a0a6018..fd8fb6b097099e6a9d47cbfd81f1b2e278e7f405 100644 --- a/crates/editor2/src/highlight_matching_bracket.rs +++ b/crates/editor2/src/highlight_matching_bracket.rs @@ -5,30 +5,29 @@ use crate::{Editor, RangeToAnchorExt}; enum MatchingBracketHighlight {} pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewContext) { - todo!() - // // editor.clear_background_highlights::(cx); + // 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 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, - // ) - // } + 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| todo!("theme.editor.document_highlight_read_background"), + cx, + ) + } } // #[cfg(test)] From 97d1d9bd9b3ba4b9367c8025b99cf101793ceb44 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Nov 2023 10:53:55 +0100 Subject: [PATCH 16/23] Uncomment Editor::report_editor_event --- Cargo.lock | 1 + crates/editor2/Cargo.toml | 1 + crates/editor2/src/editor.rs | 74 ++++++++++++++++++------------------ 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 135e8a7ca94454a72dd59ece021db3e6b2bfd06e..161bc15d97f9ee5b29adba22e07f5add679748e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2657,6 +2657,7 @@ dependencies = [ "schemars", "serde", "serde_derive", + "serde_json", "settings2", "smallvec", "smol", diff --git a/crates/editor2/Cargo.toml b/crates/editor2/Cargo.toml index e356a3515530eb1f92a0fa2ed7c38ec8b7f1768b..121c03ec00922bf9b462630b4b88d34958f9757e 100644 --- a/crates/editor2/Cargo.toml +++ b/crates/editor2/Cargo.toml @@ -62,6 +62,7 @@ postage.workspace = true rand.workspace = true schemars.workspace = true serde.workspace = true +serde_json.workspace = true serde_derive.workspace = true smallvec.workspace = true smol.workspace = true diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 4bd1a5153860a26d1bd720f1600d17ae36af95df..18b5b547dfcbe176fbd67f9f9273a5fa23c0cd1a 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -8981,45 +8981,43 @@ impl Editor { file_extension: Option, cx: &AppContext, ) { - todo!("old version below"); + 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 = *TelemetrySettings::get_global(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) } - // 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. From 6fc7b172599d04d3d1461b8b699bd2b875750ad5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Nov 2023 11:00:57 +0100 Subject: [PATCH 17/23] Expose a focus handle from workspace::Item --- crates/editor2/src/items.rs | 8 ++++++-- crates/workspace2/src/item.rs | 10 ++++++++-- crates/workspace2/src/pane.rs | 8 ++++---- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index b0b344a868354f79a982328116e4095be86743cc..3d5bfabaced2d95c3dc2e62dc09c68eb387a62b4 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -7,8 +7,8 @@ use anyhow::{anyhow, Context, Result}; use collections::HashSet; use futures::future::try_join_all; use gpui::{ - point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, Model, Pixels, SharedString, - Subscription, Task, View, ViewContext, VisualContext, WeakView, + point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, FocusHandle, Model, Pixels, + SharedString, Subscription, Task, View, ViewContext, VisualContext, WeakView, }; use language::{ proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point, @@ -515,6 +515,10 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor) } impl Item for Editor { + fn focus_handle(&self) -> FocusHandle { + self.focus_handle.clone() + } + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { todo!(); // if let Ok(data) = data.downcast::() { diff --git a/crates/workspace2/src/item.rs b/crates/workspace2/src/item.rs index 5d02f5d6fdcc194c0bef7fc57125d3673520d3ff..43ebab4521da35562949c5c3a88bc578d4e77e49 100644 --- a/crates/workspace2/src/item.rs +++ b/crates/workspace2/src/item.rs @@ -12,8 +12,8 @@ use client2::{ Client, }; use gpui2::{ - AnyElement, AnyView, AppContext, Entity, EntityId, EventEmitter, HighlightStyle, Model, Pixels, - Point, Render, SharedString, Task, View, ViewContext, WeakView, WindowContext, + AnyElement, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, HighlightStyle, + Model, Pixels, Point, Render, SharedString, Task, View, ViewContext, WeakView, WindowContext, }; use parking_lot::Mutex; use project2::{Project, ProjectEntryId, ProjectPath}; @@ -91,6 +91,7 @@ pub struct BreadcrumbText { } pub trait Item: Render + EventEmitter { + fn focus_handle(&self) -> FocusHandle; fn deactivated(&mut self, _: &mut ViewContext) {} fn workspace_deactivated(&mut self, _: &mut ViewContext) {} fn navigate(&mut self, _: Box, _: &mut ViewContext) -> bool { @@ -212,6 +213,7 @@ pub trait Item: Render + EventEmitter { } pub trait ItemHandle: 'static + Send { + fn focus_handle(&self, cx: &WindowContext) -> FocusHandle; fn subscribe_to_item_events( &self, cx: &mut WindowContext, @@ -290,6 +292,10 @@ impl dyn ItemHandle { } impl ItemHandle for View { + fn focus_handle(&self, cx: &WindowContext) -> FocusHandle { + self.read(cx).focus_handle() + } + fn subscribe_to_item_events( &self, cx: &mut WindowContext, diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index b30ec0b7f82dba2ea98d1bae2372b87805dcc170..2754df92c32eb05e2e0720a77b6f6c4fe25202b3 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1185,10 +1185,10 @@ impl Pane { } pub fn focus_active_item(&mut self, cx: &mut ViewContext) { - todo!(); - // if let Some(active_item) = self.active_item() { - // cx.focus(active_item.as_any()); - // } + if let Some(active_item) = self.active_item() { + let focus_handle = active_item.focus_handle(cx); + cx.focus(&focus_handle); + } } // pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext) { From bed10b433acc786121da3e512b42ce78618c13a0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Nov 2023 11:36:18 +0100 Subject: [PATCH 18/23] Allow converting from a WeakView to an AnyWeakView --- crates/gpui2/src/view.rs | 84 +++++++++++++++++++++++++++------------- 1 file changed, 57 insertions(+), 27 deletions(-) diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index d81df5b21c9e8a64af4932462423009a0e291fbd..d12d84f43b424f83c5785d7dd0b33c776054bbb3 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -1,7 +1,7 @@ use crate::{ private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, - BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, LayoutId, - Model, Pixels, Size, ViewContext, VisualContext, WeakModel, WindowContext, + Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, LayoutId, Model, Pixels, + Size, ViewContext, VisualContext, WeakModel, WindowContext, }; use anyhow::{Context, Result}; use std::{ @@ -196,31 +196,9 @@ impl From> for AnyView { fn from(value: View) -> Self { AnyView { model: value.model.into_any(), - initialize: |view, cx| { - cx.with_element_id(view.model.entity_id, |_, cx| { - let view = view.clone().downcast::().unwrap(); - let element = view.update(cx, |view, cx| { - let mut element = AnyElement::new(view.render(cx)); - element.initialize(view, cx); - element - }); - Box::new(element) - }) - }, - layout: |view, element, cx| { - cx.with_element_id(view.model.entity_id, |_, cx| { - let view = view.clone().downcast::().unwrap(); - let element = element.downcast_mut::>().unwrap(); - view.update(cx, |view, cx| element.layout(view, cx)) - }) - }, - paint: |view, element, cx| { - cx.with_element_id(view.model.entity_id, |_, cx| { - let view = view.clone().downcast::().unwrap(); - let element = element.downcast_mut::>().unwrap(); - view.update(cx, |view, cx| element.paint(view, cx)) - }) - }, + initialize: any_view::initialize::, + layout: any_view::layout::, + paint: any_view::paint::, } } } @@ -280,6 +258,17 @@ impl AnyWeakView { } } +impl From> for AnyWeakView { + fn from(view: WeakView) -> Self { + Self { + model: view.model.into(), + initialize: any_view::initialize::, + layout: any_view::layout::, + paint: any_view::paint::, + } + } +} + impl Render for T where T: 'static + FnMut(&mut WindowContext) -> E, @@ -291,3 +280,44 @@ where (self)(cx) } } + +mod any_view { + use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext}; + use std::any::Any; + + pub(crate) fn initialize(view: &AnyView, cx: &mut WindowContext) -> Box { + cx.with_element_id(view.model.entity_id, |_, cx| { + let view = view.clone().downcast::().unwrap(); + let element = view.update(cx, |view, cx| { + let mut element = AnyElement::new(view.render(cx)); + element.initialize(view, cx); + element + }); + Box::new(element) + }) + } + + pub(crate) fn layout( + view: &AnyView, + element: &mut Box, + cx: &mut WindowContext, + ) -> LayoutId { + cx.with_element_id(view.model.entity_id, |_, cx| { + let view = view.clone().downcast::().unwrap(); + let element = element.downcast_mut::>().unwrap(); + view.update(cx, |view, cx| element.layout(view, cx)) + }) + } + + pub(crate) fn paint( + view: &AnyView, + element: &mut Box, + cx: &mut WindowContext, + ) { + cx.with_element_id(view.model.entity_id, |_, cx| { + let view = view.clone().downcast::().unwrap(); + let element = element.downcast_mut::>().unwrap(); + view.update(cx, |view, cx| element.paint(view, cx)) + }) + } +} From b7712c2f4b4bfb7d464f4a8957e9633a00d26e6f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Nov 2023 11:36:39 +0100 Subject: [PATCH 19/23] Fix a todo in workspace --- crates/workspace2/src/pane.rs | 32 +- crates/workspace2/src/workspace2.rs | 493 ++++++++++++++-------------- 2 files changed, 265 insertions(+), 260 deletions(-) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 2754df92c32eb05e2e0720a77b6f6c4fe25202b3..41fafa0330a66556acb69c3d66660e677690cee5 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -9,8 +9,8 @@ use crate::{ use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; use gpui2::{ - AppContext, AsyncWindowContext, Component, Div, EntityId, EventEmitter, Model, PromptLevel, - Render, Task, View, ViewContext, VisualContext, WeakView, WindowContext, + AppContext, AsyncWindowContext, Component, Div, EntityId, EventEmitter, FocusHandle, Model, + PromptLevel, Render, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use parking_lot::Mutex; use project2::{Project, ProjectEntryId, ProjectPath}; @@ -171,6 +171,7 @@ impl fmt::Debug for Event { } pub struct Pane { + focus_handle: FocusHandle, items: Vec>, activation_history: Vec, zoomed: bool, @@ -183,7 +184,6 @@ pub struct Pane { // tab_context_menu: ViewHandle, workspace: WeakView, project: Model, - has_focus: bool, // can_drop: Rc, &WindowContext) -> bool>, // can_split: bool, // render_tab_bar_buttons: Rc) -> AnyElement>, @@ -330,6 +330,7 @@ impl Pane { let handle = cx.view().downgrade(); Self { + focus_handle: cx.focus_handle(), items: Vec::new(), activation_history: Vec::new(), zoomed: false, @@ -353,7 +354,6 @@ impl Pane { // tab_context_menu: cx.add_view(|cx| ContextMenu::new(pane_view_id, cx)), workspace, project, - has_focus: false, // can_drop: Rc::new(|_, _| true), // can_split: true, // render_tab_bar_buttons: Rc::new(move |pane, cx| { @@ -420,8 +420,8 @@ impl Pane { // &self.workspace // } - pub fn has_focus(&self) -> bool { - self.has_focus + pub fn has_focus(&self, cx: &WindowContext) -> bool { + self.focus_handle.contains_focused(cx) } // pub fn active_item_index(&self) -> usize { @@ -1020,7 +1020,7 @@ impl Pane { // to activating the item to the left .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1)); - let should_activate = activate_pane || self.has_focus; + let should_activate = activate_pane || self.has_focus(cx); self.activate_item(index_to_activate, should_activate, should_activate, cx); } @@ -1184,6 +1184,10 @@ impl Pane { } } + pub fn focus(&mut self, cx: &mut ViewContext) { + cx.focus(&self.focus_handle); + } + pub fn focus_active_item(&mut self, cx: &mut ViewContext) { if let Some(active_item) = self.active_item() { let focus_handle = active_item.focus_handle(cx); @@ -1865,14 +1869,14 @@ impl Pane { // .into_any() // } - // pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { - // self.zoomed = zoomed; - // cx.notify(); - // } + pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { + self.zoomed = zoomed; + cx.notify(); + } - // pub fn is_zoomed(&self) -> bool { - // self.zoomed - // } + pub fn is_zoomed(&self) -> bool { + self.zoomed + } } // impl Entity for Pane { diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index f5f507f3b15dc3d471347c734ccfc236898d1487..81adebbd65888dd01d4756170893c79a7de9e2bc 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -26,7 +26,7 @@ use client2::{ proto::{self, PeerId}, Client, TypedEnvelope, UserStore, }; -use collections::{HashMap, HashSet}; +use collections::{hash_map, HashMap, HashSet}; use dock::{Dock, DockPosition, PanelButtons}; use futures::{ channel::{mpsc, oneshot}, @@ -35,10 +35,10 @@ use futures::{ }; use gpui2::{ div, point, size, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, - AsyncWindowContext, Bounds, Component, Div, EntityId, EventEmitter, GlobalPixels, Model, - ModelContext, ParentElement, Point, Render, Size, StatefulInteractive, Styled, Subscription, - Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, - WindowOptions, + AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter, FocusHandle, + GlobalPixels, Model, ModelContext, ParentElement, Point, Render, Size, StatefulInteractive, + Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, + WindowContext, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use language2::LanguageRegistry; @@ -547,9 +547,10 @@ pub enum Event { pub struct Workspace { weak_self: WeakView, + focus_handle: FocusHandle, // modal: Option, zoomed: Option, - // zoomed_position: Option, + zoomed_position: Option, center: PaneGroup, left_dock: View, bottom_dock: View, @@ -766,9 +767,10 @@ impl Workspace { cx.defer(|this, cx| this.update_window_title(cx)); Workspace { weak_self: weak_handle.clone(), + focus_handle: cx.focus_handle(), // modal: None, zoomed: None, - // zoomed_position: None, + zoomed_position: None, center: PaneGroup::new(center_pane.clone()), panes: vec![center_pane.clone()], panes_by_item: Default::default(), @@ -1699,9 +1701,9 @@ impl Workspace { self.active_pane().read(cx).active_item() } - // fn active_project_path(&self, cx: &ViewContext) -> Option { - // self.active_item(cx).and_then(|item| item.project_path(cx)) - // } + fn active_project_path(&self, cx: &ViewContext) -> Option { + self.active_item(cx).and_then(|item| item.project_path(cx)) + } // pub fn save_active_item( // &mut self, @@ -1923,44 +1925,44 @@ impl Workspace { // self.zoomed.and_then(|view| view.upgrade(cx)) // } - // fn dismiss_zoomed_items_to_reveal( - // &mut self, - // dock_to_reveal: Option, - // cx: &mut ViewContext, - // ) { - // // If a center pane is zoomed, unzoom it. - // for pane in &self.panes { - // if pane != &self.active_pane || dock_to_reveal.is_some() { - // pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); - // } - // } + fn dismiss_zoomed_items_to_reveal( + &mut self, + dock_to_reveal: Option, + cx: &mut ViewContext, + ) { + // If a center pane is zoomed, unzoom it. + for pane in &self.panes { + if pane != &self.active_pane || dock_to_reveal.is_some() { + pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); + } + } - // // If another dock is zoomed, hide it. - // let mut focus_center = false; - // for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] { - // dock.update(cx, |dock, cx| { - // if Some(dock.position()) != dock_to_reveal { - // if let Some(panel) = dock.active_panel() { - // if panel.is_zoomed(cx) { - // focus_center |= panel.has_focus(cx); - // dock.set_open(false, cx); - // } - // } - // } - // }); - // } + // If another dock is zoomed, hide it. + let mut focus_center = false; + for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] { + dock.update(cx, |dock, cx| { + if Some(dock.position()) != dock_to_reveal { + if let Some(panel) = dock.active_panel() { + if panel.is_zoomed(cx) { + focus_center |= panel.has_focus(cx); + dock.set_open(false, cx); + } + } + } + }); + } - // if focus_center { - // cx.focus_self(); - // } + if focus_center { + cx.focus(&self.focus_handle); + } - // if self.zoomed_position != dock_to_reveal { - // self.zoomed = None; - // self.zoomed_position = None; - // } + if self.zoomed_position != dock_to_reveal { + self.zoomed = None; + self.zoomed_position = None; + } - // cx.notify(); - // } + cx.notify(); + } fn add_pane(&mut self, _cx: &mut ViewContext) -> View { todo!() @@ -2288,214 +2290,213 @@ impl Workspace { // self.center.pane_at_pixel_position(target) // } - // fn handle_pane_focused(&mut self, pane: View, cx: &mut ViewContext) { - // if self.active_pane != pane { - // self.active_pane = pane.clone(); - // self.status_bar.update(cx, |status_bar, cx| { - // status_bar.set_active_pane(&self.active_pane, cx); - // }); - // self.active_item_path_changed(cx); - // self.last_active_center_pane = Some(pane.downgrade()); - // } + fn handle_pane_focused(&mut self, pane: View, cx: &mut ViewContext) { + if self.active_pane != pane { + self.active_pane = pane.clone(); + self.status_bar.update(cx, |status_bar, cx| { + status_bar.set_active_pane(&self.active_pane, cx); + }); + self.active_item_path_changed(cx); + self.last_active_center_pane = Some(pane.downgrade()); + } - // self.dismiss_zoomed_items_to_reveal(None, cx); - // if pane.read(cx).is_zoomed() { - // self.zoomed = Some(pane.downgrade().into_any()); - // } else { - // self.zoomed = None; - // } - // self.zoomed_position = None; - // self.update_active_view_for_followers(cx); + self.dismiss_zoomed_items_to_reveal(None, cx); + if pane.read(cx).is_zoomed() { + self.zoomed = Some(pane.downgrade().into()); + } else { + self.zoomed = None; + } + self.zoomed_position = None; + self.update_active_view_for_followers(cx); - // cx.notify(); - // } + cx.notify(); + } fn handle_pane_event( &mut self, - _pane: View, - _event: &pane::Event, - _cx: &mut ViewContext, + pane: View, + event: &pane::Event, + cx: &mut ViewContext, ) { - todo!() - // match event { - // pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx), - // pane::Event::Split(direction) => { - // self.split_and_clone(pane, *direction, cx); - // } - // pane::Event::Remove => self.remove_pane(pane, cx), - // pane::Event::ActivateItem { local } => { - // if *local { - // self.unfollow(&pane, cx); - // } - // if &pane == self.active_pane() { - // self.active_item_path_changed(cx); - // } - // } - // pane::Event::ChangeItemTitle => { - // if pane == self.active_pane { - // self.active_item_path_changed(cx); - // } - // self.update_window_edited(cx); - // } - // pane::Event::RemoveItem { item_id } => { - // self.update_window_edited(cx); - // if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) { - // if entry.get().id() == pane.id() { - // entry.remove(); - // } - // } - // } - // pane::Event::Focus => { - // self.handle_pane_focused(pane.clone(), cx); - // } - // pane::Event::ZoomIn => { - // if pane == self.active_pane { - // pane.update(cx, |pane, cx| pane.set_zoomed(true, cx)); - // if pane.read(cx).has_focus() { - // self.zoomed = Some(pane.downgrade().into_any()); - // self.zoomed_position = None; - // } - // cx.notify(); - // } - // } - // pane::Event::ZoomOut => { - // pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); - // if self.zoomed_position.is_none() { - // self.zoomed = None; - // } - // cx.notify(); - // } - // } + match event { + pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx), + pane::Event::Split(direction) => { + self.split_and_clone(pane, *direction, cx); + } + pane::Event::Remove => self.remove_pane(pane, cx), + pane::Event::ActivateItem { local } => { + if *local { + self.unfollow(&pane, cx); + } + if &pane == self.active_pane() { + self.active_item_path_changed(cx); + } + } + pane::Event::ChangeItemTitle => { + if pane == self.active_pane { + self.active_item_path_changed(cx); + } + self.update_window_edited(cx); + } + pane::Event::RemoveItem { item_id } => { + self.update_window_edited(cx); + if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) { + if entry.get().entity_id() == pane.entity_id() { + entry.remove(); + } + } + } + pane::Event::Focus => { + self.handle_pane_focused(pane.clone(), cx); + } + pane::Event::ZoomIn => { + if pane == self.active_pane { + pane.update(cx, |pane, cx| pane.set_zoomed(true, cx)); + if pane.read(cx).has_focus(cx) { + self.zoomed = Some(pane.downgrade().into()); + self.zoomed_position = None; + } + cx.notify(); + } + } + pane::Event::ZoomOut => { + pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); + if self.zoomed_position.is_none() { + self.zoomed = None; + } + cx.notify(); + } + } - // self.serialize_workspace(cx); + self.serialize_workspace(cx); } - // pub fn split_pane( - // &mut self, - // pane_to_split: View, - // split_direction: SplitDirection, - // cx: &mut ViewContext, - // ) -> View { - // let new_pane = self.add_pane(cx); - // self.center - // .split(&pane_to_split, &new_pane, split_direction) - // .unwrap(); - // cx.notify(); - // new_pane - // } - - // pub fn split_and_clone( - // &mut self, - // pane: View, - // direction: SplitDirection, - // cx: &mut ViewContext, - // ) -> Option> { - // let item = pane.read(cx).active_item()?; - // let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) { - // let new_pane = self.add_pane(cx); - // new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx)); - // self.center.split(&pane, &new_pane, direction).unwrap(); - // Some(new_pane) - // } else { - // None - // }; - // cx.notify(); - // maybe_pane_handle - // } + pub fn split_pane( + &mut self, + pane_to_split: View, + split_direction: SplitDirection, + cx: &mut ViewContext, + ) -> View { + let new_pane = self.add_pane(cx); + self.center + .split(&pane_to_split, &new_pane, split_direction) + .unwrap(); + cx.notify(); + new_pane + } - // pub fn split_pane_with_item( - // &mut self, - // pane_to_split: WeakView, - // split_direction: SplitDirection, - // from: WeakView, - // item_id_to_move: usize, - // cx: &mut ViewContext, - // ) { - // let Some(pane_to_split) = pane_to_split.upgrade(cx) else { - // return; - // }; - // let Some(from) = from.upgrade(cx) else { - // return; - // }; + pub fn split_and_clone( + &mut self, + pane: View, + direction: SplitDirection, + cx: &mut ViewContext, + ) -> Option> { + let item = pane.read(cx).active_item()?; + let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) { + let new_pane = self.add_pane(cx); + new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx)); + self.center.split(&pane, &new_pane, direction).unwrap(); + Some(new_pane) + } else { + None + }; + cx.notify(); + maybe_pane_handle + } - // let new_pane = self.add_pane(cx); - // self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx); - // self.center - // .split(&pane_to_split, &new_pane, split_direction) - // .unwrap(); - // cx.notify(); - // } + pub fn split_pane_with_item( + &mut self, + pane_to_split: WeakView, + split_direction: SplitDirection, + from: WeakView, + item_id_to_move: EntityId, + cx: &mut ViewContext, + ) { + let Some(pane_to_split) = pane_to_split.upgrade() else { + return; + }; + let Some(from) = from.upgrade() else { + return; + }; - // pub fn split_pane_with_project_entry( - // &mut self, - // pane_to_split: WeakView, - // split_direction: SplitDirection, - // project_entry: ProjectEntryId, - // cx: &mut ViewContext, - // ) -> Option>> { - // let pane_to_split = pane_to_split.upgrade(cx)?; - // let new_pane = self.add_pane(cx); - // self.center - // .split(&pane_to_split, &new_pane, split_direction) - // .unwrap(); - - // let path = self.project.read(cx).path_for_entry(project_entry, cx)?; - // let task = self.open_path(path, Some(new_pane.downgrade()), true, cx); - // Some(cx.foreground().spawn(async move { - // task.await?; - // Ok(()) - // })) - // } + let new_pane = self.add_pane(cx); + self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx); + self.center + .split(&pane_to_split, &new_pane, split_direction) + .unwrap(); + cx.notify(); + } - // pub fn move_item( - // &mut self, - // source: View, - // destination: View, - // item_id_to_move: usize, - // destination_index: usize, - // cx: &mut ViewContext, - // ) { - // let item_to_move = source - // .read(cx) - // .items() - // .enumerate() - // .find(|(_, item_handle)| item_handle.id() == item_id_to_move); - - // if item_to_move.is_none() { - // log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop"); - // return; - // } - // let (item_ix, item_handle) = item_to_move.unwrap(); - // let item_handle = item_handle.clone(); + pub fn split_pane_with_project_entry( + &mut self, + pane_to_split: WeakView, + split_direction: SplitDirection, + project_entry: ProjectEntryId, + cx: &mut ViewContext, + ) -> Option>> { + let pane_to_split = pane_to_split.upgrade()?; + let new_pane = self.add_pane(cx); + self.center + .split(&pane_to_split, &new_pane, split_direction) + .unwrap(); + + let path = self.project.read(cx).path_for_entry(project_entry, cx)?; + let task = self.open_path(path, Some(new_pane.downgrade()), true, cx); + Some(cx.foreground_executor().spawn(async move { + task.await?; + Ok(()) + })) + } - // if source != destination { - // // Close item from previous pane - // source.update(cx, |source, cx| { - // source.remove_item(item_ix, false, cx); - // }); - // } + pub fn move_item( + &mut self, + source: View, + destination: View, + item_id_to_move: EntityId, + destination_index: usize, + cx: &mut ViewContext, + ) { + let item_to_move = source + .read(cx) + .items() + .enumerate() + .find(|(_, item_handle)| item_handle.id() == item_id_to_move); + + if item_to_move.is_none() { + log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop"); + return; + } + let (item_ix, item_handle) = item_to_move.unwrap(); + let item_handle = item_handle.clone(); + + if source != destination { + // Close item from previous pane + source.update(cx, |source, cx| { + source.remove_item(item_ix, false, cx); + }); + } - // // This automatically removes duplicate items in the pane - // destination.update(cx, |destination, cx| { - // destination.add_item(item_handle, true, true, Some(destination_index), cx); - // cx.focus_self(); - // }); - // } + // This automatically removes duplicate items in the pane + destination.update(cx, |destination, cx| { + destination.add_item(item_handle, true, true, Some(destination_index), cx); + destination.focus(cx) + }); + } - // fn remove_pane(&mut self, pane: View, cx: &mut ViewContext) { - // if self.center.remove(&pane).unwrap() { - // self.force_remove_pane(&pane, cx); - // self.unfollow(&pane, cx); - // self.last_leaders_by_pane.remove(&pane.downgrade()); - // for removed_item in pane.read(cx).items() { - // self.panes_by_item.remove(&removed_item.id()); - // } + fn remove_pane(&mut self, pane: View, cx: &mut ViewContext) { + if self.center.remove(&pane).unwrap() { + self.force_remove_pane(&pane, cx); + self.unfollow(&pane, cx); + self.last_leaders_by_pane.remove(&pane.downgrade()); + for removed_item in pane.read(cx).items() { + self.panes_by_item.remove(&removed_item.id()); + } - // cx.notify(); - // } else { - // self.active_item_path_changed(cx); - // } - // } + cx.notify(); + } else { + self.active_item_path_changed(cx); + } + } pub fn panes(&self) -> &[View] { &self.panes @@ -2708,12 +2709,12 @@ impl Workspace { .child("Collab title bar Item") // self.titlebar_item } - // fn active_item_path_changed(&mut self, cx: &mut ViewContext) { - // let active_entry = self.active_project_path(cx); - // self.project - // .update(cx, |project, cx| project.set_active_path(active_entry, cx)); - // self.update_window_title(cx); - // } + fn active_item_path_changed(&mut self, cx: &mut ViewContext) { + let active_entry = self.active_project_path(cx); + self.project + .update(cx, |project, cx| project.set_active_path(active_entry, cx)); + self.update_window_title(cx); + } fn update_window_title(&mut self, cx: &mut ViewContext) { let project = self.project().read(cx); @@ -3010,7 +3011,7 @@ impl Workspace { fn update_active_view_for_followers(&mut self, cx: &mut ViewContext) { let mut is_project_item = true; let mut update = proto::UpdateActiveView::default(); - if self.active_pane.read(cx).has_focus() { + if self.active_pane.read(cx).has_focus(cx) { let item = self .active_item(cx) .and_then(|item| item.to_followable_item_handle(cx)); @@ -3105,7 +3106,7 @@ impl Workspace { } for (pane, item) in items_to_activate { - let pane_was_focused = pane.read(cx).has_focus(); + let pane_was_focused = pane.read(cx).has_focus(cx); if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) { pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx)); } else { @@ -3242,7 +3243,7 @@ impl Workspace { // } fn serialize_workspace(&self, cx: &mut ViewContext) { - fn serialize_pane_handle(pane_handle: &View, cx: &AppContext) -> SerializedPane { + fn serialize_pane_handle(pane_handle: &View, cx: &WindowContext) -> SerializedPane { let (items, active) = { let pane = pane_handle.read(cx); let active_item_id = pane.active_item().map(|item| item.id()); @@ -3256,7 +3257,7 @@ impl Workspace { }) }) .collect::>(), - pane.has_focus(), + pane.has_focus(cx), ) }; @@ -3265,7 +3266,7 @@ impl Workspace { fn build_serialized_pane_group( pane_group: &Member, - cx: &AppContext, + cx: &WindowContext, ) -> SerializedPaneGroup { match pane_group { Member::Axis(PaneAxis { From 77e3c7f8ee87b5608dbc027e7552d42a59bc6ce1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Nov 2023 12:10:15 +0100 Subject: [PATCH 20/23] WIP --- crates/editor2/src/items.rs | 2 +- crates/workspace2/src/item.rs | 162 +++++++++++++++++----------------- 2 files changed, 84 insertions(+), 80 deletions(-) diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 3d5bfabaced2d95c3dc2e62dc09c68eb387a62b4..637877b971c5e9478d72073d740ced550b6a3f01 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -576,7 +576,7 @@ impl Item for Editor { } fn tab_content(&self, detail: Option, cx: &AppContext) -> AnyElement { - todo!(); + AnyElement::new(gpui::ParentElement::child(gpui::div(), "HEY")) // Flex::row() // .with_child(Label::new(self.title(cx).to_string(), style.label.clone()).into_any()) diff --git a/crates/workspace2/src/item.rs b/crates/workspace2/src/item.rs index 43ebab4521da35562949c5c3a88bc578d4e77e49..c3013595e6d36922cada65f78a3292877370f8b5 100644 --- a/crates/workspace2/src/item.rs +++ b/crates/workspace2/src/item.rs @@ -15,7 +15,6 @@ use gpui2::{ AnyElement, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, HighlightStyle, Model, Pixels, Point, Render, SharedString, Task, View, ViewContext, WeakView, WindowContext, }; -use parking_lot::Mutex; use project2::{Project, ProjectEntryId, ProjectPath}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -23,8 +22,10 @@ use settings2::Settings; use smallvec::SmallVec; use std::{ any::{Any, TypeId}, + cell::RefCell, ops::Range, path::PathBuf, + rc::Rc, sync::{ atomic::{AtomicBool, Ordering}, Arc, @@ -412,91 +413,94 @@ impl ItemHandle for View { .is_none() { let mut pending_autosave = DelayedDebouncedEditAction::new(); - let pending_update = Arc::new(Mutex::new(None)); + let pending_update = Rc::new(RefCell::new(None)); let pending_update_scheduled = Arc::new(AtomicBool::new(false)); - let event_subscription = Some(cx.subscribe(self, move |workspace, item, event, cx| { - let pane = if let Some(pane) = workspace - .panes_by_item - .get(&item.id()) - .and_then(|pane| pane.upgrade()) - { - pane - } else { - log::error!("unexpected item event after pane was dropped"); - return; - }; - - if let Some(item) = item.to_followable_item_handle(cx) { - let _is_project_item = item.is_project_item(cx); - let leader_id = workspace.leader_for_pane(&pane); - - if leader_id.is_some() && item.should_unfollow_on_event(event, cx) { - workspace.unfollow(&pane, cx); - } - - if item.add_event_to_update_proto(event, &mut *pending_update.lock(), cx) - && !pending_update_scheduled.load(Ordering::SeqCst) + let mut event_subscription = + Some(cx.subscribe(self, move |workspace, item, event, cx| { + let pane = if let Some(pane) = workspace + .panes_by_item + .get(&item.id()) + .and_then(|pane| pane.upgrade()) { - pending_update_scheduled.store(true, Ordering::SeqCst); - todo!("replace with on_next_frame?"); - // cx.after_window_update({ - // let pending_update = pending_update.clone(); - // let pending_update_scheduled = pending_update_scheduled.clone(); - // move |this, cx| { - // pending_update_scheduled.store(false, Ordering::SeqCst); - // this.update_followers( - // is_project_item, - // proto::update_followers::Variant::UpdateView( - // proto::UpdateView { - // id: item - // .remote_id(&this.app_state.client, cx) - // .map(|id| id.to_proto()), - // variant: pending_update.borrow_mut().take(), - // leader_id, - // }, - // ), - // cx, - // ); - // } - // }); - } - } - - for item_event in T::to_item_events(event).into_iter() { - match item_event { - ItemEvent::CloseItem => { - pane.update(cx, |pane, cx| { - pane.close_item_by_id(item.id(), crate::SaveIntent::Close, cx) - }) - .detach_and_log_err(cx); - return; + pane + } else { + log::error!("unexpected item event after pane was dropped"); + return; + }; + + if let Some(item) = item.to_followable_item_handle(cx) { + let is_project_item = item.is_project_item(cx); + let leader_id = workspace.leader_for_pane(&pane); + + if leader_id.is_some() && item.should_unfollow_on_event(event, cx) { + workspace.unfollow(&pane, cx); } - ItemEvent::UpdateTab => { - pane.update(cx, |_, cx| { - cx.emit(pane::Event::ChangeItemTitle); - cx.notify(); + if item.add_event_to_update_proto( + event, + &mut *pending_update.borrow_mut(), + cx, + ) && !pending_update_scheduled.load(Ordering::SeqCst) + { + pending_update_scheduled.store(true, Ordering::SeqCst); + cx.on_next_frame({ + let pending_update = pending_update.clone(); + let pending_update_scheduled = pending_update_scheduled.clone(); + move |this, cx| { + pending_update_scheduled.store(false, Ordering::SeqCst); + this.update_followers( + is_project_item, + proto::update_followers::Variant::UpdateView( + proto::UpdateView { + id: item + .remote_id(&this.app_state.client, cx) + .map(|id| id.to_proto()), + variant: pending_update.borrow_mut().take(), + leader_id, + }, + ), + cx, + ); + } }); } + } - ItemEvent::Edit => { - let autosave = WorkspaceSettings::get_global(cx).autosave; - if let AutosaveSetting::AfterDelay { milliseconds } = autosave { - let delay = Duration::from_millis(milliseconds); - let item = item.clone(); - pending_autosave.fire_new(delay, cx, move |workspace, cx| { - Pane::autosave_item(&item, workspace.project().clone(), cx) + for item_event in T::to_item_events(event).into_iter() { + match item_event { + ItemEvent::CloseItem => { + pane.update(cx, |pane, cx| { + pane.close_item_by_id(item.id(), crate::SaveIntent::Close, cx) + }) + .detach_and_log_err(cx); + return; + } + + ItemEvent::UpdateTab => { + pane.update(cx, |_, cx| { + cx.emit(pane::Event::ChangeItemTitle); + cx.notify(); }); } - } - _ => {} + ItemEvent::Edit => { + let autosave = WorkspaceSettings::get_global(cx).autosave; + if let AutosaveSetting::AfterDelay { milliseconds } = autosave { + let delay = Duration::from_millis(milliseconds); + let item = item.clone(); + pending_autosave.fire_new(delay, cx, move |workspace, cx| { + Pane::autosave_item(&item, workspace.project().clone(), cx) + }); + } + } + + _ => {} + } } - } - })); + })); - todo!("observe focus"); + // todo!() // cx.observe_focus(self, move |workspace, item, focused, cx| { // if !focused // && WorkspaceSettings::get_global(cx).autosave == AutosaveSetting::OnFocusChange @@ -507,12 +511,12 @@ impl ItemHandle for View { // }) // .detach(); - // let item_id = self.id(); - // cx.observe_release(self, move |workspace, _, _| { - // workspace.panes_by_item.remove(&item_id); - // event_subscription.take(); - // }) - // .detach(); + let item_id = self.id(); + cx.observe_release(self, move |workspace, _, _| { + workspace.panes_by_item.remove(&item_id); + event_subscription.take(); + }) + .detach(); } cx.defer(|workspace, cx| { From feaab953a8d74206423044db83c60300c7a32678 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Nov 2023 14:41:37 +0100 Subject: [PATCH 21/23] Add `ViewContext::window_context` --- crates/gpui2/src/window.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 8f9538001de6536f9c487edfaed02244f804b137..2284f3ccc2269c21fe55a32b3ad3d1f4b1b3ca4a 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1664,6 +1664,11 @@ impl<'a, V: 'static> ViewContext<'a, V> { self.view.model.clone() } + /// Access the underlying window context. + pub fn window_context(&mut self) -> &mut WindowContext<'a> { + &mut self.window_cx + } + pub fn stack(&mut self, order: u32, f: impl FnOnce(&mut Self) -> R) -> R { self.window.z_index_stack.push(order); let result = f(self); From 3e8fcefaef136033fd6de74a8ac247671293f779 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Nov 2023 14:42:26 +0100 Subject: [PATCH 22/23] Remove more todos --- crates/editor2/src/editor.rs | 1085 ++++++++++--------- crates/editor2/src/items.rs | 41 +- crates/editor2/src/link_go_to_definition.rs | 538 +++++---- crates/editor2/src/scroll.rs | 3 +- crates/workspace2/src/pane.rs | 26 +- crates/workspace2/src/workspace2.rs | 142 +-- 6 files changed, 915 insertions(+), 920 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 18b5b547dfcbe176fbd67f9f9273a5fa23c0cd1a..ea747de5de689e6050581aecd5a618acd79ef9a5 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -21,7 +21,7 @@ mod editor_tests; #[cfg(any(test, feature = "test-support"))] pub mod test; use aho_corasick::AhoCorasick; -use anyhow::Result; +use anyhow::{Context as _, Result}; use blink_manager::BlinkManager; use client::{ClickhouseEvent, Client, Collaborator, ParticipantIndex, TelemetrySettings}; use clock::ReplicaId; @@ -36,9 +36,9 @@ pub use element::{ use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - AnyElement, AppContext, BackgroundExecutor, Context, Element, EventEmitter, FocusHandle, Hsla, - Model, Pixels, Render, Subscription, Task, TextStyle, View, ViewContext, WeakView, - WindowContext, + div, AnyElement, AppContext, BackgroundExecutor, Context, Div, Element, EventEmitter, + FocusHandle, Hsla, Model, Pixels, Render, Subscription, Task, TextStyle, View, ViewContext, + VisualContext, WeakView, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -48,10 +48,11 @@ use itertools::Itertools; pub use language::{char_kind, CharKind}; use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, - AutoindentMode, BracketPair, Buffer, CodeAction, Completion, CursorShape, Diagnostic, Language, - LanguageRegistry, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId, + point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, Completion, CursorShape, + Diagnostic, Language, LanguageRegistry, LanguageServerName, OffsetRangeExt, Point, Selection, + SelectionGoal, TransactionId, }; -use link_go_to_definition::{InlayHighlight, LinkGoToDefinitionState}; +use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState}; use lsp::{Documentation, LanguageServerId}; pub use multi_buffer::{ Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset, @@ -59,7 +60,7 @@ pub use multi_buffer::{ }; use ordered_float::OrderedFloat; use parking_lot::RwLock; -use project::{FormatTrigger, Project}; +use project::{FormatTrigger, Location, Project}; use rpc::proto::*; use scroll::{ autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide, @@ -69,6 +70,7 @@ use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; use std::{ any::TypeId, + borrow::Cow, cmp::{self, Reverse}, ops::{ControlFlow, Deref, DerefMut, Range}, path::Path, @@ -80,7 +82,7 @@ use sum_tree::TreeMap; use text::Rope; use theme::ThemeColors; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; -use workspace::{ItemNavHistory, ViewId, Workspace}; +use workspace::{ItemNavHistory, SplitDirection, ViewId, Workspace}; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); const MAX_LINE_LEN: usize = 1024; @@ -389,6 +391,10 @@ impl InlayId { // todo!(revisit these actions) pub struct ShowCompletions; pub struct Rename; +pub struct GoToDefinition; +pub struct GoToTypeDefinition; +pub struct GoToDefinitionSplit; +pub struct GoToTypeDefinitionSplit; enum DocumentHighlightRead {} enum DocumentHighlightWrite {} @@ -561,7 +567,7 @@ pub enum SelectPhase { Update { position: DisplayPoint, goal_column: u32, - scroll_position: gpui::Point, + scroll_position: gpui::Point, }, End, } @@ -1836,13 +1842,13 @@ impl Editor { Self::new(EditorMode::Full, buffer, project, cx) } - // pub fn for_multibuffer( - // buffer: Model, - // project: Option>, - // cx: &mut ViewContext, - // ) -> Self { - // Self::new(EditorMode::Full, buffer, project, None, cx) - // } + pub fn for_multibuffer( + buffer: Model, + project: Option>, + cx: &mut ViewContext, + ) -> Self { + Self::new(EditorMode::Full, buffer, project, cx) + } pub fn clone(&self, cx: &mut ViewContext) -> Self { let mut clone = Self::new( @@ -2048,9 +2054,9 @@ impl Editor { // } // } - // pub fn replica_id(&self, cx: &AppContext) -> ReplicaId { - // self.buffer.read(cx).replica_id() - // } + 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 @@ -2060,13 +2066,13 @@ impl Editor { &self.buffer } - // fn workspace(&self, cx: &AppContext) -> Option> { - // self.workspace.as_ref()?.0.upgrade(cx) - // } + fn workspace(&self) -> Option> { + self.workspace.as_ref()?.0.upgrade() + } - // pub fn title<'a>(&self, cx: &'a AppContext) -> Cow<'a, str> { - // self.buffer().read(cx).title(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 { @@ -2140,12 +2146,12 @@ impl Editor { // 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 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 { @@ -2383,253 +2389,253 @@ impl Editor { // }); // } - // 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); + 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), + } + } - // 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; - // } + 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, - // _ => {} - // } + 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) - // }); - // } + 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(); - // } + fn begin_selection( + &mut self, + position: DisplayPoint, + add: bool, + click_count: usize, + cx: &mut ViewContext, + ) { + if !self.focused { + cx.focus(&self.focus_handle); + } - // 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; - // } - // } + 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) - // } + 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); - // }); - // } + 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(); - // } + fn begin_columnar_selection( + &mut self, + position: DisplayPoint, + goal_column: u32, + cx: &mut ViewContext, + ) { + if !self.focused { + cx.focus(&self.focus_handle); + } - // 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)); + 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, - // ); - // } + 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)); + fn update_selection( + &mut self, + position: DisplayPoint, + goal_column: u32, + scroll_position: gpui::Point, + 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 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 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 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 <= 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; - // } + 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.change_selections(None, cx, |s| { + s.set_pending(pending, mode); + }); + } else { + log::error!("update_selection dispatched with no pending selection"); + return; + } - // self.set_scroll_position(scroll_position, cx); - // cx.notify(); - // } + self.set_scroll_position(scroll_position, cx); + cx.notify(); + } fn end_selection(&mut self, cx: &mut ViewContext) { self.columnar_selection_tail.take(); @@ -7347,239 +7353,233 @@ impl Editor { // } // } - // 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_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_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_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); - // } + 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; - // }; + fn go_to_definition_of_kind( + &mut self, + kind: GotoDefinitionKind, + split: bool, + cx: &mut ViewContext, + ) { + let Some(workspace) = self.workspace() 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), - // }); + 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); - // } + cx.spawn(|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, - // ) - // }); + pub fn navigate_to_definitions( + &mut self, + mut definitions: Vec, + split: bool, + cx: &mut ViewContext, + ) { + let Some(workspace) = self.workspace() 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: View = + 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); - // } - // } + 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))); - // }; + 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) - // }) - // } + 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, @@ -7629,65 +7629,65 @@ impl Editor { // )) // } - // /// 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.build_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; - // } - // } + /// 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.build_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, - // )) - // } + 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) - // }); + 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); - // } - // } + let editor = cx.build_view(|cx| { + Editor::for_multibuffer(excerpt_buffer, Some(workspace.project().clone()), cx) + }); + editor.update(cx, |editor, cx| { + editor.highlight_background::( + ranges_to_highlight, + |theme| todo!("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 _; @@ -9321,10 +9321,11 @@ impl EventEmitter for Editor { } impl Render for Editor { - type Element = EditorElement; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - todo!() + // todo!() + div() } } diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 637877b971c5e9478d72073d740ced550b6a3f01..c439adcc440f75a559240810db512bd9f9a07874 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -7,8 +7,9 @@ use anyhow::{anyhow, Context, Result}; use collections::HashSet; use futures::future::try_join_all; use gpui::{ - point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, FocusHandle, Model, Pixels, - SharedString, Subscription, Task, View, ViewContext, VisualContext, WeakView, + div, point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, FocusHandle, Model, + ParentElement, Pixels, SharedString, Styled, Subscription, Task, View, ViewContext, + VisualContext, WeakView, }; use language::{ proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point, @@ -26,7 +27,7 @@ use std::{ sync::Arc, }; use text::Selection; -use theme::ThemeVariant; +use theme::{ActiveTheme, ThemeVariant}; use util::{paths::PathExt, ResultExt, TryFutureExt}; use workspace::item::{BreadcrumbText, FollowableItemHandle}; use workspace::{ @@ -576,25 +577,21 @@ impl Item for Editor { } fn tab_content(&self, detail: Option, cx: &AppContext) -> AnyElement { - AnyElement::new(gpui::ParentElement::child(gpui::div(), "HEY")) - - // 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() + let theme = cx.theme(); + AnyElement::new( + div() + .flex() + .flex_row() + .items_center() + .bg(gpui::white()) + .text_color(gpui::white()) + .child(self.title(cx).to_string()) + .children(detail.and_then(|detail| { + let path = path_for_buffer(&self.buffer, detail, false, cx)?; + let description = path.to_string_lossy(); + Some(util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN)) + })), + ) } fn for_each_project_item( diff --git a/crates/editor2/src/link_go_to_definition.rs b/crates/editor2/src/link_go_to_definition.rs index f9a54978464ac859d6502dd44ffde4cde1f7a075..8c285c8214c1948435daf7f81e1b38c073be4baa 100644 --- a/crates/editor2/src/link_go_to_definition.rs +++ b/crates/editor2/src/link_go_to_definition.rs @@ -2,7 +2,8 @@ use crate::{ display_map::DisplaySnapshot, element::PointForPosition, hover_popover::{self, InlayHover}, - Anchor, DisplayPoint, Editor, EditorSnapshot, InlayId, SelectPhase, + Anchor, DisplayPoint, Editor, EditorSnapshot, GoToDefinition, GoToTypeDefinition, InlayId, + SelectPhase, }; use gpui::{Task, ViewContext}; use language::{Bias, ToOffset}; @@ -108,61 +109,59 @@ pub fn update_go_to_definition_link( shift_held: bool, cx: &mut ViewContext, ) { - todo!("old version below"); -} -// 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; -// } -// } -// _ => {} -// } -// } + 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(); + editor.link_go_to_definition_state.last_trigger_point = trigger_point.clone(); -// if pending_nonempty_selection { -// hide_link_definition(editor, cx); -// return; -// } + 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; -// } -// } + 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); -// } + hide_link_definition(editor, cx); +} pub fn update_inlay_link_and_hover_points( snapshot: &DisplaySnapshot, @@ -353,204 +352,202 @@ pub fn show_link_definition( snapshot: EditorSnapshot, cx: &mut ViewContext, ) { - todo!("old implementation below") -} -// 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 same_kind = editor.link_go_to_definition_state.kind == Some(definition_kind); + if !same_kind { + hide_link_definition(editor, cx); + } -// let project = if let Some(project) = editor.project.clone() { -// project -// } else { -// return; -// }; + if editor.pending_rename.is_some() { + 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 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) -// } + let task = cx.spawn(|this, mut cx| { + async move { + let result = match &trigger_point { + TriggerPoint::Text(_) => { + // query the LSP for definition info + project + .update(&mut 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 { + // todo!() + // // 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); + } + } + })?; -// 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, -// )], -// )), -// }; + Ok::<_, anyhow::Error>(()) + } + .log_err() + }); -// 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 { -// // todo!() -// // // 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); -// } -// } -// })?; + editor.link_go_to_definition_state.task = Some(task); +} -// Ok::<_, anyhow::Error>(()) -// } -// .log_err() -// }); +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 = Some(task); -// } + editor.link_go_to_definition_state.task = None; -pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext) { - todo!() - // 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); + editor.clear_highlights::(cx); } pub fn go_to_fetched_definition( @@ -578,35 +575,34 @@ fn go_to_fetched_definition_of_kind( split: bool, cx: &mut ViewContext, ) { - todo!(); - // 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), - // } - // } - // } + 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(&editor.focus_handle); + } + + 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(&GoToDefinition, cx), + LinkDefinitionKind::Type => editor.go_to_type_definition(&GoToTypeDefinition, cx), + } + } + } } // #[cfg(test)] diff --git a/crates/editor2/src/scroll.rs b/crates/editor2/src/scroll.rs index d81d33ff23a71f56b86d71588bb85f1ee07523ea..5e4b32265a221cb2561134776fac7bb3fc266931 100644 --- a/crates/editor2/src/scroll.rs +++ b/crates/editor2/src/scroll.rs @@ -346,8 +346,7 @@ impl Editor { cx, ); - // todo!() - // self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); + self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); } // pub fn scroll_position(&self, cx: &mut ViewContext) -> gpui::Point { diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 41fafa0330a66556acb69c3d66660e677690cee5..f940108c5a1f4c61cf6908c577ae8df69eba35ff 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -639,19 +639,19 @@ impl Pane { // .pixel_position_of_cursor(cx) // } - // pub fn item_for_entry( - // &self, - // entry_id: ProjectEntryId, - // cx: &AppContext, - // ) -> Option> { - // self.items.iter().find_map(|item| { - // if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] { - // Some(item.boxed_clone()) - // } else { - // None - // } - // }) - // } + pub fn item_for_entry( + &self, + entry_id: ProjectEntryId, + cx: &AppContext, + ) -> Option> { + self.items.iter().find_map(|item| { + if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] { + Some(item.boxed_clone()) + } else { + None + } + }) + } pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option { self.items.iter().position(|i| i.id() == item.id()) diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 81adebbd65888dd01d4756170893c79a7de9e2bc..2d0ca4529f06528b3f1a640cc6d8c41945475df5 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -1999,22 +1999,22 @@ impl Workspace { // } // } - // pub fn add_item(&mut self, item: Box, cx: &mut ViewContext) { - // self.active_pane - // .update(cx, |pane, cx| pane.add_item(item, true, true, None, cx)); - // } + pub fn add_item(&mut self, item: Box, cx: &mut ViewContext) { + self.active_pane + .update(cx, |pane, cx| pane.add_item(item, true, true, None, cx)); + } - // pub fn split_item( - // &mut self, - // split_direction: SplitDirection, - // item: Box, - // cx: &mut ViewContext, - // ) { - // let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx); - // new_pane.update(cx, move |new_pane, cx| { - // new_pane.add_item(item, true, true, None, cx) - // }) - // } + pub fn split_item( + &mut self, + split_direction: SplitDirection, + item: Box, + cx: &mut ViewContext, + ) { + let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx); + new_pane.update(cx, move |new_pane, cx| { + new_pane.add_item(item, true, true, None, cx) + }) + } // pub fn open_abs_path( // &mut self, @@ -2144,53 +2144,55 @@ impl Workspace { }) } - // pub fn open_project_item( - // &mut self, - // project_item: ModelHandle, - // cx: &mut ViewContext, - // ) -> View - // where - // T: ProjectItem, - // { - // use project::Item as _; + pub fn open_project_item( + &mut self, + project_item: Model, + cx: &mut ViewContext, + ) -> View + where + T: ProjectItem, + { + use project2::Item as _; - // let entry_id = project_item.read(cx).entry_id(cx); - // if let Some(item) = entry_id - // .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx)) - // .and_then(|item| item.downcast()) - // { - // self.activate_item(&item, cx); - // return item; - // } + let entry_id = project_item.read(cx).entry_id(cx); + if let Some(item) = entry_id + .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx)) + .and_then(|item| item.downcast()) + { + self.activate_item(&item, cx); + return item; + } - // let item = cx.build_view(|cx| T::for_project_item(self.project().clone(), project_item, cx)); - // self.add_item(Box::new(item.clone()), cx); - // item - // } + let item = + cx.build_view(|cx| T::for_project_item(self.project().clone(), project_item, cx)); + self.add_item(Box::new(item.clone()), cx); + item + } - // pub fn split_project_item( - // &mut self, - // project_item: ModelHandle, - // cx: &mut ViewContext, - // ) -> View - // where - // T: ProjectItem, - // { - // use project::Item as _; + pub fn split_project_item( + &mut self, + project_item: Model, + cx: &mut ViewContext, + ) -> View + where + T: ProjectItem, + { + use project2::Item as _; - // let entry_id = project_item.read(cx).entry_id(cx); - // if let Some(item) = entry_id - // .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx)) - // .and_then(|item| item.downcast()) - // { - // self.activate_item(&item, cx); - // return item; - // } + let entry_id = project_item.read(cx).entry_id(cx); + if let Some(item) = entry_id + .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx)) + .and_then(|item| item.downcast()) + { + self.activate_item(&item, cx); + return item; + } - // let item = cx.build_view(|cx| T::for_project_item(self.project().clone(), project_item, cx)); - // self.split_item(SplitDirection::Right, Box::new(item.clone()), cx); - // item - // } + let item = + cx.build_view(|cx| T::for_project_item(self.project().clone(), project_item, cx)); + self.split_item(SplitDirection::Right, Box::new(item.clone()), cx); + item + } // pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext) { // if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) { @@ -2200,19 +2202,19 @@ impl Workspace { // } // } - // pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext) -> bool { - // let result = self.panes.iter().find_map(|pane| { - // pane.read(cx) - // .index_for_item(item) - // .map(|ix| (pane.clone(), ix)) - // }); - // if let Some((pane, ix)) = result { - // pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx)); - // true - // } else { - // false - // } - // } + pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext) -> bool { + let result = self.panes.iter().find_map(|pane| { + pane.read(cx) + .index_for_item(item) + .map(|ix| (pane.clone(), ix)) + }); + if let Some((pane, ix)) = result { + pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx)); + true + } else { + false + } + } // fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext) { // let panes = self.center.panes(); From de3d37e070d7e903ad6f85cabeae5358ae349c76 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Nov 2023 14:53:08 +0100 Subject: [PATCH 23/23] Don't depend on gpui2 in text --- Cargo.lock | 2 +- crates/text/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 803ae775e4572d2c9c447be0673320f4f972d478..d5d049393631d8df2334064a405816c063b1a37a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8881,7 +8881,7 @@ dependencies = [ "ctor", "digest 0.9.0", "env_logger 0.9.3", - "gpui2", + "gpui", "lazy_static", "log", "parking_lot 0.11.2", diff --git a/crates/text/Cargo.toml b/crates/text/Cargo.toml index 5a21500b2ff521182833dadb09ed7b1f8bfc6f8f..d1bc6cc8f8e6826c0f50f3f2fa15b28fe3ca8161 100644 --- a/crates/text/Cargo.toml +++ b/crates/text/Cargo.toml @@ -30,7 +30,7 @@ regex.workspace = true [dev-dependencies] collections = { path = "../collections", features = ["test-support"] } -gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +gpui = { path = "../gpui", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } ctor.workspace = true env_logger.workspace = true