diff --git a/Cargo.lock b/Cargo.lock index 1d87abea42..ee0ef32dfb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.1.0" dependencies = [ "anyhow", "auto_update", - "editor2", + "editor", "futures 0.3.28", "gpui2", "language2", @@ -375,7 +375,7 @@ dependencies = [ "client2", "collections", "ctor", - "editor2", + "editor", "env_logger", "fs2", "futures 0.3.28", @@ -1089,7 +1089,7 @@ name = "breadcrumbs" version = "0.1.0" dependencies = [ "collections", - "editor2", + "editor", "gpui2", "itertools 0.10.5", "language2", @@ -1717,7 +1717,7 @@ dependencies = [ "collections", "ctor", "dashmap", - "editor2", + "editor", "env_logger", "envy", "fs2", @@ -1782,7 +1782,7 @@ dependencies = [ "clock", "collections", "db2", - "editor2", + "editor", "feature_flags2", "feedback", "futures 0.3.28", @@ -1852,7 +1852,7 @@ dependencies = [ "anyhow", "collections", "ctor", - "editor2", + "editor", "env_logger", "fuzzy2", "go_to_line", @@ -2004,7 +2004,7 @@ version = "0.1.0" dependencies = [ "anyhow", "copilot2", - "editor2", + "editor", "fs2", "futures 0.3.28", "gpui2", @@ -2529,7 +2529,7 @@ dependencies = [ "anyhow", "client2", "collections", - "editor2", + "editor", "futures 0.3.28", "gpui2", "language2", @@ -2685,60 +2685,6 @@ checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd" [[package]] name = "editor" version = "0.1.0" -dependencies = [ - "aho-corasick", - "anyhow", - "client", - "clock", - "collections", - "context_menu", - "convert_case 0.6.0", - "copilot", - "ctor", - "db", - "drag_and_drop", - "env_logger", - "futures 0.3.28", - "fuzzy", - "git", - "gpui", - "indoc", - "itertools 0.10.5", - "language", - "lazy_static", - "log", - "lsp", - "multi_buffer", - "ordered-float 2.10.0", - "parking_lot 0.11.2", - "postage", - "project", - "rand 0.8.5", - "rich_text", - "rpc", - "schemars", - "serde", - "serde_derive", - "settings", - "smallvec", - "smol", - "snippet", - "sqlez", - "sum_tree", - "text", - "theme", - "tree-sitter", - "tree-sitter-html", - "tree-sitter-rust", - "tree-sitter-typescript", - "unindent", - "util", - "workspace", -] - -[[package]] -name = "editor2" -version = "0.1.0" dependencies = [ "aho-corasick", "anyhow", @@ -2981,7 +2927,7 @@ dependencies = [ "bitflags 2.4.1", "client2", "db2", - "editor2", + "editor", "futures 0.3.28", "gpui2", "human_bytes", @@ -3014,7 +2960,7 @@ version = "0.1.0" dependencies = [ "collections", "ctor", - "editor2", + "editor", "env_logger", "fuzzy2", "gpui2", @@ -3568,7 +3514,7 @@ dependencies = [ name = "go_to_line" version = "0.1.0" dependencies = [ - "editor2", + "editor", "gpui2", "menu2", "postage", @@ -4296,7 +4242,7 @@ dependencies = [ "anyhow", "chrono", "dirs 4.0.0", - "editor2", + "editor", "gpui2", "log", "schemars", @@ -4488,7 +4434,7 @@ name = "language_selector" version = "0.1.0" dependencies = [ "anyhow", - "editor2", + "editor", "fuzzy2", "gpui2", "language2", @@ -4508,7 +4454,7 @@ dependencies = [ "anyhow", "client2", "collections", - "editor2", + "editor", "env_logger", "futures 0.3.28", "gpui2", @@ -5815,7 +5761,7 @@ dependencies = [ name = "outline2" version = "0.1.0" dependencies = [ - "editor2", + "editor", "fuzzy2", "gpui2", "language2", @@ -6039,7 +5985,7 @@ name = "picker" version = "0.1.0" dependencies = [ "ctor", - "editor2", + "editor", "env_logger", "gpui2", "menu2", @@ -6459,7 +6405,7 @@ dependencies = [ "client2", "collections", "db2", - "editor2", + "editor", "futures 0.3.28", "gpui2", "language2", @@ -6486,7 +6432,7 @@ name = "project_symbols" version = "0.1.0" dependencies = [ "anyhow", - "editor2", + "editor", "futures 0.3.28", "fuzzy2", "gpui2", @@ -6665,7 +6611,7 @@ name = "quick_action_bar" version = "0.1.0" dependencies = [ "assistant2", - "editor2", + "editor", "gpui2", "search", "ui2", @@ -6837,7 +6783,7 @@ dependencies = [ name = "recent_projects" version = "0.1.0" dependencies = [ - "editor2", + "editor", "futures 0.3.28", "fuzzy2", "gpui2", @@ -7679,7 +7625,7 @@ dependencies = [ "bitflags 1.3.2", "client2", "collections", - "editor2", + "editor", "futures 0.3.28", "gpui2", "language2", @@ -8605,7 +8551,7 @@ dependencies = [ "chrono", "clap 4.4.4", "dialoguer", - "editor2", + "editor", "fuzzy2", "gpui2", "indoc", @@ -8968,7 +8914,7 @@ dependencies = [ "client2", "db2", "dirs 4.0.0", - "editor2", + "editor", "futures 0.3.28", "gpui2", "itertools 0.10.5", @@ -9114,7 +9060,7 @@ name = "theme_selector" version = "0.1.0" dependencies = [ "client2", - "editor2", + "editor", "feature_flags2", "fs2", "fuzzy2", @@ -10211,7 +10157,7 @@ dependencies = [ "collections", "command_palette", "diagnostics", - "editor2", + "editor", "futures 0.3.28", "gpui2", "indoc", @@ -10629,7 +10575,7 @@ dependencies = [ "anyhow", "client2", "db2", - "editor2", + "editor", "fs2", "fuzzy2", "gpui2", @@ -11072,7 +11018,7 @@ dependencies = [ "ctor", "db2", "diagnostics", - "editor2", + "editor", "env_logger", "feature_flags2", "feedback", diff --git a/crates/activity_indicator/Cargo.toml b/crates/activity_indicator/Cargo.toml index a9c03d540e..0b4889f2bd 100644 --- a/crates/activity_indicator/Cargo.toml +++ b/crates/activity_indicator/Cargo.toml @@ -10,7 +10,7 @@ doctest = false [dependencies] auto_update = { path = "../auto_update" } -editor = { path = "../editor2", package = "editor2" } +editor = { path = "../editor" } language = { path = "../language2", package = "language2" } gpui = { path = "../gpui2", package = "gpui2" } project = { path = "../project2", package = "project2" } @@ -25,4 +25,4 @@ futures.workspace = true smallvec.workspace = true [dev-dependencies] -editor = { path = "../editor2", package = "editor2", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index aa5eb48a13..6e8132f724 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -12,7 +12,7 @@ doctest = false ai = { package = "ai2", path = "../ai2" } client = { package = "client2", path = "../client2" } collections = { path = "../collections"} -editor = { package = "editor2", path = "../editor2" } +editor = { path = "../editor" } fs = { package = "fs2", path = "../fs2" } gpui = { package = "gpui2", path = "../gpui2" } language = { package = "language2", path = "../language2" } @@ -45,7 +45,7 @@ tiktoken-rs.workspace = true [dev-dependencies] ai = { package = "ai2", path = "../ai2", features = ["test-support"]} -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } project = { package = "project2", path = "../project2", features = ["test-support"] } ctor.workspace = true diff --git a/crates/breadcrumbs/Cargo.toml b/crates/breadcrumbs/Cargo.toml index 712d2ecc39..4a6c5240af 100644 --- a/crates/breadcrumbs/Cargo.toml +++ b/crates/breadcrumbs/Cargo.toml @@ -10,7 +10,7 @@ doctest = false [dependencies] collections = { path = "../collections" } -editor = { package = "editor2", path = "../editor2" } +editor = { path = "../editor" } gpui = { package = "gpui2", path = "../gpui2" } ui = { package = "ui2", path = "../ui2" } language = { package = "language2", path = "../language2" } @@ -23,6 +23,6 @@ outline = { package = "outline2", path = "../outline2" } itertools = "0.10" [dev-dependencies] -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } diff --git a/crates/collab2/Cargo.toml b/crates/collab2/Cargo.toml index 7059733809..21aa51fb90 100644 --- a/crates/collab2/Cargo.toml +++ b/crates/collab2/Cargo.toml @@ -66,7 +66,7 @@ gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } call = { package = "call2", path = "../call2", features = ["test-support"] } client = { package = "client2", path = "../client2", features = ["test-support"] } channel = { package = "channel2", path = "../channel2" } -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } language = { package = "language2", path = "../language2", features = ["test-support"] } fs = { package = "fs2", path = "../fs2", features = ["test-support"] } git = { package = "git3", path = "../git3", features = ["test-support"] } diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 3d9a2abe25..642e78eb2d 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -31,7 +31,7 @@ clock = { path = "../clock" } collections = { path = "../collections" } # context_menu = { path = "../context_menu" } # drag_and_drop = { path = "../drag_and_drop" } -editor = { package="editor2", path = "../editor2" } +editor = { path = "../editor" } feedback = { path = "../feedback" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" } gpui = { package = "gpui2", path = "../gpui2" } @@ -68,7 +68,7 @@ smallvec.workspace = true call = { package = "call2", path = "../call2", features = ["test-support"] } client = { package = "client2", path = "../client2", features = ["test-support"] } collections = { path = "../collections", features = ["test-support"] } -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } notifications = { package = "notifications2", path = "../notifications2", features = ["test-support"] } project = { package = "project2", path = "../project2", features = ["test-support"] } diff --git a/crates/command_palette/Cargo.toml b/crates/command_palette/Cargo.toml index d974fdb305..387a238734 100644 --- a/crates/command_palette/Cargo.toml +++ b/crates/command_palette/Cargo.toml @@ -10,7 +10,7 @@ doctest = false [dependencies] collections = { path = "../collections" } -editor = { package = "editor2", path = "../editor2" } +editor = { path = "../editor" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" } gpui = { package = "gpui2", path = "../gpui2" } picker = { path = "../picker" } @@ -26,7 +26,7 @@ serde.workspace = true [dev-dependencies] gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } language = { package="language2", path = "../language2", features = ["test-support"] } project = { package="project2", path = "../project2", features = ["test-support"] } menu = { package = "menu2", path = "../menu2" } diff --git a/crates/command_palette2/Cargo.toml b/crates/command_palette2/Cargo.toml index e9f74feb2e..9e8615a876 100644 --- a/crates/command_palette2/Cargo.toml +++ b/crates/command_palette2/Cargo.toml @@ -10,7 +10,7 @@ doctest = false [dependencies] collections = { path = "../collections" } -editor = { package = "editor2", path = "../editor2" } +editor = { path = "../editor" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" } gpui = { package = "gpui2", path = "../gpui2" } picker = { path = "../picker" } @@ -25,7 +25,7 @@ anyhow.workspace = true serde.workspace = true [dev-dependencies] gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } language = { package="language2", path = "../language2", features = ["test-support"] } project = { package="project2", path = "../project2", features = ["test-support"] } menu = { package = "menu2", path = "../menu2" } diff --git a/crates/copilot_button/Cargo.toml b/crates/copilot_button/Cargo.toml index f7611ed3f3..4ed9fcc653 100644 --- a/crates/copilot_button/Cargo.toml +++ b/crates/copilot_button/Cargo.toml @@ -10,7 +10,7 @@ doctest = false [dependencies] copilot = { package = "copilot2", path = "../copilot2" } -editor = { package = "editor2", path = "../editor2" } +editor = { path = "../editor" } fs = { package = "fs2", path = "../fs2" } zed-actions = { package="zed_actions2", path = "../zed_actions2"} gpui = { package = "gpui2", path = "../gpui2" } @@ -24,4 +24,4 @@ smol.workspace = true futures.workspace = true [dev-dependencies] -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml index 35fa5598ad..643173b3d5 100644 --- a/crates/diagnostics/Cargo.toml +++ b/crates/diagnostics/Cargo.toml @@ -10,7 +10,7 @@ doctest = false [dependencies] collections = { path = "../collections" } -editor = { package = "editor2", path = "../editor2" } +editor = { path = "../editor" } gpui = { package = "gpui2", path = "../gpui2" } ui = { package = "ui2", path = "../ui2" } language = { package = "language2", path = "../language2" } @@ -32,7 +32,7 @@ postage.workspace = true [dev-dependencies] client = { package = "client2", path = "../client2", features = ["test-support"] } -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +editor = { path = "../editor", 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"] } diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 5113b5e7de..2c48f06a25 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -23,30 +23,30 @@ test-support = [ ] [dependencies] -client = { path = "../client" } +client = { package = "client2", path = "../client2" } clock = { path = "../clock" } -copilot = { path = "../copilot" } -db = { path = "../db" } -drag_and_drop = { path = "../drag_and_drop" } +copilot = { package="copilot2", path = "../copilot2" } +db = { package="db2", path = "../db2" } 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" } +# context_menu = { path = "../context_menu" } +fuzzy = { package = "fuzzy2", path = "../fuzzy2" } +git = { package = "git3", path = "../git3" } +gpui = { package = "gpui2", path = "../gpui2" } +language = { package = "language2", path = "../language2" } +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 = { package = "rich_text2", path = "../rich_text2" } +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" } +ui = { package = "ui2", path = "../ui2" } util = { path = "../util" } sqlez = { path = "../sqlez" } -workspace = { path = "../workspace" } +workspace = { package = "workspace2", path = "../workspace2" } aho-corasick = "1.1" anyhow.workspace = true @@ -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 @@ -71,16 +72,16 @@ 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"] } -multi_buffer = { path = "../multi_buffer", 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 = { package = "multi_buffer2", path = "../multi_buffer2", features = ["test-support"] } ctor.workspace = true env_logger.workspace = true diff --git a/crates/editor/src/blink_manager.rs b/crates/editor/src/blink_manager.rs index fa5a3af0c6..e3a8ce6293 100644 --- a/crates/editor/src/blink_manager.rs +++ b/crates/editor/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(); } @@ -61,20 +57,18 @@ 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(); 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)) + .ok(); } }) .detach(); @@ -92,6 +86,10 @@ impl BlinkManager { } pub fn enable(&mut self, cx: &mut ModelContext) { + if self.enabled { + return; + } + self.enabled = true; // Set cursors as invisible and start blinking: this causes cursors // to be visible during the next render. @@ -107,7 +105,3 @@ impl BlinkManager { self.visible } } - -impl Entity for BlinkManager { - type Event = (); -} diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 1d6deb910a..8703f1ba40 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -4,19 +4,15 @@ mod inlay_map; mod tab_map; mod wrap_map; +use crate::EditorStyle; 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::{ - color::Color, - fonts::{FontId, HighlightStyle, Underline}, - text_layout::{Line, RunStyle}, - Entity, ModelContext, ModelHandle, -}; +use gpui::{Font, HighlightStyle, Hsla, LineLayout, Model, ModelContext, Pixels, UnderlineStyle}; use inlay_map::InlayMap; use language::{ language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription, @@ -25,6 +21,7 @@ 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::{ @@ -32,7 +29,7 @@ pub use block_map::{ BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock, }; -pub use self::fold_map::FoldPoint; +pub use self::fold_map::{Fold, FoldPoint}; pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -41,6 +38,8 @@ pub enum FoldStatus { Foldable, } +const UNNECESSARY_CODE_FADE: f32 = 0.3; + pub trait ToDisplayPoint { fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint; } @@ -49,28 +48,24 @@ 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, 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: Model, + font: Font, + font_size: Pixels, + wrap_width: Option, buffer_header_height: u8, excerpt_header_height: u8, cx: &mut ModelContext, @@ -81,7 +76,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 { @@ -127,7 +122,7 @@ impl DisplayMap { self.fold( other .folds_in_range(0..other.buffer_snapshot.len()) - .map(|fold| fold.to_offset(&other.buffer_snapshot)), + .map(|fold| fold.range.to_offset(&other.buffer_snapshot)), cx, ); } @@ -249,16 +244,16 @@ 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)) + .update(cx, |map, cx| map.set_font_with_size(font, 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) } - 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)) } @@ -296,7 +291,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() @@ -510,18 +505,18 @@ impl DisplaySnapshot { &'a self, display_rows: Range, language_aware: bool, - style: &'a EditorStyle, + editor_style: &'a EditorStyle, ) -> impl Iterator> { self.chunks( display_rows, language_aware, - Some(style.theme.hint), - Some(style.theme.suggestion), + Some(editor_style.inlays_style), + Some(editor_style.suggestions_style), ) .map(|chunk| { let mut highlight_style = chunk .syntax_highlight_id - .and_then(|id| id.style(&style.syntax)); + .and_then(|id| id.style(&editor_style.syntax)); if let Some(chunk_highlight) = chunk.highlight_style { if let Some(highlight_style) = highlight_style.as_mut() { @@ -534,17 +529,18 @@ impl DisplaySnapshot { let mut diagnostic_highlight = HighlightStyle::default(); if chunk.is_unnecessary { - diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade); + diagnostic_highlight.fade_out = Some(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), + let diagnostic_color = + super::diagnostic_style(severity, true, &editor_style.status); + diagnostic_highlight.underline = Some(UnderlineStyle { + color: Some(diagnostic_color), thickness: 1.0.into(), - squiggly: true, + wavy: true, }); } } @@ -563,81 +559,64 @@ impl DisplaySnapshot { }) } - pub fn lay_out_line_for_row( + pub fn layout_row( &self, display_row: u32, TextLayoutDetails { - font_cache, - text_layout_cache, + text_system, editor_style, + rem_size, }: &TextLayoutDetails, - ) -> Line { - let mut styles = Vec::new(); + ) -> Arc { + let mut runs = 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) { + 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)) + Cow::Owned(editor_style.text.clone().highlight(style)) } 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, - }, - )); + runs.push(text_style.to_run(chunk.chunk.len())) } - // 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, - }, - )); + if line.ends_with('\n') { + line.pop(); + if let Some(last_run) = runs.last_mut() { + last_run.len -= 1; + if last_run.len == 0 { + runs.pop(); + } + } } - text_layout_cache.layout_str(&line, editor_style.text.font_size, &styles) + let font_size = editor_style.text.font_size.to_pixels(*rem_size); + text_system + .layout_line(&line, font_size, &runs) + .expect("we expect the font to be loaded because it's rendered by the editor") } - pub fn x_for_point( + pub fn x_for_display_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) + ) -> Pixels { + let line = self.layout_row(display_point.row(), text_layout_details); + line.x_for_index(display_point.column() as usize) } - pub fn column_for_x( + pub fn display_column_for_x( &self, display_row: u32, - x_coordinate: f32, - text_layout_details: &TextLayoutDetails, + x: Pixels, + 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 + let layout_line = self.layout_row(display_row, details); + layout_line.closest_index_for_x(x) as u32 } pub fn chars_at( @@ -740,7 +719,7 @@ impl DisplaySnapshot { DisplayPoint(point) } - pub fn folds_in_range(&self, range: Range) -> impl Iterator> + pub fn folds_in_range(&self, range: Range) -> impl Iterator where T: ToOffset, { @@ -1015,7 +994,7 @@ pub mod tests { movement, test::{editor_test_context::EditorTestContext, marked_display_snapshot}, }; - use gpui::{color::Color, elements::*, test::observe, AppContext}; + use gpui::{div, font, observe, px, AppContext, Context, Element, Hsla}; use language::{ language_settings::{AllLanguageSettings, AllLanguageSettingsContent}, Buffer, Language, LanguageConfig, SelectionGoal, @@ -1025,34 +1004,27 @@ pub mod tests { use settings::SettingsStore; use smol::stream::StreamExt; use std::{env, sync::Arc}; - use theme::SyntaxTheme; + use theme::{LoadThemes, 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(); + cx.background_executor.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 _test_platform = &cx.test_platform; 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 font_size = px(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)) + Some(px(rng.gen_range(0.0..=max_wrap_width))) }; log::info!("tab size: {}", tab_size); @@ -1074,10 +1046,10 @@ pub mod tests { } }); - let map = cx.add_model(|cx| { + let map = cx.new_model(|cx| { DisplayMap::new( buffer.clone(), - font_id, + font("Helvetica"), font_size, wrap_width, buffer_start_excerpt_header_height, @@ -1103,7 +1075,7 @@ pub mod tests { wrap_width = if rng.gen_bool(0.2) { None } else { - Some(rng.gen_range(0.0..=max_wrap_width)) + Some(px(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)); @@ -1114,7 +1086,7 @@ pub mod tests { tab_size = *tab_sizes.choose(&mut rng).unwrap(); log::info!("setting tab size to {:?}", tab_size); cx.update(|cx| { - cx.update_global::(|store, cx| { + cx.update_global::(|store, cx| { store.update_user_settings::(cx, |s| { s.defaults.tab_size = NonZeroU32::new(tab_size); }); @@ -1150,7 +1122,7 @@ pub mod tests { position, height, disposition, - render: Arc::new(|_| Empty::new().into_any()), + render: Arc::new(|_| div().into_any()), } }) .collect::>(); @@ -1295,7 +1267,8 @@ pub mod tests { #[gpui::test(retries = 5)] async fn test_soft_wraps(cx: &mut gpui::TestAppContext) { - cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); + cx.background_executor + .set_block_on_ticks(usize::MAX..=usize::MAX); cx.update(|cx| { init_test(cx, |_| {}); }); @@ -1304,25 +1277,25 @@ pub mod tests { let editor = cx.editor.clone(); let window = cx.window.clone(); - cx.update_window(window, |cx| { + _ = cx.update_window(window, |_, cx| { let text_layout_details = - editor.read_with(cx, |editor, cx| editor.text_layout_details(cx)); + editor.update(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 font_size = px(12.0); + let wrap_width = Some(px(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 map = cx.new_model(|cx| { + DisplayMap::new( + buffer.clone(), + font("Helvetica"), + font_size, + wrap_width, + 1, + 1, + cx, + ) }); let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); @@ -1347,7 +1320,7 @@ pub mod tests { DisplayPoint::new(0, 7) ); - let x = snapshot.x_for_point(DisplayPoint::new(1, 10), &text_layout_details); + let x = snapshot.x_for_display_point(DisplayPoint::new(1, 10), &text_layout_details); assert_eq!( movement::up( &snapshot, @@ -1358,33 +1331,33 @@ pub mod tests { ), ( DisplayPoint::new(0, 7), - SelectionGoal::HorizontalPosition(x) + SelectionGoal::HorizontalPosition(x.0) ) ); assert_eq!( movement::down( &snapshot, DisplayPoint::new(0, 7), - SelectionGoal::HorizontalPosition(x), + SelectionGoal::HorizontalPosition(x.0), false, &text_layout_details ), ( DisplayPoint::new(1, 10), - SelectionGoal::HorizontalPosition(x) + SelectionGoal::HorizontalPosition(x.0) ) ); assert_eq!( movement::down( &snapshot, DisplayPoint::new(1, 10), - SelectionGoal::HorizontalPosition(x), + SelectionGoal::HorizontalPosition(x.0), false, &text_layout_details ), ( DisplayPoint::new(2, 4), - SelectionGoal::HorizontalPosition(x) + SelectionGoal::HorizontalPosition(x.0) ) ); @@ -1400,7 +1373,9 @@ pub mod tests { ); // 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(font("Helvetica"), px(font_size.0 + 3.), cx) + }); let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); assert_eq!( @@ -1416,17 +1391,11 @@ pub mod tests { 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)); + + let font_size = px(14.0); + let map = cx.new_model(|cx| { + DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx) + }); buffer.update(cx, |buffer, cx| { buffer.edit( @@ -1470,9 +1439,9 @@ pub mod tests { }"# .unindent(); - let theme = SyntaxTheme::new(vec![ - ("mod.body".to_string(), Color::red().into()), - ("fn.name".to_string(), Color::blue().into()), + let theme = SyntaxTheme::new_test(vec![ + ("mod.body", Hsla::red().into()), + ("fn.name", Hsla::blue().into()), ]); let language = Arc::new( Language::new( @@ -1495,38 +1464,33 @@ pub mod tests { 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 buffer = cx.new_model(|cx| { + Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) + }); + cx.condition(&buffer, |buf, _| !buf.is_parsing()).await; + let buffer = cx.new_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 font_size = px(14.0); - let map = cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx)); + let map = cx + .new_model(|cx| DisplayMap::new(buffer, font("Helvetica"), 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())), + ("outer".to_string(), Some(Hsla::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())), + ("{\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(Color::red())), - ("inner".to_string(), Some(Color::blue())), - ("() {}\n}".to_string(), Some(Color::red())), + (" fn ".to_string(), Some(Hsla::red())), + ("inner".to_string(), Some(Hsla::blue())), + ("() {}\n}".to_string(), Some(Hsla::red())), ] ); @@ -1537,11 +1501,11 @@ pub mod tests { cx.update(|cx| syntax_chunks(0..2, &map, &theme, cx)), vec![ ("fn ".to_string(), None), - ("out".to_string(), Some(Color::blue())), + ("out".to_string(), Some(Hsla::blue())), ("⋯".to_string(), None), - (" fn ".to_string(), Some(Color::red())), - ("inner".to_string(), Some(Color::blue())), - ("() {}\n}".to_string(), Some(Color::red())), + (" fn ".to_string(), Some(Hsla::red())), + ("inner".to_string(), Some(Hsla::blue())), + ("() {}\n}".to_string(), Some(Hsla::red())), ] ); } @@ -1550,7 +1514,8 @@ pub mod tests { 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); + cx.background_executor + .set_block_on_ticks(usize::MAX..=usize::MAX); let text = r#" fn outer() {} @@ -1560,9 +1525,9 @@ pub mod tests { }"# .unindent(); - let theme = SyntaxTheme::new(vec![ - ("mod.body".to_string(), Color::red().into()), - ("fn.name".to_string(), Color::blue().into()), + let theme = SyntaxTheme::new_test(vec![ + ("mod.body", Hsla::red().into()), + ("fn.name", Hsla::blue().into()), ]); let language = Arc::new( Language::new( @@ -1585,28 +1550,22 @@ pub mod tests { 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 buffer = cx.new_model(|cx| { + Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) + }); + cx.condition(&buffer, |buf, _| !buf.is_parsing()).await; + let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let font_cache = cx.font_cache(); + let font_size = px(16.0); - 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)); + let map = cx.new_model(|cx| { + DisplayMap::new(buffer, font("Courier"), font_size, Some(px(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())), + ("oute\nr".to_string(), Some(Hsla::blue())), ("() \n{}\n\n".to_string(), None), ] ); @@ -1621,10 +1580,10 @@ pub mod tests { assert_eq!( cx.update(|cx| syntax_chunks(1..4, &map, &theme, cx)), [ - ("out".to_string(), Some(Color::blue())), + ("out".to_string(), Some(Hsla::blue())), ("⋯\n".to_string(), None), - (" \nfn ".to_string(), Some(Color::red())), - ("i\n".to_string(), Some(Color::blue())) + (" \nfn ".to_string(), Some(Hsla::red())), + ("i\n".to_string(), Some(Hsla::blue())) ] ); } @@ -1633,9 +1592,9 @@ pub mod tests { 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 theme = SyntaxTheme::new_test(vec![ + ("operator", Hsla::red().into()), + ("string", Hsla::green().into()), ]); let language = Arc::new( Language::new( @@ -1658,27 +1617,22 @@ pub mod tests { 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.new_model(|cx| { + Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) + }); + cx.condition(&buffer, |buf, _| !buf.is_parsing()).await; - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let buffer = cx.new_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)); + let font_size = px(16.0); + let map = + cx.new_model(|cx| DisplayMap::new(buffer, font("Courier"), font_size, None, 1, 1, cx)); enum MyType {} let style = HighlightStyle { - color: Some(Color::blue()), + color: Some(Hsla::blue()), ..Default::default() }; @@ -1700,12 +1654,12 @@ pub mod tests { 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), + ("a".to_string(), None, Some(Hsla::blue())), + (":".to_string(), Some(Hsla::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), + ("\"c ".to_string(), Some(Hsla::green()), None), + ("d".to_string(), Some(Hsla::green()), Some(Hsla::blue())), + ("\"".to_string(), Some(Hsla::green()), None), ] ); } @@ -1785,17 +1739,11 @@ pub mod tests { 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 font_size = px(14.0); - let map = - cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx)); + let map = cx.new_model(|cx| { + DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx) + }); let map = map.update(cx, |map, cx| map.snapshot(cx)); assert_eq!(map.text(), "✅ α\nβ \n🏀β γ"); assert_eq!( @@ -1846,16 +1794,10 @@ pub mod tests { 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)); + let font_size = px(14.0); + let map = cx.new_model(|cx| { + DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx) + }); assert_eq!( map.update(cx, |map, cx| map.snapshot(cx)).max_point(), DisplayPoint::new(1, 11) @@ -1864,10 +1806,10 @@ pub mod tests { fn syntax_chunks<'a>( rows: Range, - map: &ModelHandle, + map: &Model, theme: &'a SyntaxTheme, cx: &mut AppContext, - ) -> Vec<(String, Option)> { + ) -> Vec<(String, Option)> { chunks(rows, map, theme, cx) .into_iter() .map(|(text, color, _)| (text, color)) @@ -1876,12 +1818,12 @@ pub mod tests { fn chunks<'a>( rows: Range, - map: &ModelHandle, + map: &Model, theme: &'a SyntaxTheme, cx: &mut AppContext, - ) -> Vec<(String, Option, Option)> { + ) -> Vec<(String, Option, Option)> { let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); - let mut chunks: Vec<(String, Option, Option)> = Vec::new(); + 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 @@ -1899,13 +1841,13 @@ pub mod tests { } fn init_test(cx: &mut AppContext, f: impl Fn(&mut AllLanguageSettingsContent)) { - cx.foreground().forbid_parking(); - cx.set_global(SettingsStore::test(cx)); + let settings = SettingsStore::test(cx); + cx.set_global(settings); language::init(cx); crate::init(cx); Project::init_settings(cx); - theme::init((), cx); - cx.update_global::(|store, cx| { + theme::init(LoadThemes::JustBase, cx); + cx.update_global::(|store, cx| { store.update_user_settings::(cx, f); }); } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index c07625bf9c..6eb0d05bfe 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -2,9 +2,9 @@ use super::{ wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot}, Highlights, }; -use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _}; +use crate::{Anchor, Editor, EditorStyle, ExcerptId, ExcerptRange, ToPoint as _}; use collections::{Bound, HashMap, HashSet}; -use gpui::{AnyElement, ViewContext}; +use gpui::{AnyElement, Pixels, ViewContext}; use language::{BufferSnapshot, Chunk, Patch, Point}; use parking_lot::Mutex; use std::{ @@ -50,7 +50,7 @@ struct BlockRow(u32); #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] struct WrapRow(u32); -pub type RenderBlock = Arc AnyElement>; +pub type RenderBlock = Arc AnyElement>; pub struct Block { id: BlockId, @@ -69,7 +69,7 @@ where pub position: P, pub height: u8, pub style: BlockStyle, - pub render: Arc AnyElement>, + pub render: Arc AnyElement>, pub disposition: BlockDisposition, } @@ -80,15 +80,15 @@ pub enum BlockStyle { 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 struct BlockContext<'a, 'b> { + pub view_context: &'b mut ViewContext<'a, Editor>, + pub anchor_x: Pixels, + pub gutter_width: Pixels, + pub gutter_padding: Pixels, + pub em_width: Pixels, + pub line_height: Pixels, pub block_id: usize, + pub editor_style: &'b EditorStyle, } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -932,22 +932,22 @@ 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 } } impl Block { - pub fn render(&self, cx: &mut BlockContext) -> AnyElement { + pub fn render(&self, cx: &mut BlockContext) -> AnyElement { self.render.lock()(cx) } @@ -993,7 +993,7 @@ 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 gpui::{div, font, px, Element}; use multi_buffer::MultiBuffer; use rand::prelude::*; use settings::SettingsStore; @@ -1015,27 +1015,19 @@ mod tests { } #[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(); + fn test_basic_blocks(cx: &mut gpui::TestAppContext) { + cx.update(|cx| init_test(cx)); let text = "aaa\nbbb\nccc\nddd"; - let buffer = MultiBuffer::build_simple(text, cx); - let buffer_snapshot = buffer.read(cx).snapshot(cx); + let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx)); + let buffer_snapshot = cx.update(|cx| 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 (wrap_map, wraps_snapshot) = + cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(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()); @@ -1045,21 +1037,21 @@ mod tests { position: buffer_snapshot.anchor_after(Point::new(1, 0)), height: 1, disposition: BlockDisposition::Above, - render: Arc::new(|_| Empty::new().into_any_named("block 1")), + render: Arc::new(|_| div().into_any()), }, 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")), + render: Arc::new(|_| div().into_any()), }, 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")), + render: Arc::new(|_| div().into_any()), }, ]); @@ -1190,26 +1182,21 @@ mod tests { } #[gpui::test] - fn test_blocks_on_wrapped_lines(cx: &mut gpui::AppContext) { - init_test(cx); + fn test_blocks_on_wrapped_lines(cx: &mut gpui::TestAppContext) { + cx.update(|cx| 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 _font_id = cx.text_system().font_id(&font("Helvetica")).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 buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx)); + let buffer_snapshot = cx.update(|cx| 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 (_, wraps_snapshot) = cx.update(|cx| { + WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), Some(px(60.)), cx) + }); let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1); let mut writer = block_map.write(wraps_snapshot.clone(), Default::default()); @@ -1218,14 +1205,14 @@ mod tests { 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")), + render: Arc::new(|_| div().into_any()), 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")), + render: Arc::new(|_| div().into_any()), height: 1, }, ]); @@ -1240,8 +1227,8 @@ mod tests { } #[gpui::test(iterations = 100)] - fn test_random_blocks(cx: &mut gpui::AppContext, mut rng: StdRng) { - init_test(cx); + fn test_random_blocks(cx: &mut gpui::TestAppContext, mut rng: StdRng) { + cx.update(|cx| init_test(cx)); let operations = env::var("OPERATIONS") .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) @@ -1250,18 +1237,10 @@ mod tests { let wrap_width = if rng.gen_bool(0.2) { None } else { - Some(rng.gen_range(0.0..=100.0)) + Some(px(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 font_size = px(14.0); let buffer_start_header_height = rng.gen_range(1..=5); let excerpt_header_height = rng.gen_range(1..=5); @@ -1272,17 +1251,17 @@ mod tests { 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) + cx.update(|cx| MultiBuffer::build_simple(&text, cx)) } else { - MultiBuffer::build_random(&mut rng, cx) + cx.update(|cx| MultiBuffer::build_random(&mut rng, cx)) }; - let mut buffer_snapshot = buffer.read(cx).snapshot(cx); + let mut buffer_snapshot = cx.update(|cx| 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 (wrap_map, wraps_snapshot) = cx + .update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), font_size, wrap_width, cx)); let mut block_map = BlockMap::new( wraps_snapshot, buffer_start_header_height, @@ -1297,7 +1276,7 @@ mod tests { let wrap_width = if rng.gen_bool(0.2) { None } else { - Some(rng.gen_range(0.0..=100.0)) + Some(px(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)); @@ -1306,7 +1285,7 @@ mod tests { let block_count = rng.gen_range(1..=5); let block_properties = (0..block_count) .map(|_| { - let buffer = buffer.read(cx).read(cx); + let buffer = cx.update(|cx| buffer.read(cx).read(cx).clone()); let position = buffer.anchor_after( buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Left), ); @@ -1328,7 +1307,7 @@ mod tests { position, height, disposition, - render: Arc::new(|_| Empty::new().into_any()), + render: Arc::new(|_| div().into_any()), } }) .collect::>(); @@ -1646,8 +1625,9 @@ mod tests { } fn init_test(cx: &mut gpui::AppContext) { - cx.set_global(SettingsStore::test(cx)); - theme::init((), cx); + let settings = SettingsStore::test(cx); + cx.set_global(settings); + theme::init(theme::LoadThemes::JustBase, cx); } impl TransformBlock { diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 4636d9a17f..4dad2d52ae 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -3,15 +3,16 @@ use super::{ Highlights, }; use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset}; -use gpui::{color::Color, fonts::HighlightStyle}; +use gpui::{ElementId, HighlightStyle, Hsla}; use language::{Chunk, Edit, Point, TextSummary}; use std::{ any::TypeId, cmp::{self, Ordering}, iter, - ops::{Add, AddAssign, Range, Sub}, + ops::{Add, AddAssign, Deref, DerefMut, Range, Sub}, }; use sum_tree::{Bias, Cursor, FilterCursor, SumTree}; +use util::post_inc; #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] pub struct FoldPoint(pub Point); @@ -90,12 +91,16 @@ impl<'a> FoldMapWriter<'a> { } // 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 { + let fold_range = + FoldRange(buffer.anchor_after(range.start)..buffer.anchor_before(range.end)); + if fold_range.0.start.excerpt_id != fold_range.0.end.excerpt_id { continue; } - folds.push(fold); + folds.push(Fold { + id: FoldId(post_inc(&mut self.0.next_fold_id.0)), + range: fold_range, + }); let inlay_range = snapshot.to_inlay_offset(range.start)..snapshot.to_inlay_offset(range.end); @@ -106,13 +111,13 @@ impl<'a> FoldMapWriter<'a> { } let buffer = &snapshot.buffer; - folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(a, b, buffer)); + folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(&a.range, &b.range, buffer)); self.0.snapshot.folds = { let mut new_tree = SumTree::new(); - let mut cursor = self.0.snapshot.folds.cursor::(); + let mut cursor = self.0.snapshot.folds.cursor::(); for fold in folds { - new_tree.append(cursor.slice(&fold, Bias::Right, buffer), buffer); + new_tree.append(cursor.slice(&fold.range, Bias::Right, buffer), buffer); new_tree.push(fold, buffer); } new_tree.append(cursor.suffix(buffer), buffer); @@ -138,7 +143,8 @@ impl<'a> FoldMapWriter<'a> { 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); + let offset_range = + fold.range.start.to_offset(buffer)..fold.range.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); @@ -174,7 +180,8 @@ impl<'a> FoldMapWriter<'a> { pub struct FoldMap { snapshot: FoldSnapshot, - ellipses_color: Option, + ellipses_color: Option, + next_fold_id: FoldId, } impl FoldMap { @@ -197,6 +204,7 @@ impl FoldMap { ellipses_color: None, }, ellipses_color: None, + next_fold_id: FoldId::default(), }; let snapshot = this.snapshot.clone(); (this, snapshot) @@ -221,7 +229,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 @@ -242,8 +250,8 @@ impl FoldMap { 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); + .range + .cmp(&next_fold.range, &self.snapshot.inlay_snapshot.buffer); assert!(comparison.is_le()); } } @@ -304,9 +312,9 @@ impl FoldMap { let anchor = inlay_snapshot .buffer .anchor_before(inlay_snapshot.to_buffer_offset(edit.new.start)); - let mut folds_cursor = self.snapshot.folds.cursor::(); + let mut folds_cursor = self.snapshot.folds.cursor::(); folds_cursor.seek( - &Fold(anchor..Anchor::max()), + &FoldRange(anchor..Anchor::max()), Bias::Left, &inlay_snapshot.buffer, ); @@ -315,8 +323,8 @@ impl FoldMap { 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); + let buffer_start = f.range.start.to_offset(&inlay_snapshot.buffer); + let buffer_end = f.range.end.to_offset(&inlay_snapshot.buffer); inlay_snapshot.to_inlay_offset(buffer_start) ..inlay_snapshot.to_inlay_offset(buffer_end) }); @@ -469,7 +477,7 @@ pub struct FoldSnapshot { folds: SumTree, pub inlay_snapshot: InlaySnapshot, pub version: usize, - pub ellipses_color: Option, + pub ellipses_color: Option, } impl FoldSnapshot { @@ -596,13 +604,13 @@ impl FoldSnapshot { self.transforms.summary().output.longest_row } - pub fn folds_in_range(&self, range: Range) -> impl Iterator> + 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); + let item = folds.item(); folds.next(&self.inlay_snapshot.buffer); item }) @@ -830,10 +838,39 @@ impl sum_tree::Summary for TransformSummary { } } -#[derive(Clone, Debug)] -struct Fold(Range); +#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] +pub struct FoldId(usize); -impl Default for Fold { +impl Into for FoldId { + fn into(self) -> ElementId { + ElementId::Integer(self.0) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Fold { + pub id: FoldId, + pub range: FoldRange, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct FoldRange(Range); + +impl Deref for FoldRange { + type Target = Range; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for FoldRange { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Default for FoldRange { fn default() -> Self { Self(Anchor::min()..Anchor::max()) } @@ -844,17 +881,17 @@ impl sum_tree::Item for Fold { 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(), + start: self.range.start.clone(), + end: self.range.end.clone(), + min_start: self.range.start.clone(), + max_end: self.range.end.clone(), count: 1, } } } #[derive(Clone, Debug)] -struct FoldSummary { +pub struct FoldSummary { start: Anchor, end: Anchor, min_start: Anchor, @@ -900,14 +937,14 @@ impl sum_tree::Summary for FoldSummary { } } -impl<'a> sum_tree::Dimension<'a, FoldSummary> for Fold { +impl<'a> sum_tree::Dimension<'a, FoldSummary> for FoldRange { 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 { +impl<'a> sum_tree::SeekTarget<'a, FoldSummary, FoldRange> for FoldRange { fn cmp(&self, other: &Self, buffer: &MultiBufferSnapshot) -> Ordering { self.0.cmp(&other.0, buffer) } @@ -959,7 +996,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> { @@ -1321,7 +1358,10 @@ mod tests { 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)) + .map(|fold| { + fold.range.start.to_point(&buffer_snapshot) + ..fold.range.end.to_point(&buffer_snapshot) + }) .collect::>(); assert_eq!( fold_ranges, @@ -1553,10 +1593,9 @@ mod tests { .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 + start.cmp(&fold.range.end, &buffer_snapshot) == Ordering::Less + && end.cmp(&fold.range.start, &buffer_snapshot) == Ordering::Greater }) - .map(|fold| fold.0) .collect::>(); assert_eq!( @@ -1629,7 +1668,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 { @@ -1638,10 +1678,10 @@ mod tests { 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)); + folds.sort_by(|a, b| a.range.cmp(&b.range, buffer)); let mut fold_ranges = folds .iter() - .map(|fold| fold.0.start.to_offset(buffer)..fold.0.end.to_offset(buffer)) + .map(|fold| fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer)) .peekable(); let mut merged_ranges = Vec::new(); diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index c0c352453b..84fad96a48 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/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::{ @@ -1889,7 +1889,8 @@ mod tests { } fn init_test(cx: &mut AppContext) { - cx.set_global(SettingsStore::test(cx)); - theme::init((), cx); + let store = SettingsStore::test(cx); + cx.set_global(store); + theme::init(theme::LoadThemes::JustBase, cx); } } diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index 60337661c1..05aa381627 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -4,15 +4,14 @@ use super::{ Highlights, }; use crate::MultiBufferSnapshot; -use gpui::{ - fonts::FontId, text_layout::LineWrapper, AppContext, Entity, ModelContext, ModelHandle, 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,13 +21,9 @@ 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, f32), -} - -impl Entity for WrapMap { - type Event = (); + font_with_size: (Font, Pixels), } #[derive(Clone)] @@ -74,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, - ) -> (ModelHandle, WrapSnapshot) { - let handle = cx.add_model(|cx| { + ) -> (Model, WrapSnapshot) { + let handle = cx.new_model(|cx| { let mut this = Self { - font: (font_id, font_size), + font_with_size: (font, font_size), wrap_width: None, pending_edits: Default::default(), interpolated_edits: Default::default(), @@ -121,14 +116,16 @@ impl WrapMap { (self.snapshot.clone(), mem::take(&mut self.edits_since_sync)) } - pub fn set_font( + pub fn set_font_with_size( &mut self, - font_id: FontId, - font_size: f32, + font: Font, + font_size: Pixels, cx: &mut ModelContext, ) -> bool { - if (font_id, font_size) != self.font { - self.font = (font_id, font_size); + 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 { @@ -136,7 +133,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; } @@ -153,34 +154,36 @@ 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 (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 mut edits = Patch::default(); + let text_system = cx.text_system().clone(); + 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, font_size).log_err() + { + let tab_snapshot = new_snapshot.tab_snapshot.clone(); + let range = TabPoint::zero()..tab_snapshot.max_point(); + 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)) => { 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 { @@ -194,7 +197,8 @@ impl WrapMap { this.background_task = None; this.flush_edits(cx); cx.notify(); - }); + }) + .ok(); })); } } @@ -237,23 +241,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 (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 text_system = cx.text_system().clone(); + let (font, font_size) = self.font_with_size.clone(); + 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, 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)) => { @@ -272,7 +278,8 @@ impl WrapMap { this.background_task = None; this.flush_edits(cx); cx.notify(); - }); + }) + .ok(); })); } } @@ -385,7 +392,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)] @@ -1026,37 +1033,34 @@ mod tests { display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap}, MultiBuffer, }; - use gpui::test::observe; + use gpui::{font, px, test::observe}; use rand::prelude::*; use settings::SettingsStore; use smol::stream::StreamExt; use std::{cmp, env, num::NonZeroU32}; use text::Rope; + use theme::LoadThemes; #[gpui::test(iterations = 100)] async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) { + // todo!() this test is flaky init_test(cx); - cx.foreground().set_block_on_ticks(0..=50); + cx.background_executor.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 text_system = cx.read(|cx| cx.text_system().clone()); let mut wrap_width = if rng.gen_bool(0.1) { None } else { - Some(rng.gen_range(0.0..=1000.0)) + Some(px(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; + let font = font("Helvetica"); + let _font_id = text_system.font_id(&font).unwrap(); + let font_size = px(14.0); log::info!("Tab size: {}", tab_size); log::info!("Wrap width: {:?}", wrap_width); @@ -1082,12 +1086,12 @@ mod tests { 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 mut line_wrapper = text_system.line_wrapper(font.clone(), font_size).unwrap(); 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)); + cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font, font_size, wrap_width, cx)); let mut notifications = observe(&wrap_map, cx); if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { @@ -1118,7 +1122,7 @@ mod tests { wrap_width = if rng.gen_bool(0.2) { None } else { - Some(rng.gen_range(0.0..=1000.0)) + Some(px(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)); @@ -1272,16 +1276,16 @@ mod tests { } fn init_test(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); cx.update(|cx| { - cx.set_global(SettingsStore::test(cx)); - theme::init((), cx); + let settings = SettingsStore::test(cx); + cx.set_global(settings); + theme::init(LoadThemes::JustBase, cx); }); } fn wrap_text( unwrapped_text: &str, - wrap_width: Option, + wrap_width: Option, line_wrapper: &mut LineWrapper, ) -> String { if let Some(wrap_width) = wrap_width { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5cfcf02174..85a156a8eb 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -20,13 +20,12 @@ 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 anyhow::{anyhow, Context as _, Result}; use blink_manager::BlinkManager; use client::{Client, Collaborator, ParticipantIndex, TelemetrySettings}; -use clock::{Global, ReplicaId}; +use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; use convert_case::{Case, Casing}; use copilot::Copilot; @@ -38,19 +37,14 @@ pub use element::{ }; use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; +use git::diff_hunk_to_display; 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, + actions, div, impl_actions, point, prelude::*, px, relative, rems, size, uniform_list, Action, + AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context, + DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, + HighlightStyle, Hsla, InputHandler, InteractiveText, KeyContext, Model, MouseButton, + ParentElement, Pixels, Render, SharedString, Styled, StyledText, Subscription, Task, TextStyle, + UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -61,16 +55,14 @@ 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, + Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, + LanguageRegistry, LanguageServerName, 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 link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState}; +use lsp::{DiagnosticSeverity, LanguageServerId}; +use mouse_context_menu::MouseContextMenu; use movement::TextLayoutDetails; use multi_buffer::ToOffsetUtf16; pub use multi_buffer::{ @@ -80,14 +72,14 @@ pub use multi_buffer::{ 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 rand::prelude::*; +use rpc::proto::{self, *}; use scroll::{ autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide, }; use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection}; use serde::{Deserialize, Serialize}; -use settings::SettingsStore; +use settings::{Settings, SettingsStore}; use smallvec::SmallVec; use snippet::Snippet; use std::{ @@ -99,16 +91,19 @@ use std::{ ops::{ControlFlow, Deref, DerefMut, Range, RangeInclusive}, path::Path, sync::Arc, + sync::Weak, time::{Duration, Instant}, }; pub use sum_tree::Bias; use sum_tree::TreeMap; -use text::Rope; -use theme::{DiagnosticStyle, Theme, ThemeSettings}; +use text::{OffsetUtf16, Rope}; +use theme::{ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, ThemeColors, ThemeSettings}; +use ui::{ + h_stack, ButtonSize, ButtonStyle, Icon, IconButton, ListItem, ListItemSpacing, Popover, Tooltip, +}; +use ui::{prelude::*, IconSize}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; -use workspace::{ItemNavHistory, SplitDirection, ViewId, Workspace}; - -use crate::git::diff_hunk_to_display; +use workspace::{searchable::SearchEvent, ItemNavHistory, Pane, SplitDirection, ViewId, Workspace}; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); const MAX_LINE_LEN: usize = 1024; @@ -120,147 +115,163 @@ pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); -pub fn render_parsed_markdown( +pub fn render_parsed_markdown( + element_id: impl Into, parsed: &language::ParsedMarkdown, editor_style: &EditorStyle, - workspace: Option>, + workspace: Option>, cx: &mut ViewContext, -) -> Text { - enum RenderedMarkdown {} +) -> InteractiveText { + let code_span_background_color = cx + .theme() + .colors() + .editor_document_highlight_read_background; - let parsed = parsed.clone(); - let view_id = cx.view_id(); - let code_span_background_color = editor_style.document_highlight_read_background; + let highlights = gpui::combine_highlights( + parsed.highlights.iter().filter_map(|(range, highlight)| { + let highlight = highlight.to_highlight_style(&editor_style.syntax)?; + Some((range.clone(), highlight)) + }), + parsed + .regions + .iter() + .zip(&parsed.region_ranges) + .filter_map(|(region, range)| { + if region.code { + Some(( + range.clone(), + HighlightStyle { + background_color: Some(code_span_background_color), + ..Default::default() + }, + )) + } else { + None + } + }), + ); - let mut region_id = 0; + let mut links = Vec::new(); + let mut link_ranges = Vec::new(); + for (range, region) in parsed.region_ranges.iter().zip(&parsed.regions) { + if let Some(link) = region.link.clone() { + links.push(link); + link_ranges.push(range.clone()); + } + } - 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(); - }); - } - } - }), - ); + InteractiveText::new( + element_id, + StyledText::new(parsed.text.clone()).with_highlights(&editor_style.text, highlights), + ) + .on_click(link_ranges, move |clicked_range_ix, cx| { + match &links[clicked_range_ix] { + markdown::Link::Web { url } => cx.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)] +#[derive(PartialEq, Clone, Deserialize, Default)] pub struct SelectNext { #[serde(default)] pub replace_newest: bool, } -#[derive(Clone, Deserialize, PartialEq, Default)] +#[derive(PartialEq, Clone, Deserialize, Default)] pub struct SelectPrevious { #[serde(default)] pub replace_newest: bool, } -#[derive(Clone, Deserialize, PartialEq, Default)] +#[derive(PartialEq, Clone, Deserialize, Default)] pub struct SelectAllMatches { #[serde(default)] pub replace_newest: bool, } -#[derive(Clone, Deserialize, PartialEq)] +#[derive(PartialEq, Clone, Deserialize, Default)] pub struct SelectToBeginningOfLine { #[serde(default)] stop_at_soft_wraps: bool, } -#[derive(Clone, Default, Deserialize, PartialEq)] +#[derive(PartialEq, Clone, Deserialize, Default)] pub struct MovePageUp { #[serde(default)] center_cursor: bool, } -#[derive(Clone, Default, Deserialize, PartialEq)] +#[derive(PartialEq, Clone, Deserialize, Default)] pub struct MovePageDown { #[serde(default)] center_cursor: bool, } -#[derive(Clone, Deserialize, PartialEq)] +#[derive(PartialEq, Clone, Deserialize, Default)] pub struct SelectToEndOfLine { #[serde(default)] stop_at_soft_wraps: bool, } -#[derive(Clone, Deserialize, PartialEq)] +#[derive(PartialEq, Clone, Deserialize, Default)] pub struct ToggleCodeActions { #[serde(default)] pub deployed_from_indicator: bool, } -#[derive(Clone, Default, Deserialize, PartialEq)] +#[derive(PartialEq, Clone, Deserialize, Default)] pub struct ConfirmCompletion { #[serde(default)] pub item_ix: Option, } -#[derive(Clone, Default, Deserialize, PartialEq)] +#[derive(PartialEq, Clone, Deserialize, Default)] pub struct ConfirmCodeAction { #[serde(default)] pub item_ix: Option, } -#[derive(Clone, Default, Deserialize, PartialEq)] +#[derive(PartialEq, Clone, Deserialize, Default)] pub struct ToggleComments { #[serde(default)] pub advance_downwards: bool, } -#[derive(Clone, Default, Deserialize, PartialEq)] +#[derive(PartialEq, Clone, Deserialize, Default)] pub struct FoldAt { pub buffer_row: u32, } -#[derive(Clone, Default, Deserialize, PartialEq)] +#[derive(PartialEq, Clone, Deserialize, Default)] pub struct UnfoldAt { pub buffer_row: u32, } -#[derive(Clone, Default, Deserialize, PartialEq)] -pub struct GutterHover { - pub hovered: bool, -} +impl_actions!( + editor, + [ + SelectNext, + SelectPrevious, + SelectAllMatches, + SelectToBeginningOfLine, + MovePageUp, + MovePageDown, + SelectToEndOfLine, + ToggleCodeActions, + ConfirmCompletion, + ConfirmCodeAction, + ToggleComments, + FoldAt, + UnfoldAt + ] +); #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum InlayId { @@ -280,134 +291,122 @@ impl InlayId { actions!( editor, [ - Cancel, + AddSelectionAbove, + AddSelectionBelow, Backspace, + Cancel, + ConfirmRename, + ContextMenuFirst, + ContextMenuLast, + ContextMenuNext, + ContextMenuPrev, + ConvertToKebabCase, + ConvertToLowerCamelCase, + ConvertToLowerCase, + ConvertToSnakeCase, + ConvertToTitleCase, + ConvertToUpperCamelCase, + ConvertToUpperCase, + Copy, + CopyHighlightJson, + CopyPath, + CopyRelativePath, + Cut, + CutToEndOfLine, Delete, + DeleteLine, + DeleteToBeginningOfLine, + DeleteToEndOfLine, + DeleteToNextSubwordEnd, + DeleteToNextWordEnd, + DeleteToPreviousSubwordStart, + DeleteToPreviousWordStart, + DuplicateLine, + ExpandMacroRecursively, + FindAllReferences, + Fold, + FoldSelectedRanges, + Format, + GoToDefinition, + GoToDefinitionSplit, + GoToDiagnostic, + GoToHunk, + GoToPrevDiagnostic, + GoToPrevHunk, + GoToTypeDefinition, + GoToTypeDefinitionSplit, + HalfPageDown, + HalfPageUp, + Hover, + Indent, + JoinLines, + LineDown, + LineUp, + MoveDown, + MoveLeft, + MoveLineDown, + MoveLineUp, + MoveRight, + MoveToBeginning, + MoveToBeginningOfLine, + MoveToEnclosingBracket, + MoveToEnd, + MoveToEndOfLine, + MoveToEndOfParagraph, + MoveToNextSubwordEnd, + MoveToNextWordEnd, + MoveToPreviousSubwordStart, + MoveToPreviousWordStart, + MoveToStartOfParagraph, + MoveUp, Newline, NewlineAbove, NewlineBelow, - GoToDiagnostic, - GoToPrevDiagnostic, - GoToHunk, - GoToPrevHunk, - Indent, + NextScreen, + OpenExcerpts, Outdent, - DeleteLine, - DeleteToPreviousWordStart, - DeleteToPreviousSubwordStart, - DeleteToNextWordEnd, - DeleteToNextSubwordEnd, - DeleteToBeginningOfLine, - DeleteToEndOfLine, - CutToEndOfLine, - DuplicateLine, - ExpandMacroRecursively, - 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, + PageUp, + Paste, + Redo, + RedoSelection, + Rename, + RestartLanguageServer, + RevealInFinder, + ReverseLines, + ScrollCursorBottom, + ScrollCursorCenter, + ScrollCursorTop, + SelectAll, SelectDown, + SelectLargerSyntaxNode, SelectLeft, + SelectLine, SelectRight, - SelectToPreviousWordStart, - SelectToPreviousSubwordStart, - SelectToNextWordEnd, - SelectToNextSubwordEnd, - SelectToStartOfParagraph, - SelectToEndOfParagraph, + SelectSmallerSyntaxNode, SelectToBeginning, SelectToEnd, - SelectAll, - SelectLine, + SelectToEndOfParagraph, + SelectToNextSubwordEnd, + SelectToNextWordEnd, + SelectToPreviousSubwordStart, + SelectToPreviousWordStart, + SelectToStartOfParagraph, + SelectUp, + ShowCharacterPalette, + ShowCompletions, + ShuffleLines, + SortLinesCaseInsensitive, + SortLinesCaseSensitive, 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 + ToggleSoftWrap, + Transpose, + Undo, + UndoSelection, + UnfoldLines, ] ); @@ -422,144 +421,41 @@ pub enum Direction { } pub fn init_settings(cx: &mut AppContext) { - settings::register::(cx); + EditorSettings::register(cx); } pub fn init(cx: &mut AppContext) { init_settings(cx); - rust_analyzer_ext::apply_related_actions(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); + cx.observe_new_views( + |workspace: &mut Workspace, _cx: &mut ViewContext| { + workspace.register_action(Editor::new_file); + workspace.register_action(Editor::new_file_in_direction); + }, + ) + .detach(); + + cx.on_action(move |_: &workspace::NewFile, cx| { + let app_state = cx.global::>(); + if let Some(app_state) = app_state.upgrade() { + workspace::open_new(&app_state, cx, |workspace, cx| { + Editor::new_file(workspace, &Default::default(), cx) + }) + .detach(); + } + }); + cx.on_action(move |_: &workspace::NewWindow, cx| { + let app_state = cx.global::>(); + if let Some(app_state) = app_state.upgrade() { + workspace::open_new(&app_state, cx, |workspace, cx| { + Editor::new_file(workspace, &Default::default(), cx) + }) + .detach(); + } + }); } trait InvalidationRegion { @@ -584,7 +480,7 @@ pub enum SelectPhase { Update { position: DisplayPoint, goal_column: u32, - scroll_position: Vector2F, + scroll_position: gpui::Point, }, End, } @@ -611,27 +507,31 @@ pub enum SoftWrap { Column(u32), } -#[derive(Clone)] +#[derive(Clone, Default)] pub struct EditorStyle { + pub background: Hsla, + pub local_player: PlayerColor, pub text: TextStyle, - pub line_height_scalar: f32, - pub placeholder_text: Option, - pub theme: theme::Editor, - pub theme_id: usize, + pub scrollbar_width: Pixels, + pub syntax: Arc, + pub status: StatusColors, + pub inlays_style: HighlightStyle, + pub suggestions_style: HighlightStyle, } 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) -> Color, Vec>); -type InlayBackgroundHighlight = (fn(&Theme) -> Color, Vec); +type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Vec>); +type InlayBackgroundHighlight = (fn(&ThemeColors) -> Hsla, Vec); pub struct Editor { - handle: WeakViewHandle, - buffer: ModelHandle, - display_map: ModelHandle, + handle: WeakView, + focus_handle: FocusHandle, + buffer: Model, + display_map: Model, pub selections: SelectionsCollection, pub scroll_manager: ScrollManager, columnar_selection_tail: Option, @@ -645,12 +545,9 @@ pub struct Editor { ime_transaction: Option, active_diagnostics: Option, 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, @@ -661,10 +558,10 @@ pub struct Editor { inlay_background_highlights: TreeMap, InlayBackgroundHighlight>, nav_history: Option, context_menu: RwLock>, - mouse_context_menu: ViewHandle, + mouse_context_menu: Option, 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, @@ -672,8 +569,8 @@ pub struct Editor { cursor_shape: CursorShape, collapse_matches: bool, autoindent_mode: Option, - workspace: Option<(WeakViewHandle, i64)>, - keymap_context_layers: BTreeMap, + workspace: Option<(WeakView, i64)>, + keymap_context_layers: BTreeMap, input_enabled: bool, read_only: bool, leader_peer_id: Option, @@ -685,7 +582,10 @@ 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>, + gutter_width: Pixels, + style: Option, + editor_actions: Vec)>>, } pub struct EditorSnapshot { @@ -841,7 +741,7 @@ struct SnippetState { pub struct RenameState { pub range: Range, pub old_name: Arc, - pub editor: ViewHandle, + pub editor: View, block_id: BlockId, } @@ -855,7 +755,7 @@ enum ContextMenu { impl ContextMenu { fn select_first( &mut self, - project: Option<&ModelHandle>, + project: Option<&Model>, cx: &mut ViewContext, ) -> bool { if self.visible() { @@ -871,7 +771,7 @@ impl ContextMenu { fn select_prev( &mut self, - project: Option<&ModelHandle>, + project: Option<&Model>, cx: &mut ViewContext, ) -> bool { if self.visible() { @@ -887,7 +787,7 @@ impl ContextMenu { fn select_next( &mut self, - project: Option<&ModelHandle>, + project: Option<&Model>, cx: &mut ViewContext, ) -> bool { if self.visible() { @@ -903,7 +803,7 @@ impl ContextMenu { fn select_last( &mut self, - project: Option<&ModelHandle>, + project: Option<&Model>, cx: &mut ViewContext, ) -> bool { if self.visible() { @@ -927,13 +827,17 @@ impl ContextMenu { fn render( &self, cursor_position: DisplayPoint, - style: EditorStyle, - workspace: Option>, + style: &EditorStyle, + max_height: Pixels, + workspace: Option>, cx: &mut ViewContext, - ) -> (DisplayPoint, AnyElement) { + ) -> (DisplayPoint, AnyElement) { match self { - ContextMenu::Completions(menu) => (cursor_position, menu.render(style, workspace, cx)), - ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx), + ContextMenu::Completions(menu) => ( + cursor_position, + menu.render(style, max_height, workspace, cx), + ), + ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, max_height, cx), } } } @@ -942,63 +846,47 @@ impl ContextMenu { struct CompletionsMenu { id: CompletionId, initial_position: Anchor, - buffer: ModelHandle, + buffer: Model, completions: Arc>>, match_candidates: Arc<[StringMatchCandidate]>, matches: Arc<[StringMatch]>, selected_item: usize, - list: UniformListState, + scroll_handle: UniformListScrollHandle, } impl CompletionsMenu { - fn select_first( - &mut self, - project: Option<&ModelHandle>, - 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.scroll_handle.scroll_to_item(self.selected_item); self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } - fn select_prev( - &mut self, - project: Option<&ModelHandle>, - cx: &mut ViewContext, - ) { + 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.scroll_handle.scroll_to_item(self.selected_item); self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } - fn select_next( - &mut self, - project: Option<&ModelHandle>, - 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 { self.selected_item = 0; } - self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.scroll_handle.scroll_to_item(self.selected_item); self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } - fn select_last( - &mut self, - project: Option<&ModelHandle>, - 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.scroll_handle.scroll_to_item(self.selected_item); self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } @@ -1008,7 +896,7 @@ impl CompletionsMenu { editor: &Editor, cx: &mut ViewContext, ) -> Option> { - let settings = settings::get::(cx); + let settings = EditorSettings::get_global(cx); if !settings.show_completion_documentation { return None; } @@ -1069,9 +957,12 @@ impl CompletionsMenu { 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 server = project + .read_with(&mut cx, |project, _| { + project.language_server_for_id(server_id) + }) + .ok() + .flatten(); let Some(server) = server else { return; }; @@ -1093,10 +984,10 @@ impl CompletionsMenu { fn attempt_resolve_selected_completion_documentation( &mut self, - project: Option<&ModelHandle>, + project: Option<&Model>, cx: &mut ViewContext, ) { - let settings = settings::get::(cx); + let settings = EditorSettings::get_global(cx); if !settings.show_completion_documentation { return; } @@ -1253,13 +1144,12 @@ impl CompletionsMenu { fn render( &self, - style: EditorStyle, - workspace: Option>, + style: &EditorStyle, + max_height: Pixels, + workspace: Option>, cx: &mut ViewContext, - ) -> AnyElement { - enum CompletionTag {} - - let settings = settings::get::(cx); + ) -> AnyElement { + let settings = EditorSettings::get_global(cx); let show_completion_documentation = settings.show_completion_documentation; let widest_completion_ix = self @@ -1285,178 +1175,123 @@ impl CompletionsMenu { let completions = self.completions.clone(); let matches = self.matches.clone(); let selected_item = self.selected_item; + let style = style.clone(); - let list = UniformList::new(self.list.clone(), matches.len(), cx, { - let style = style.clone(); - move |_, range, items, cx| { + let multiline_docs = { + let mat = &self.matches[selected_item]; + let multiline_docs = match &self.completions.read()[mat.candidate_id].documentation { + Some(Documentation::MultiLinePlainText(text)) => { + Some(div().child(SharedString::from(text.clone()))) + } + Some(Documentation::MultiLineMarkdown(parsed)) => Some(div().child( + render_parsed_markdown("completions_markdown", parsed, &style, workspace, cx), + )), + _ => None, + }; + multiline_docs.map(|div| { + div.id("multiline_docs") + .max_h(max_height) + .flex_1() + .px_1p5() + .py_1() + .min_w(px(260.)) + .max_w(px(640.)) + .w(px(500.)) + .overflow_y_scroll() + // Prevent a mouse down on documentation from being propagated to the editor, + // because that would move the cursor. + .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) + }) + }; + + let list = uniform_list( + cx.view().clone(), + "completions", + matches.len(), + move |_editor, range, 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]; + matches[range] + .iter() + .enumerate() + .map(|(ix, mat)| { + 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 - }; + 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), + let highlights = gpui::combine_highlights( + mat.ranges().map(|range| (range, FontWeight::BOLD.into())), + styled_runs_for_code_label(&completion.label, &style.syntax).map( + |(range, mut highlight)| { + // Ignore font weight for syntax highlighting, as we'll use it + // for fuzzy matches. + highlight.font_weight = None; + (range, highlight) }, - cx, - ) - .map(|task| task.detach()); - }) - .constrained() - .with_min_width(style.autocomplete.completion_min_width) - .with_max_width(style.autocomplete.completion_max_width) - .into_any(), - ); - } - } - }) + ), + ); + let completion_label = StyledText::new(completion.label.text.clone()) + .with_highlights(&style.text, highlights); + let documentation_label = + if let Some(Documentation::SingleLine(text)) = documentation { + if text.trim().is_empty() { + None + } else { + Some( + h_stack().ml_4().child( + Label::new(text.clone()) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + } + } else { + None + }; + + div().min_w(px(220.)).max_w(px(540.)).child( + ListItem::new(mat.candidate_id) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .selected(item_ix == selected_item) + .on_click(cx.listener(move |editor, _event, cx| { + cx.stop_propagation(); + editor + .confirm_completion( + &ConfirmCompletion { + item_ix: Some(item_ix), + }, + cx, + ) + .map(|task| task.detach_and_log_err(cx)); + })) + .child(h_stack().overflow_hidden().child(completion_label)) + .end_slot::
(documentation_label), + ) + }) + .collect() + }, + ) + .max_h(max_height) + .track_scroll(self.scroll_handle.clone()) .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, - } + Popover::new() + .child(list) + .when_some(multiline_docs, |popover, multiline_docs| { + popover.aside(multiline_docs) }) - .contained() - .with_style(style.autocomplete.container) - .into_any() + .into_any_element() } - pub async fn filter(&mut self, query: Option<&str>, executor: Arc) { + 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, @@ -1505,15 +1340,15 @@ impl CompletionsMenu { 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; + let completion = &completions[mat.candidate_id]; + mat.string = completion.label.text.clone(); for position in &mut mat.positions { - *position += filter_start; + *position += completion.label.filter_range.start; } } + drop(completions); self.matches = matches.into(); self.selected_item = 0; @@ -1523,16 +1358,16 @@ impl CompletionsMenu { #[derive(Clone)] struct CodeActionsMenu { actions: Arc<[CodeAction]>, - buffer: ModelHandle, + buffer: Model, selected_item: usize, - list: UniformListState, + scroll_handle: UniformListScrollHandle, 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)); + self.scroll_handle.scroll_to_item(self.selected_item); cx.notify() } @@ -1542,7 +1377,7 @@ impl CodeActionsMenu { } else { self.selected_item = self.actions.len() - 1; } - self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.scroll_handle.scroll_to_item(self.selected_item); cx.notify(); } @@ -1552,13 +1387,13 @@ impl CodeActionsMenu { } else { self.selected_item = 0; } - self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.scroll_handle.scroll_to_item(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)); + self.scroll_handle.scroll_to_item(self.selected_item); cx.notify() } @@ -1569,64 +1404,63 @@ impl CodeActionsMenu { fn render( &self, mut cursor_position: DisplayPoint, - style: EditorStyle, + _style: &EditorStyle, + max_height: Pixels, cx: &mut ViewContext, - ) -> (DisplayPoint, AnyElement) { - enum ActionTag {} - - let container_style = style.autocomplete.container; + ) -> (DisplayPoint, AnyElement) { 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, + let element = uniform_list( + cx.view().clone(), + "code_actions_menu", + self.actions.len(), + move |_this, range, cx| { + actions[range.clone()] + .iter() + .enumerate() + .map(|(ix, action)| { + let item_ix = range.start + ix; + let selected = selected_item == item_ix; + let colors = cx.theme().colors(); + div() + .px_2() + .text_color(colors.text) + .when(selected, |style| { + style + .bg(colors.element_active) + .text_color(colors.text_accent) + }) + .hover(|style| { + style + .bg(colors.element_hover) + .text_color(colors.text_accent) + }) + .on_mouse_down( + MouseButton::Left, + cx.listener(move |editor, _, cx| { + cx.stop_propagation(); + editor + .confirm_code_action( &ConfirmCodeAction { item_ix: Some(item_ix), }, cx, - ) { - task.detach_and_log_err(cx); - } - }); - } - }); - }) - .into_any(), - ); - } + ) + .map(|task| task.detach_and_log_err(cx)); + }), + ) + // TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here. + .child(SharedString::from(action.lsp_action.title.clone())) + }) + .collect() }, ) + .elevation_1(cx) + .px_2() + .py_1() + .max_h(max_height) + .track_scroll(self.scroll_handle.clone()) .with_width_from_item( self.actions .iter() @@ -1634,9 +1468,7 @@ impl CodeActionsMenu { .max_by_key(|(_, action)| action.lsp_action.title.chars().count()) .map(|(ix, _)| ix), ) - .contained() - .with_style(container_style) - .into_any(); + .into_any_element(); if self.deployed_from_indicator { *cursor_position.column_mut() = 0; @@ -1773,7 +1605,7 @@ pub struct NavigationData { scroll_top_row: u32, } -pub struct EditorCreated(pub ViewHandle); +pub struct EditorCreated(pub View); enum GotoDefinitionKind { Symbol, @@ -1803,65 +1635,43 @@ 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 single_line(cx: &mut ViewContext) -> Self { + let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), String::new())); + let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new(EditorMode::SingleLine, buffer, None, 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 multi_line(cx: &mut ViewContext) -> Self { + let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), String::new())); + let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new(EditorMode::Full, buffer, None, 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 auto_height(max_lines: usize, cx: &mut ViewContext) -> Self { + let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), String::new())); + let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new(EditorMode::AutoHeight { max_lines }, buffer, None, cx) } pub fn for_buffer( - buffer: ModelHandle, - project: Option>, + 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) + let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new(EditorMode::Full, buffer, project, cx) } pub fn for_multibuffer( - buffer: ModelHandle, - project: Option>, + buffer: Model, + project: Option>, cx: &mut ViewContext, ) -> Self { - Self::new(EditorMode::Full, buffer, project, None, cx) + Self::new(EditorMode::Full, buffer, project, 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, - ); + let mut clone = Self::new(self.mode, self.buffer.clone(), self.project.clone(), cx); self.display_map.update(cx, |display_map, cx| { let snapshot = display_map.snapshot(cx); clone.display_map.update(cx, |display_map, cx| { @@ -1876,29 +1686,19 @@ impl Editor { fn new( mode: EditorMode, - buffer: ModelHandle, - project: Option>, - get_field_editor_theme: Option>, + buffer: Model, + project: 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 style = cx.text_style(); + let font_size = style.font_size.to_pixels(cx.rem_size()); + let display_map = cx.new_model(|cx| { + DisplayMap::new(buffer.clone(), style.font(), 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 blink_manager = cx.new_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx)); let soft_wrap_mode_override = (mode == EditorMode::SingleLine).then(|| language_settings::SoftWrap::None); @@ -1908,7 +1708,7 @@ impl Editor { if let Some(project) = project.as_ref() { if buffer.read(cx).is_singleton() { project_subscriptions.push(cx.observe(project, |_, _, cx| { - cx.emit(Event::TitleChanged); + cx.emit(EditorEvent::TitleChanged); })); } project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| { @@ -1925,8 +1725,13 @@ impl Editor { cx, ); + let focus_handle = cx.focus_handle(); + cx.on_focus(&focus_handle, Self::handle_focus).detach(); + cx.on_blur(&focus_handle, Self::handle_blur).detach(); + let mut this = Self { - handle: cx.weak_handle(), + handle: cx.view().downgrade(), + focus_handle, buffer: buffer.clone(), display_map: display_map.clone(), selections, @@ -1942,10 +1747,8 @@ impl Editor { 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, @@ -1957,8 +1760,7 @@ impl Editor { 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)), + mouse_context_menu: None, completion_tasks: Default::default(), next_completion_id: 0, next_inlay_id: 0, @@ -1967,7 +1769,6 @@ impl Editor { 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, @@ -1983,13 +1784,17 @@ impl Editor { inlay_hint_cache: InlayHintCache::new(inlay_hint_settings), gutter_hovered: false, pixel_position_of_newest_cursor: None, + gutter_width: Default::default(), + style: None, + editor_actions: Default::default(), _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| { + 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); @@ -2007,11 +1812,12 @@ impl Editor { this.end_selection(cx); this.scroll_manager.show_scrollbar(cx); - let editor_created_event = EditorCreated(cx.handle()); - cx.emit_global(editor_created_event); + // todo!("use a different mechanism") + // 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(); + let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars(); cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars)); } @@ -2019,6 +1825,48 @@ impl Editor { this } + fn key_context(&self, cx: &AppContext) -> KeyContext { + let mut key_context = KeyContext::default(); + key_context.add("Editor"); + let mode = match self.mode { + EditorMode::SingleLine => "single_line", + EditorMode::AutoHeight { .. } => "auto_height", + EditorMode::Full => "full", + }; + key_context.set("mode", mode); + if self.pending_rename.is_some() { + key_context.add("renaming"); + } + if self.context_menu_visible() { + match self.context_menu.read().as_ref() { + Some(ContextMenu::Completions(_)) => { + key_context.add("menu"); + key_context.add("showing_completions") + } + Some(ContextMenu::CodeActions(_)) => { + key_context.add("menu"); + key_context.add("showing_code_actions") + } + None => {} + } + } + + for layer in self.keymap_context_layers.values() { + key_context.extend(layer); + } + + if let Some(extension) = self + .buffer + .read(cx) + .as_singleton() + .and_then(|buffer| buffer.read(cx).file()?.path().extension()?.to_str()) + { + key_context.set("extension", extension.to_string()); + } + + key_context + } + pub fn new_file( workspace: &mut Workspace, _: &workspace::NewFile, @@ -2026,13 +1874,13 @@ impl Editor { ) { let project = workspace.project().clone(); if project.read(cx).is_remote() { - cx.propagate_action(); + cx.propagate(); } 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))), + Box::new(cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))), cx, ); } @@ -2045,14 +1893,14 @@ impl Editor { ) { let project = workspace.project().clone(); if project.read(cx).is_remote() { - cx.propagate_action(); + cx.propagate(); } 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))), + Box::new(cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))), cx, ); } @@ -2066,12 +1914,16 @@ impl Editor { self.leader_peer_id } - pub fn buffer(&self) -> &ModelHandle { + pub fn buffer(&self) -> &Model { &self.buffer } - fn workspace(&self, cx: &AppContext) -> Option> { - self.workspace.as_ref()?.0.upgrade(cx) + pub fn workspace(&self) -> Option> { + self.workspace.as_ref()?.0.upgrade() + } + + pub fn pane(&self, cx: &AppContext) -> Option> { + self.workspace()?.read(cx).pane_for(&self.handle.upgrade()?) } pub fn title<'a>(&self, cx: &'a AppContext) -> Cow<'a, str> { @@ -2086,42 +1938,39 @@ impl Editor { 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)), + is_focused: self.focus_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 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 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)> { + ) -> 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 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 @@ -2135,13 +1984,20 @@ impl Editor { self.collaboration_hub = Some(hub); } + pub fn placeholder_text(&self) -> Option<&str> { + self.placeholder_text.as_deref() + } + pub fn set_placeholder_text( &mut self, placeholder_text: impl Into>, cx: &mut ViewContext, ) { - self.placeholder_text = Some(placeholder_text.into()); - cx.notify(); + let placeholder_text = Some(placeholder_text.into()); + if self.placeholder_text != placeholder_text { + self.placeholder_text = placeholder_text; + cx.notify(); + } } pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext) { @@ -2169,7 +2025,7 @@ impl Editor { pub fn set_keymap_context_layer( &mut self, - context: KeymapContext, + context: KeyContext, cx: &mut ViewContext, ) { self.keymap_context_layers @@ -2202,22 +2058,13 @@ impl Editor { 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() { + if self.focus_handle.is_focused(cx) && self.leader_peer_id.is_none() { self.buffer.update(cx, |buffer, cx| { buffer.set_active_selections( &self.selections.disjoint_anchors(), @@ -2274,7 +2121,7 @@ impl Editor { 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()) + .filter(query.as_deref(), cx.background_executor().clone()) .await; this.update(&mut cx, |this, cx| { @@ -2317,7 +2164,12 @@ impl Editor { } self.blink_manager.update(cx, BlinkManager::pause_blinking); - cx.emit(Event::SelectionsChanged { local }); + cx.emit(EditorEvent::SelectionsChanged { local }); + + if self.selections.disjoint_anchors().len() == 1 { + cx.emit(SearchEvent::ActiveMatchChanged) + } + cx.notify(); } @@ -2464,8 +2316,8 @@ impl Editor { click_count: usize, cx: &mut ViewContext, ) { - if !self.focused { - cx.focus_self(); + if !self.focus_handle.is_focused(cx) { + cx.focus(&self.focus_handle); } let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); @@ -2530,8 +2382,8 @@ impl Editor { goal_column: u32, cx: &mut ViewContext, ) { - if !self.focused { - cx.focus_self(); + if !self.focus_handle.is_focused(cx) { + cx.focus(&self.focus_handle); } let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); @@ -2551,7 +2403,7 @@ impl Editor { &mut self, position: DisplayPoint, goal_column: u32, - scroll_position: Vector2F, + scroll_position: gpui::Point, cx: &mut ViewContext, ) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); @@ -2636,7 +2488,7 @@ impl Editor { s.set_pending(pending, mode); }); } else { - error!("update_selection dispatched with no pending selection"); + log::error!("update_selection dispatched with no pending selection"); return; } @@ -2739,7 +2591,7 @@ impl Editor { } } - cx.propagate_action(); + cx.propagate(); } pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext) { @@ -2914,7 +2766,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::get_global(cx).use_on_type_format { if let Some(on_type_format_task) = this.trigger_on_type_formatting(text.to_string(), cx) { @@ -3233,7 +3085,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::get_global(cx).show_completions_on_input { return; } @@ -3435,7 +3287,7 @@ impl Editor { } } - fn visible_inlay_hints(&self, cx: &ViewContext<'_, '_, Editor>) -> Vec { + fn visible_inlay_hints(&self, cx: &ViewContext<'_, Editor>) -> Vec { self.display_map .read(cx) .current_inlays() @@ -3449,8 +3301,8 @@ impl Editor { pub fn excerpts_for_inlay_hints_query( &self, restrict_to_languages: Option<&HashSet>>, - cx: &mut ViewContext<'_, '_, Editor>, - ) -> HashMap, Global, Range)> { + cx: &mut ViewContext, + ) -> HashMap, clock::Global, Range)> { let Some(project) = self.project.as_ref() else { return HashMap::default(); }; @@ -3482,6 +3334,7 @@ impl Editor { if worktree_entry.is_ignored { return None; } + let language = buffer.language()?; if let Some(restrict_to_languages) = restrict_to_languages { if !restrict_to_languages.contains(language) { @@ -3502,9 +3355,9 @@ impl Editor { 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), + text_system: cx.text_system().clone(), + editor_style: self.style.clone().unwrap(), + rem_size: cx.rem_size(), } } @@ -3554,9 +3407,11 @@ impl Editor { 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()); - }); + buffer + .update(&mut cx, |buffer, _| { + buffer.push_transaction(transaction, Instant::now()); + }) + .ok(); } editor.update(&mut cx, |editor, cx| { editor.refresh_document_highlights(cx); @@ -3616,10 +3471,10 @@ impl Editor { completions: Arc::new(RwLock::new(completions.into())), matches: Vec::new().into(), selected_item: 0, - list: Default::default(), + scroll_handle: UniformListScrollHandle::new(), }; - - menu.filter(query.as_deref(), cx.background()).await; + menu.filter(query.as_deref(), cx.background_executor().clone()) + .await; if menu.matches.is_empty() { (None, None) @@ -3652,16 +3507,16 @@ impl Editor { _ => return, } - if this.focused && menu.is_some() { + if this.focus_handle.is_focused(cx) && 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.len() <= 1 { - // If there are no more completion tasks (omitting ourself) and - // the last menu was empty, we should hide it. If it was already - // hidden, we should also show the copilot suggestion when available. + // 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); @@ -3769,7 +3624,7 @@ impl Editor { } let text = &text[common_prefix_len..]; - cx.emit(Event::InputHandled { + cx.emit(EditorEvent::InputHandled { utf16_range_to_replace: range_to_replace, text: text.into(), }); @@ -3805,7 +3660,7 @@ impl Editor { cx, ) }); - Some(cx.foreground().spawn(async move { + Some(cx.foreground_executor().spawn(async move { apply_edits.await?; Ok(()) })) @@ -3829,7 +3684,7 @@ impl Editor { } this.update(&mut cx, |this, cx| { - if this.focused { + if this.focus_handle.is_focused(cx) { if let Some((buffer, actions)) = this.available_code_actions.clone() { this.completion_tasks.clear(); this.discard_copilot_suggestion(cx); @@ -3838,9 +3693,10 @@ impl Editor { buffer, actions, selected_item: Default::default(), - list: Default::default(), + scroll_handle: UniformListScrollHandle::default(), deployed_from_indicator, })); + cx.notify(); } } })?; @@ -3851,14 +3707,11 @@ impl Editor { } pub fn confirm_code_action( - workspace: &mut Workspace, + &mut self, action: &ConfirmCodeAction, - cx: &mut ViewContext, + 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))? - { + let actions_menu = if let ContextMenu::CodeActions(menu) = self.hide_context_menu(cx)? { menu } else { return None; @@ -3867,37 +3720,44 @@ impl Editor { let action = actions_menu.actions.get(action_ix)?.clone(); let title = action.lsp_action.title.clone(); let buffer = actions_menu.buffer; + let workspace = self.workspace()?; - 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 apply_code_actions = workspace + .read(cx) + .project() + .clone() + .update(cx, |project, cx| { + project.apply_code_action(buffer, action, true, cx) + }); + let workspace = workspace.downgrade(); + Some(cx.spawn(|editor, 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, + this: &WeakView, + workspace: WeakView, transaction: ProjectTransaction, title: String, - mut cx: AsyncAppContext, + mut cx: AsyncWindowContext, ) -> Result<()> { - let replica_id = this.read_with(&cx, |this, cx| this.replica_id(cx))?; + let replica_id = this.update(&mut 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())) - }); + cx.update(|_, cx| { + entries.sort_unstable_by_key(|(buffer, _)| { + buffer.read(cx).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| { + let excerpt = this.update(&mut cx, |editor, cx| { editor .buffer() .read(cx) @@ -3913,7 +3773,7 @@ impl Editor { excerpt_range.start <= range.start && excerpt_range.end >= range.end }) - }); + })?; if all_edits_within_excerpt { return Ok(()); @@ -3926,7 +3786,7 @@ impl Editor { } let mut ranges_to_highlight = Vec::new(); - let excerpt_buffer = cx.add_model(|cx| { + let excerpt_buffer = cx.new_model(|cx| { let mut multibuffer = MultiBuffer::new(replica_id).with_title(title); for (buffer_handle, transaction) in &entries { let buffer = buffer_handle.read(cx); @@ -3943,17 +3803,17 @@ impl Editor { } 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)); + cx.new_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, + |theme| theme.editor_highlighted_line_background, cx, ); }); @@ -3973,16 +3833,20 @@ impl Editor { } 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) - }) + cx.background_executor() + .timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT) .await; + 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 + }; + this.update(&mut cx, |this, cx| { - this.available_code_actions = actions.log_err().and_then(|actions| { + this.available_code_actions = actions.and_then(|actions| { if actions.is_empty() { None } else { @@ -4013,16 +3877,20 @@ impl Editor { } self.document_highlights_task = Some(cx.spawn(|this, mut cx| async move { - cx.background() + cx.background_executor() .timer(DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT) .await; - let highlights = project + let highlights = if let Some(highlights) = project .update(&mut cx, |project, cx| { project.document_highlights(&cursor_buffer, cursor_buffer_position, cx) }) - .await - .log_err(); + .log_err() + { + highlights.await.log_err() + } else { + None + }; if let Some(highlights) = highlights { this.update(&mut cx, |this, cx| { @@ -4077,12 +3945,12 @@ impl Editor { this.highlight_background::( read_ranges, - |theme| theme.editor.document_highlight_read_background, + |theme| theme.editor_document_highlight_read_background, cx, ); this.highlight_background::( write_ranges, - |theme| theme.editor.document_highlight_write_background, + |theme| theme.editor_document_highlight_write_background, cx, ); cx.notify(); @@ -4116,13 +3984,17 @@ impl Editor { 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; + cx.background_executor() + .timer(COPILOT_DEBOUNCE_TIMEOUT) + .await; } 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() @@ -4171,6 +4043,7 @@ impl Editor { .update(&mut cx, |copilot, cx| { copilot.completions_cycling(&buffer, buffer_position, cx) }) + .log_err()? .await; this.update(&mut cx, |this, cx| { @@ -4205,7 +4078,7 @@ impl Editor { } else { let is_copilot_disabled = self.refresh_copilot_suggestions(false, cx).is_none(); if is_copilot_disabled { - cx.propagate_action(); + cx.propagate(); } } } @@ -4220,7 +4093,7 @@ impl Editor { } else { let is_copilot_disabled = self.refresh_copilot_suggestions(false, cx).is_none(); if is_copilot_disabled { - cx.propagate_action(); + cx.propagate(); } } } @@ -4236,7 +4109,7 @@ impl Editor { self.report_copilot_event(Some(completion.uuid.clone()), true, cx) } - cx.emit(Event::InputHandled { + cx.emit(EditorEvent::InputHandled { utf16_range_to_replace: None, text: suggestion.text.to_string().into(), }); @@ -4344,34 +4217,24 @@ impl Editor { pub fn render_code_actions_indicator( &self, - style: &EditorStyle, + _style: &EditorStyle, is_active: bool, cx: &mut ViewContext, - ) -> Option> { + ) -> 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(), + IconButton::new("code_actions_indicator", ui::Icon::Bolt) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .selected(is_active) + .on_click(cx.listener(|editor, _e, cx| { + editor.toggle_code_actions( + &ToggleCodeActions { + deployed_from_indicator: true, + }, + cx, + ); + })), ) } else { None @@ -4381,16 +4244,12 @@ impl Editor { pub fn render_fold_indicators( &self, fold_data: Vec>, - style: &EditorStyle, + _style: &EditorStyle, gutter_hovered: bool, - line_height: f32, - gutter_margin: f32, + _line_height: Pixels, + _gutter_margin: Pixels, cx: &mut ViewContext, - ) -> Vec>> { - enum FoldIndicators {} - - let style = style.folds.clone(); - + ) -> Vec> { fold_data .iter() .enumerate() @@ -4398,43 +4257,20 @@ impl Editor { 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 { + IconButton::new(ix as usize, ui::Icon::ChevronDown) + .on_click(cx.listener(move |editor, _e, cx| match fold_status { FoldStatus::Folded => { editor.unfold_at(&UnfoldAt { buffer_row }, cx); } FoldStatus::Foldable => { editor.fold_at(&FoldAt { buffer_row }, cx); } - } - }) - .into_any() + })) + .icon_color(ui::Color::Muted) + .icon_size(ui::IconSize::Small) + .selected(fold_status == FoldStatus::Folded) + .selected_icon(ui::Icon::ChevronRight) + .size(ui::ButtonSize::None) }) }) .flatten() @@ -4452,13 +4288,15 @@ impl Editor { pub fn render_context_menu( &self, cursor_position: DisplayPoint, - style: EditorStyle, + style: &EditorStyle, + max_height: Pixels, cx: &mut ViewContext, - ) -> Option<(DisplayPoint, AnyElement)> { + ) -> Option<(DisplayPoint, AnyElement)> { self.context_menu.read().as_ref().map(|menu| { menu.render( cursor_position, style, + max_height, self.workspace.as_ref().map(|(w, _)| w.clone()), cx, ) @@ -5336,8 +5174,8 @@ impl Editor { 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); + let mut start = fold.range.start.to_point(&buffer); + let mut end = fold.range.end.to_point(&buffer); start.row -= row_delta; end.row -= row_delta; refold_ranges.push(start..end); @@ -5427,8 +5265,8 @@ impl Editor { 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); + let mut start = fold.range.start.to_point(&buffer); + let mut end = fold.range.end.to_point(&buffer); start.row += row_delta; end.row += row_delta; refold_ranges.push(start..end); @@ -5478,7 +5316,9 @@ impl Editor { *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), + display_map + .x_for_display_point(head, &text_layout_details) + .into(), ); selection.collapse_to(head, goal); @@ -5670,7 +5510,7 @@ impl Editor { self.request_autoscroll(Autoscroll::fit(), cx); self.unmark_text(cx); self.refresh_copilot_suggestions(true, cx); - cx.emit(Event::Edited); + cx.emit(EditorEvent::Edited); } } @@ -5685,7 +5525,7 @@ impl Editor { self.request_autoscroll(Autoscroll::fit(), cx); self.unmark_text(cx); self.refresh_copilot_suggestions(true, cx); - cx.emit(Event::Edited); + cx.emit(EditorEvent::Edited); } } @@ -5740,7 +5580,7 @@ impl Editor { } if matches!(self.mode, EditorMode::SingleLine) { - cx.propagate_action(); + cx.propagate(); return; } @@ -5770,7 +5610,7 @@ impl Editor { } if matches!(self.mode, EditorMode::SingleLine) { - cx.propagate_action(); + cx.propagate(); return; } @@ -5820,7 +5660,7 @@ impl Editor { self.take_rename(true, cx); if self.mode == EditorMode::SingleLine { - cx.propagate_action(); + cx.propagate(); return; } @@ -5859,7 +5699,7 @@ impl Editor { } if matches!(self.mode, EditorMode::SingleLine) { - cx.propagate_action(); + cx.propagate(); return; } @@ -6206,7 +6046,7 @@ impl Editor { cx: &mut ViewContext, ) { if matches!(self.mode, EditorMode::SingleLine) { - cx.propagate_action(); + cx.propagate(); return; } @@ -6226,7 +6066,7 @@ impl Editor { cx: &mut ViewContext, ) { if matches!(self.mode, EditorMode::SingleLine) { - cx.propagate_action(); + cx.propagate(); return; } @@ -6246,7 +6086,7 @@ impl Editor { cx: &mut ViewContext, ) { if matches!(self.mode, EditorMode::SingleLine) { - cx.propagate_action(); + cx.propagate(); return; } @@ -6266,7 +6106,7 @@ impl Editor { cx: &mut ViewContext, ) { if matches!(self.mode, EditorMode::SingleLine) { - cx.propagate_action(); + cx.propagate(); return; } @@ -6282,7 +6122,7 @@ impl Editor { pub fn move_to_beginning(&mut self, _: &MoveToBeginning, cx: &mut ViewContext) { if matches!(self.mode, EditorMode::SingleLine) { - cx.propagate_action(); + cx.propagate(); return; } @@ -6302,7 +6142,7 @@ impl Editor { pub fn move_to_end(&mut self, _: &MoveToEnd, cx: &mut ViewContext) { if matches!(self.mode, EditorMode::SingleLine) { - cx.propagate_action(); + cx.propagate(); return; } @@ -6424,8 +6264,8 @@ impl Editor { 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 start_x = display_map.x_for_display_point(range.start, &text_layout_details); + let end_x = display_map.x_for_display_point(range.end, &text_layout_details); let positions = start_x.min(end_x)..start_x.max(end_x); selections.clear(); @@ -6464,16 +6304,16 @@ impl Editor { 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) - }; + let positions = + if let SelectionGoal::HorizontalRange { start, end } = selection.goal { + px(start)..px(end) + } else { + let start_x = + display_map.x_for_display_point(range.start, &text_layout_details); + let end_x = + display_map.x_for_display_point(range.end, &text_layout_details); + start_x.min(end_x)..start_x.max(end_x) + }; while row != end_row { if above { @@ -7025,7 +6865,9 @@ impl Editor { 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_snapshot + .x_for_display_point(display_point, &text_layout_details) + .into(), ); (display_point, goal) }) @@ -7391,7 +7233,7 @@ impl Editor { split: bool, cx: &mut ViewContext, ) { - let Some(workspace) = self.workspace(cx) else { + let Some(workspace) = self.workspace() else { return; }; let buffer = self.buffer.read(cx); @@ -7408,7 +7250,7 @@ impl Editor { GotoDefinitionKind::Type => project.type_definition(&buffer, head, cx), }); - cx.spawn_labeled("Fetching Definition...", |editor, mut cx| async move { + cx.spawn(|editor, mut cx| async move { let definitions = definitions.await?; editor.update(&mut cx, |editor, cx| { editor.navigate_to_definitions( @@ -7431,7 +7273,7 @@ impl Editor { split: bool, cx: &mut ViewContext, ) { - let Some(workspace) = self.workspace(cx) else { + let Some(workspace) = self.workspace() else { return; }; let pane = workspace.read(cx).active_pane().clone(); @@ -7456,7 +7298,7 @@ impl Editor { }); } else { cx.window_context().defer(move |cx| { - let target_editor: ViewHandle = + let target_editor: View = workspace.update(cx, |workspace, cx| { if split { workspace.split_project_item(target.buffer.clone(), cx) @@ -7528,11 +7370,13 @@ impl Editor { .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, - ) - }); + workspace + .update(&mut cx, |workspace, cx| { + Self::open_locations_in_multibuffer( + workspace, locations, replica_id, title, split, cx, + ) + }) + .ok(); anyhow::Ok(()) }) @@ -7574,20 +7418,14 @@ impl Editor { 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) - }) - }; + 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, @@ -7600,51 +7438,45 @@ impl Editor { } pub fn find_all_references( - workspace: &mut Workspace, + &mut self, _: &FindAllReferences, - cx: &mut ViewContext, + 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 = self.buffer.read(cx); + let head = self.selections.newest::(cx).head(); let (buffer, head) = buffer.text_anchor_for_position(head, cx)?; - let replica_id = editor.replica_id(cx); + let replica_id = self.replica_id(cx); - let project = workspace.project().clone(); + let workspace = self.workspace()?; + let project = workspace.read(cx).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(()); - } + Some(cx.spawn(|_, 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, - ); - })?; + 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(()) - }, - )) + Ok(()) + })) } /// Opens a multibuffer with the given project locations in it @@ -7661,7 +7493,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.new_model(|cx| { let mut multibuffer = MultiBuffer::new(replica_id); while let Some(location) = locations.next() { let buffer = location.buffer.read(cx); @@ -7690,13 +7522,13 @@ impl Editor { multibuffer.with_title(title) }); - let editor = cx.add_view(|cx| { + let editor = cx.new_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, + |theme| theme.editor_highlighted_line_background, cx, ); }); @@ -7754,7 +7586,6 @@ impl Editor { 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); @@ -7776,12 +7607,8 @@ impl Editor { // 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))); - } + let rename_editor = cx.new_view(|cx| { + let mut editor = Editor::single_line(cx); editor.buffer.update(cx, |buffer, cx| { buffer.edit([(0..0, old_name.clone())], None, cx) }); @@ -7803,24 +7630,51 @@ impl Editor { this.highlight_text::( ranges, HighlightStyle { - fade_out: Some(style.rename_fade), + fade_out: Some(0.6), ..Default::default() }, cx, ); - cx.focus(&rename_editor); + let rename_focus_handle = rename_editor.focus_handle(cx); + cx.focus(&rename_focus_handle); let block_id = this.insert_blocks( [BlockProperties { style: BlockStyle::Flex, position: range.start.clone(), height: 1, render: Arc::new({ - let editor = rename_editor.clone(); + let rename_editor = rename_editor.clone(); move |cx: &mut BlockContext| { - ChildView::new(&editor, cx) - .contained() - .with_padding_left(cx.anchor_x) - .into_any() + let mut text_style = cx.editor_style.text.clone(); + if let Some(highlight_style) = old_highlight_id + .and_then(|h| h.style(&cx.editor_style.syntax)) + { + text_style = text_style.highlight(highlight_style); + } + div() + .pl(cx.anchor_x) + .child(EditorElement::new( + &rename_editor, + EditorStyle { + background: cx.theme().system().transparent, + local_player: cx.editor_style.local_player, + text: text_style, + scrollbar_width: cx.editor_style.scrollbar_width, + syntax: cx.editor_style.syntax.clone(), + status: cx.editor_style.status.clone(), + // todo!("what about the rest of the highlight style parts for inlays and suggestions?") + inlays_style: HighlightStyle { + color: Some(cx.theme().status().hint), + font_weight: Some(FontWeight::BOLD), + ..HighlightStyle::default() + }, + suggestions_style: HighlightStyle { + color: Some(cx.theme().status().predictive), + ..HighlightStyle::default() + }, + }, + )) + .into_any_element() } }), disposition: BlockDisposition::Below, @@ -7842,33 +7696,39 @@ impl Editor { } pub fn confirm_rename( - workspace: &mut Workspace, + &mut self, _: &ConfirmRename, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> Option>> { - let editor = workspace.active_item(cx)?.act_as::(cx)?; + let rename = self.take_rename(false, cx)?; + let workspace = self.workspace()?; + let (start_buffer, start) = self + .buffer + .read(cx) + .text_anchor_for_position(rename.range.start.clone(), cx)?; + let (end_buffer, end) = self + .buffer + .read(cx) + .text_anchor_for_position(rename.range.end.clone(), cx)?; + if start_buffer != end_buffer { + return None; + } - 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 buffer = start_buffer; + let range = start..end; + let old_name = rename.old_name; + let new_name = rename.editor.read(cx).text(cx); - let rename = workspace.project().clone().update(cx, |project, cx| { - project.perform_rename(buffer.clone(), range.start, new_name.clone(), true, cx) - }); + let rename = workspace + .read(cx) + .project() + .clone() + .update(cx, |project, cx| { + project.perform_rename(buffer.clone(), range.start, new_name.clone(), true, cx) + }); + let workspace = workspace.downgrade(); - let editor = editor.downgrade(); - Some(cx.spawn(|workspace, mut cx| async move { + Some(cx.spawn(|editor, mut cx| async move { let project_transaction = rename.await?; Self::open_project_transaction( &editor, @@ -7892,6 +7752,10 @@ impl Editor { cx: &mut ViewContext, ) -> Option { let rename = self.pending_rename.take()?; + if rename.editor.focus_handle(cx).is_focused(cx) { + cx.focus(&self.focus_handle); + } + self.remove_blocks( [rename.block_id].into_iter().collect(), Some(Autoscroll::fit()), @@ -7939,14 +7803,14 @@ impl Editor { fn perform_format( &mut self, - project: ModelHandle, + 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 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 { @@ -7958,15 +7822,17 @@ impl Editor { 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); + 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(()) }) @@ -8138,10 +8004,10 @@ impl Editor { 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"); + log::error!("unexpectedly ended a transaction that wasn't started by this editor"); } - cx.emit(Event::Edited); + cx.emit(EditorEvent::Edited); Some(tx_id) } else { None @@ -8280,13 +8146,11 @@ impl Editor { } } - pub fn gutter_hover( - &mut self, - GutterHover { hovered }: &GutterHover, - cx: &mut ViewContext, - ) { - self.gutter_hovered = *hovered; - cx.notify(); + pub fn set_gutter_hovered(&mut self, hovered: bool, cx: &mut ViewContext) { + if hovered != self.gutter_hovered { + self.gutter_hovered = hovered; + cx.notify(); + } } pub fn insert_blocks( @@ -8347,6 +8211,17 @@ impl Editor { self.buffer.read(cx).read(cx).text() } + pub fn text_option(&self, cx: &AppContext) -> Option { + let text = self.text(cx); + let text = text.trim(); + + if text.is_empty() { + return None; + } + + Some(text.to_string()) + } + pub fn set_text(&mut self, text: impl Into>, cx: &mut ViewContext) { self.transact(cx, |this, cx| { this.buffer @@ -8404,7 +8279,26 @@ impl Editor { cx.notify(); } - pub fn set_wrap_width(&self, width: Option, cx: &mut AppContext) -> bool { + pub fn set_style(&mut self, style: EditorStyle, cx: &mut ViewContext) { + let rem_size = cx.rem_size(); + self.display_map.update(cx, |map, cx| { + map.set_font( + style.text.font(), + style.text.font_size.to_pixels(rem_size), + cx, + ) + }); + self.style = Some(style); + } + + #[cfg(any(test, feature = "test-support"))] + pub fn style(&self) -> Option<&EditorStyle> { + self.style.as_ref() + } + + // Called by the element. This method is not designed to be called outside of the editor + // element's layout code because it does not notify when rewrapping is computed synchronously. + pub(crate) fn set_wrap_width(&self, width: Option, cx: &mut AppContext) -> bool { self.display_map .update(cx, |map, cx| map.set_wrap_width(width, cx)) } @@ -8471,7 +8365,7 @@ impl Editor { pub fn highlight_background( &mut self, ranges: Vec>, - color_fetcher: fn(&Theme) -> Color, + color_fetcher: fn(&ThemeColors) -> Hsla, cx: &mut ViewContext, ) { self.background_highlights @@ -8482,7 +8376,7 @@ impl Editor { pub fn highlight_inlay_background( &mut self, ranges: Vec, - color_fetcher: fn(&Theme) -> Color, + color_fetcher: fn(&ThemeColors) -> Hsla, cx: &mut ViewContext, ) { // TODO: no actual highlights happen for inlays currently, find a way to do that @@ -8509,13 +8403,13 @@ impl Editor { pub fn all_text_background_highlights( &mut self, cx: &mut ViewContext, - ) -> Vec<(Range, Color)> { + ) -> Vec<(Range, Hsla)> { 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()) + let theme = cx.theme().colors(); + self.background_highlights_in_range(start..end, &snapshot, theme) } fn document_highlights_for_position<'a>( @@ -8559,8 +8453,8 @@ impl Editor { &self, search_range: Range, display_snapshot: &DisplaySnapshot, - theme: &Theme, - ) -> Vec<(Range, Color)> { + theme: &ThemeColors, + ) -> Vec<(Range, Hsla)> { let mut results = Vec::new(); for (color_fetcher, ranges) in self.background_highlights.values() { let color = color_fetcher(theme); @@ -8708,17 +8602,17 @@ impl Editor { } } - pub fn show_local_cursors(&self, cx: &AppContext) -> bool { - self.blink_manager.read(cx).visible() && self.focused + pub fn show_local_cursors(&self, cx: &WindowContext) -> bool { + self.blink_manager.read(cx).visible() && self.focus_handle.is_focused(cx) } - fn on_buffer_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { + fn on_buffer_changed(&mut self, _: Model, cx: &mut ViewContext) { cx.notify(); } fn on_buffer_event( &mut self, - multibuffer: ModelHandle, + multibuffer: Model, event: &multi_buffer::Event, cx: &mut ViewContext, ) { @@ -8731,7 +8625,8 @@ impl Editor { if self.has_active_copilot_suggestion(cx) { self.update_visible_copilot_suggestion(cx); } - cx.emit(Event::BufferEdited); + cx.emit(EditorEvent::BufferEdited); + cx.emit(SearchEvent::MatchesInvalidated); if *sigleton_buffer_edited { if let Some(project) = &self.project { @@ -8767,7 +8662,7 @@ impl Editor { predecessor, excerpts, } => { - cx.emit(Event::ExcerptsAdded { + cx.emit(EditorEvent::ExcerptsAdded { buffer: buffer.clone(), predecessor: *predecessor, excerpts: excerpts.clone(), @@ -8776,15 +8671,16 @@ impl Editor { } multi_buffer::Event::ExcerptsRemoved { ids } => { self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx); - cx.emit(Event::ExcerptsRemoved { ids: ids.clone() }) + cx.emit(EditorEvent::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::Reparsed => cx.emit(EditorEvent::Reparsed), + multi_buffer::Event::DirtyChanged => cx.emit(EditorEvent::DirtyChanged), + multi_buffer::Event::Saved => cx.emit(EditorEvent::Saved), + multi_buffer::Event::FileHandleChanged | multi_buffer::Event::Reloaded => { + cx.emit(EditorEvent::TitleChanged) + } + multi_buffer::Event::DiffBaseChanged => cx.emit(EditorEvent::DiffBaseChanged), + multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed), multi_buffer::Event::DiagnosticsUpdated => { self.refresh_active_diagnostics(cx); } @@ -8792,7 +8688,7 @@ impl Editor { }; } - fn on_display_map_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { + fn on_display_map_changed(&mut self, _: Model, cx: &mut ViewContext) { cx.notify(); } @@ -8816,27 +8712,20 @@ impl Editor { 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); + fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext) { + let buffer = self.buffer.read(cx); if buffer.is_singleton() { - cx.propagate_action(); + cx.propagate(); return; } + let Some(workspace) = self.workspace() else { + cx.propagate(); + return; + }; + let mut new_selections_by_buffer = HashMap::default(); - for selection in editor.selections.all::(cx) { + for selection in self.selections.all::(cx) { for (buffer, mut range, _) in buffer.range_to_buffer_ranges(selection.start..selection.end, cx) { @@ -8850,38 +8739,43 @@ impl Editor { } } - 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()); + self.push_to_nav_history(self.selections.newest_anchor().head(), None, cx); // 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); - }); - }); - } + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + let pane = workspace.active_pane().clone(); + pane.update(cx, |pane, _| pane.disable_history()); - pane.update(cx, |pane, _| pane.enable_history()); + 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, + &mut self, path: ProjectPath, position: Point, anchor: language::Anchor, - cx: &mut ViewContext, + cx: &mut ViewContext, ) { - let editor = workspace.open_path(path, None, true, cx); + let workspace = self.workspace(); cx.spawn(|_, mut cx| async move { + let workspace = workspace.ok_or_else(|| anyhow!("cannot jump without workspace"))?; + let editor = workspace.update(&mut cx, |workspace, cx| { + workspace.open_path(path, None, true, cx) + })?; let editor = editor .await? .downcast::() @@ -8971,7 +8865,7 @@ impl Editor { .map(|a| a.to_string()); let telemetry = project.read(cx).client().telemetry().clone(); - let telemetry_settings = *settings::get::(cx); + let telemetry_settings = *TelemetrySettings::get_global(cx); telemetry.report_copilot_event( telemetry_settings, @@ -9016,7 +8910,7 @@ impl Editor { .raw_user_settings() .get("vim_mode") == Some(&serde_json::Value::Bool(true)); - let telemetry_settings = *settings::get::(cx); + 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 @@ -9064,10 +8958,14 @@ impl Editor { let mut lines = Vec::new(); let mut line: VecDeque = VecDeque::new(); - let theme = &theme::current(cx).editor.syntax; + let Some(style) = self.style.as_ref() else { + return; + }; for chunk in chunks { - let highlight = chunk.syntax_highlight_id.and_then(|id| id.name(theme)); + let highlight = chunk + .syntax_highlight_id + .and_then(|id| id.name(&style.syntax)); let mut chunk_lines = chunk.text.split("\n").peekable(); while let Some(text) = chunk_lines.next() { let mut merged_with_last_token = false; @@ -9115,7 +9013,7 @@ impl Editor { cx: &mut ViewContext, ) { if !self.input_enabled { - cx.emit(Event::InputIgnored { text: text.into() }); + cx.emit(EditorEvent::InputIgnored { text: text.into() }); return; } if let Some(relative_utf16_range) = relative_utf16_range { @@ -9165,6 +9063,66 @@ impl Editor { }); supports } + + pub fn focus(&self, cx: &mut WindowContext) { + cx.focus(&self.focus_handle) + } + + pub fn is_focused(&self, cx: &WindowContext) -> bool { + self.focus_handle.is_focused(cx) + } + + fn handle_focus(&mut self, cx: &mut ViewContext) { + cx.emit(EditorEvent::Focused); + + if let Some(rename) = self.pending_rename.as_ref() { + let rename_editor_focus_handle = rename.editor.read(cx).focus_handle.clone(); + cx.focus(&rename_editor_focus_handle); + } else { + self.blink_manager.update(cx, BlinkManager::enable); + 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, + ); + } + }); + } + } + + pub fn handle_blur(&mut self, cx: &mut ViewContext) { + 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(EditorEvent::Blurred); + cx.notify(); + } + + pub fn register_action( + &mut self, + listener: impl Fn(&A, &mut WindowContext) + 'static, + ) -> &mut Self { + let listener = Arc::new(listener); + + self.editor_actions.push(Box::new(move |cx| { + let _view = cx.view().clone(); + let cx = cx.window_context(); + let listener = listener.clone(); + cx.on_action(TypeId::of::(), move |action, phase, cx| { + let action = action.downcast_ref().unwrap(); + if phase == DispatchPhase::Bubble { + listener(action, cx) + } + }) + })); + self + } } pub trait CollaborationHub { @@ -9175,7 +9133,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() } @@ -9191,7 +9149,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); @@ -9271,7 +9229,7 @@ impl EditorSnapshot { self.placeholder_text.as_ref() } - pub fn scroll_position(&self) -> Vector2F { + pub fn scroll_position(&self) -> gpui::Point { self.scroll_anchor.scroll_position(&self.display_snapshot) } } @@ -9285,7 +9243,7 @@ impl Deref for EditorSnapshot { } #[derive(Clone, Debug, PartialEq, Eq)] -pub enum Event { +pub enum EditorEvent { InputIgnored { text: Arc, }, @@ -9294,7 +9252,7 @@ pub enum Event { text: Arc, }, ExcerptsAdded { - buffer: ModelHandle, + buffer: Model, predecessor: ExcerptId, excerpts: Vec<(ExcerptId, ExcerptRange)>, }, @@ -9320,158 +9278,81 @@ pub enum Event { Closed, } -pub struct EditorFocused(pub ViewHandle); -pub struct EditorBlurred(pub ViewHandle); -pub struct EditorReleased(pub WeakViewHandle); +impl EventEmitter for Editor {} -impl Entity for Editor { - type Event = Event; - - fn release(&mut self, cx: &mut AppContext) { - cx.emit_global(EditorReleased(self.handle.clone())); +impl FocusableView for Editor { + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.focus_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) - }); +impl Render for Editor { + fn render<'a>(&mut self, cx: &mut ViewContext<'a, Self>) -> impl IntoElement { + let settings = ThemeSettings::get_global(cx); + let text_style = match self.mode { + EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle { + color: cx.theme().colors().editor_foreground, + font_family: settings.ui_font.family.clone(), + font_features: settings.ui_font.features, + font_size: rems(0.875).into(), + font_weight: FontWeight::NORMAL, + font_style: FontStyle::Normal, + line_height: relative(settings.buffer_line_height.value()), + background_color: None, + underline: None, + white_space: WhiteSpace::Normal, + }, - 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", + EditorMode::Full => TextStyle { + color: cx.theme().colors().editor_foreground, + font_family: settings.buffer_font.family.clone(), + font_features: settings.buffer_font.features, + font_size: settings.buffer_font_size(cx).into(), + font_weight: FontWeight::NORMAL, + font_style: FontStyle::Normal, + line_height: relative(settings.buffer_line_height.value()), + background_color: None, + underline: None, + white_space: WhiteSpace::Normal, + }, }; - 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); - } + let background = match self.mode { + EditorMode::SingleLine => cx.theme().system().transparent, + EditorMode::AutoHeight { max_lines: _ } => cx.theme().system().transparent, + EditorMode::Full => cx.theme().colors().editor_background, + }; - 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()); - } + EditorElement::new( + cx.view(), + EditorStyle { + background, + local_player: cx.theme().players().local(), + text: text_style, + scrollbar_width: px(12.), + syntax: cx.theme().syntax().clone(), + status: cx.theme().status().clone(), + // todo!("what about the rest of the highlight style parts?") + inlays_style: HighlightStyle { + color: Some(cx.theme().status().hint), + font_weight: Some(FontWeight::BOLD), + ..HighlightStyle::default() + }, + suggestions_style: HighlightStyle { + color: Some(cx.theme().status().predictive), + ..HighlightStyle::default() + }, + }, + ) } +} - fn text_for_range(&self, range_utf16: Range, cx: &AppContext) -> Option { +impl InputHandler for Editor { + fn text_for_range( + &mut self, + range_utf16: Range, + cx: &mut ViewContext, + ) -> Option { Some( self.buffer .read(cx) @@ -9481,7 +9362,7 @@ impl View for Editor { ) } - fn selected_text_range(&self, cx: &AppContext) -> Option> { + fn selected_text_range(&mut self, cx: &mut ViewContext) -> Option> { // Prevent the IME menu from appearing when holding down an alphabetic key // while input is disabled. if !self.input_enabled { @@ -9492,7 +9373,7 @@ impl View for Editor { Some(range.start.0..range.end.0) } - fn marked_text_range(&self, cx: &AppContext) -> Option> { + fn marked_text_range(&self, cx: &mut ViewContext) -> 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) @@ -9510,7 +9391,7 @@ impl View for Editor { cx: &mut ViewContext, ) { if !self.input_enabled { - cx.emit(Event::InputIgnored { text: text.into() }); + cx.emit(EditorEvent::InputIgnored { text: text.into() }); return; } @@ -9540,7 +9421,7 @@ impl View for Editor { }) }); - cx.emit(Event::InputHandled { + cx.emit(EditorEvent::InputHandled { utf16_range_to_replace: range_to_replace, text: text.into(), }); @@ -9571,7 +9452,7 @@ impl View for Editor { cx: &mut ViewContext, ) { if !self.input_enabled { - cx.emit(Event::InputIgnored { text: text.into() }); + cx.emit(EditorEvent::InputIgnored { text: text.into() }); return; } @@ -9614,7 +9495,7 @@ impl View for Editor { }) }); - cx.emit(Event::InputHandled { + cx.emit(EditorEvent::InputHandled { utf16_range_to_replace: range_to_replace, text: text.into(), }); @@ -9639,7 +9520,7 @@ impl View for Editor { } else { this.highlight_text::( marked_ranges.clone(), - this.style(cx).composition_mark, + HighlightStyle::default(), // todo!() this.style(cx).composition_mark, cx, ); } @@ -9677,71 +9558,39 @@ impl View for Editor { 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, - } - }; + fn bounds_for_range( + &mut self, + range_utf16: Range, + element_bounds: gpui::Bounds, + cx: &mut ViewContext, + ) -> Option> { + let text_layout_details = self.text_layout_details(cx); + let style = &text_layout_details.editor_style; + let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); + let font_size = style.text.font_size.to_pixels(cx.rem_size()); + let line_height = style.text.line_height_in_pixels(cx.rem_size()); + let em_width = cx + .text_system() + .typographic_bounds(font_id, font_size, 'm') + .unwrap() + .size + .width; - 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 snapshot = self.snapshot(cx); + let scroll_position = snapshot.scroll_position(); + let scroll_left = scroll_position.x * em_width; + + let start = OffsetUtf16(range_utf16.start).to_display_point(&snapshot); + let x = snapshot.x_for_display_point(start, &text_layout_details) - scroll_left + + self.gutter_width; + let y = line_height * (start.row() as f32 - scroll_position.y); + + Some(Bounds { + origin: element_bounds.origin + point(x, y), + size: size(em_width, line_height), + }) } - - style } trait SelectionExt { @@ -9860,183 +9709,96 @@ impl InvalidationRegion for SnippetState { } } -impl Deref for EditorStyle { - type Target = theme::Editor; +pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> RenderBlock { + let (text_without_backticks, code_ranges) = highlight_diagnostic_message(&diagnostic); - 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() + let color = Some(cx.theme().colors().text_accent); + let group_id: SharedString = cx.block_id.to_string().into(); + // TODO: Nate: We should tint the background of the block with the severity color + // We need to extend the theme before we can do this + h_stack() + .id(cx.block_id) + .group(group_id.clone()) + .relative() + .pl(cx.anchor_x) + .size_full() + .gap_2() + .child( + StyledText::new(text_without_backticks.clone()).with_highlights( + &cx.text_style(), + code_ranges.iter().map(|range| { + ( + range.clone(), + HighlightStyle { + color, + ..Default::default() + }, + ) + }), + ), + ) + .child( + IconButton::new(("copy-block", cx.block_id), Icon::Copy) + .icon_color(Color::Muted) + .size(ButtonSize::Compact) + .style(ButtonStyle::Transparent) + .visible_on_hover(group_id) + .on_click(cx.listener({ + let message = diagnostic.message.clone(); + move |_, _, cx| cx.write_to_clipboard(ClipboardItem::new(message.clone())) + })) + .tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)), + ) + .into_any_element() }) } -pub fn highlight_diagnostic_message( - initial_highlights: Vec, - message: &str, -) -> (String, Vec) { - let mut message_without_backticks = String::new(); +pub fn highlight_diagnostic_message(diagnostic: &Diagnostic) -> (SharedString, Vec>) { + let mut text_without_backticks = String::new(); + let mut code_ranges = Vec::new(); + + if let Some(source) = &diagnostic.source { + text_without_backticks.push_str(&source); + code_ranges.push(0..source.len()); + text_without_backticks.push_str(": "); + } + let mut prev_offset = 0; - let mut inside_block = false; - let mut highlights = initial_highlights; - for (match_ix, (offset, _)) in message + let mut in_code_block = false; + for (ix, _) in diagnostic + .message .match_indices('`') - .chain([(message.len(), "")]) - .enumerate() + .chain([(diagnostic.message.len(), "")]) { - message_without_backticks.push_str(&message[prev_offset..offset]); - if inside_block { - highlights.extend(prev_offset - match_ix..offset - match_ix); + let prev_len = text_without_backticks.len(); + text_without_backticks.push_str(&diagnostic.message[prev_offset..ix]); + prev_offset = ix + 1; + if in_code_block { + code_ranges.push(prev_len..text_without_backticks.len()); + in_code_block = false; + } else { + in_code_block = true; } - - inside_block = !inside_block; - prev_offset = offset + 1; } - (message_without_backticks, highlights) + (text_without_backticks.into(), code_ranges) } -pub fn diagnostic_style( - severity: DiagnosticSeverity, - valid: bool, - theme: &theme::Editor, -) -> DiagnosticStyle { +pub fn diagnostic_style(severity: DiagnosticSeverity, valid: bool, colors: &StatusColors) -> Hsla { 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(), + (DiagnosticSeverity::ERROR, true) => colors.error, + (DiagnosticSeverity::ERROR, false) => colors.error, + (DiagnosticSeverity::WARNING, true) => colors.warning, + (DiagnosticSeverity::WARNING, false) => colors.warning, + (DiagnosticSeverity::INFORMATION, true) => colors.info, + (DiagnosticSeverity::INFORMATION, false) => colors.info, + (DiagnosticSeverity::HINT, true) => colors.info, + (DiagnosticSeverity::HINT, false) => colors.info, + _ => colors.ignored, } } -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, diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index b885e065a1..fd7e2feea3 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -1,8 +1,8 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::Setting; +use settings::Settings; -#[derive(Clone, Deserialize)] +#[derive(Deserialize)] pub struct EditorSettings { pub cursor_blink: bool, pub hover_popover_enabled: bool, @@ -57,7 +57,7 @@ pub struct ScrollbarContent { pub selections: Option, } -impl Setting for EditorSettings { +impl Settings for EditorSettings { const KEY: Option<&'static str> = None; type FileContent = EditorSettingsContent; @@ -65,7 +65,7 @@ impl Setting 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/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index feca741737..4d507e0d37 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -7,14 +7,12 @@ use crate::{ }, JoinLines, }; -use drag_and_drop::DragAndDrop; + use futures::StreamExt; use gpui::{ - executor::Deterministic, - geometry::{rect::RectF, vector::vec2f}, - platform::{WindowBounds, WindowOptions}, + div, serde_json::{self, json}, - TestAppContext, + TestAppContext, VisualTestContext, WindowBounds, WindowOptions, }; use indoc::indoc; use language::{ @@ -34,7 +32,7 @@ use util::{ test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker}, }; use workspace::{ - item::{FollowableItem, Item, ItemHandle}, + item::{FollowEvent, FollowableItem, Item, ItemHandle}, NavigationEntry, ViewId, }; @@ -42,127 +40,110 @@ use workspace::{ 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"); + let buffer = cx.new_model(|cx| { + let mut buffer = language::Buffer::new(0, cx.entity_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); + let editor1 = cx.add_window({ + let events = events.clone(); + |cx| { + let view = cx.view().clone(); + cx.subscribe(&view, move |_, _, event: &EditorEvent, _| { + if matches!(event, EditorEvent::Edited | EditorEvent::BufferEdited) { + events.borrow_mut().push(("editor1", event.clone())); + } + }) + .detach(); + Editor::for_buffer(buffer.clone(), None, cx) + } + }); + + let editor2 = cx.add_window({ + let events = events.clone(); + |cx| { + cx.subscribe(&cx.view().clone(), move |_, _, event: &EditorEvent, _| { + if matches!(event, EditorEvent::Edited | EditorEvent::BufferEdited) { + events.borrow_mut().push(("editor2", event.clone())); + } + }) + .detach(); + Editor::for_buffer(buffer.clone(), None, 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)); + _ = 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) + ("editor1", EditorEvent::Edited), + ("editor1", EditorEvent::BufferEdited), + ("editor2", EditorEvent::BufferEdited), ] ); // Mutating editor 2 will emit an `Edited` event only for that editor. - editor2.update(cx, |editor, cx| editor.delete(&Delete, cx)); + _ = 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), + ("editor2", EditorEvent::Edited), + ("editor1", EditorEvent::BufferEdited), + ("editor2", EditorEvent::BufferEdited), ] ); // Undoing on editor 1 will emit an `Edited` event only for that editor. - editor1.update(cx, |editor, cx| editor.undo(&Undo, cx)); + _ = 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), + ("editor1", EditorEvent::Edited), + ("editor1", EditorEvent::BufferEdited), + ("editor2", EditorEvent::BufferEdited), ] ); // Redoing on editor 1 will emit an `Edited` event only for that editor. - editor1.update(cx, |editor, cx| editor.redo(&Redo, cx)); + _ = 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), + ("editor1", EditorEvent::Edited), + ("editor1", EditorEvent::BufferEdited), + ("editor2", EditorEvent::BufferEdited), ] ); // Undoing on editor 2 will emit an `Edited` event only for that editor. - editor2.update(cx, |editor, cx| editor.undo(&Undo, cx)); + _ = 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), + ("editor2", EditorEvent::Edited), + ("editor1", EditorEvent::BufferEdited), + ("editor2", EditorEvent::BufferEdited), ] ); // Redoing on editor 2 will emit an `Edited` event only for that editor. - editor2.update(cx, |editor, cx| editor.redo(&Redo, cx)); + _ = 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), + ("editor2", EditorEvent::Edited), + ("editor1", EditorEvent::BufferEdited), + ("editor2", EditorEvent::BufferEdited), ] ); // No event is emitted when the mutation is a no-op. - editor2.update(cx, |editor, cx| { + _ = editor2.update(cx, |editor, cx| { editor.change_selections(None, cx, |s| s.select_ranges([0..0])); editor.backspace(&Backspace, cx); @@ -175,14 +156,12 @@ 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); + let buffer = cx.new_model(|cx| language::Buffer::new(0, cx.entity_id().as_u64(), "123456")); + let group_interval = buffer.update(cx, |buffer, _| buffer.transaction_group_interval()); + let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer.clone(), cx)); - editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, cx| { editor.start_transaction_at(now, cx); editor.change_selections(None, cx, |s| s.select_ranges([2..4])); @@ -202,7 +181,7 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { editor.change_selections(None, cx, |s| s.select_ranges([2..2])); // Simulate an edit in another editor - buffer.update(cx, |buffer, cx| { + _ = 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); @@ -247,14 +226,14 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { 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"); + let buffer = cx.new_model(|cx| { + let mut buffer = language::Buffer::new(0, cx.entity_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)); + let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); cx.add_window(|cx| { let mut editor = build_editor(buffer.clone(), cx); @@ -350,67 +329,98 @@ fn test_ime_composition(cx: &mut TestAppContext) { 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| { + let editor = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); + build_editor(buffer, 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)), + editor + .update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] ); - editor.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx); + _ = editor.update(cx, |view, cx| { + view.update_selection( + DisplayPoint::new(3, 3), + 0, + gpui::Point::::default(), + cx, + ); }); assert_eq!( - editor.update(cx, |view, cx| view.selections.display_ranges(cx)), + editor + .update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] ); - editor.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx); + _ = editor.update(cx, |view, cx| { + view.update_selection( + DisplayPoint::new(1, 1), + 0, + gpui::Point::::default(), + cx, + ); }); assert_eq!( - editor.update(cx, |view, cx| view.selections.display_ranges(cx)), + editor + .update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] ); - editor.update(cx, |view, cx| { + _ = editor.update(cx, |view, cx| { view.end_selection(cx); - view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx); + view.update_selection( + DisplayPoint::new(3, 3), + 0, + gpui::Point::::default(), + cx, + ); }); assert_eq!( - editor.update(cx, |view, cx| view.selections.display_ranges(cx)), + editor + .update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] ); - editor.update(cx, |view, cx| { + _ = 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); + view.update_selection( + DisplayPoint::new(0, 0), + 0, + gpui::Point::::default(), + cx, + ); }); assert_eq!( - editor.update(cx, |view, cx| view.selections.display_ranges(cx)), + editor + .update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), [ DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1), DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0) ] ); - editor.update(cx, |view, cx| { + _ = editor.update(cx, |view, cx| { view.end_selection(cx); }); assert_eq!( - editor.update(cx, |view, cx| view.selections.display_ranges(cx)), + editor + .update(cx, |view, cx| view.selections.display_ranges(cx)) + .unwrap(), [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)] ); } @@ -419,14 +429,12 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) { 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); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); + build_editor(buffer, cx) + }); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); assert_eq!( view.selections.display_ranges(cx), @@ -434,17 +442,27 @@ fn test_canceling_pending_selection(cx: &mut TestAppContext) { ); }); - view.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx); + _ = view.update(cx, |view, cx| { + view.update_selection( + DisplayPoint::new(3, 3), + 0, + gpui::Point::::default(), + cx, + ); assert_eq!( view.selections.display_ranges(cx), [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] ); }); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.cancel(&Cancel, cx); - view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx); + view.update_selection( + DisplayPoint::new(1, 1), + 0, + gpui::Point::::default(), + cx, + ); assert_eq!( view.selections.display_ranges(cx), [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] @@ -467,14 +485,12 @@ fn test_clone(cx: &mut TestAppContext) { true, ); - let editor = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple(&text, cx); - build_editor(buffer, cx) - }) - .root(cx); + let editor = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple(&text, cx); + build_editor(buffer, cx) + }); - editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, cx| { editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone())); editor.fold_ranges( [ @@ -488,16 +504,18 @@ fn test_clone(cx: &mut TestAppContext) { let cloned_editor = editor .update(cx, |editor, cx| { - cx.add_window(Default::default(), |cx| editor.clone(cx)) + cx.open_window(Default::default(), |cx| cx.new_view(|cx| editor.clone(cx))) }) - .root(cx); + .unwrap(); - let snapshot = editor.update(cx, |e, cx| e.snapshot(cx)); - let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx)); + let snapshot = editor.update(cx, |e, cx| e.snapshot(cx)).unwrap(); + let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx)).unwrap(); assert_eq!( - cloned_editor.update(cx, |e, cx| e.display_text(cx)), - editor.update(cx, |e, cx| e.display_text(cx)) + cloned_editor + .update(cx, |e, cx| e.display_text(cx)) + .unwrap(), + editor.update(cx, |e, cx| e.display_text(cx)).unwrap() ); assert_eq!( cloned_snapshot @@ -506,127 +524,139 @@ fn test_clone(cx: &mut TestAppContext) { 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)) + cloned_editor + .update(cx, |editor, cx| editor.selections.ranges::(cx)) + .unwrap(), + editor + .update(cx, |editor, cx| editor.selections.ranges(cx)) + .unwrap() ); assert_set_eq!( - cloned_editor.update(cx, |e, cx| e.selections.display_ranges(cx)), - editor.update(cx, |e, cx| e.selections.display_ranges(cx)) + cloned_editor + .update(cx, |e, cx| e.selections.display_ranges(cx)) + .unwrap(), + editor + .update(cx, |e, cx| e.selections.display_ranges(cx)) + .unwrap() ); } +//todo!(editor navigate) #[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 fs = FakeFs::new(cx.executor()); 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))); + let workspace = cx.add_window(|cx| Workspace::test_new(project, cx)); + let pane = workspace + .update(cx, |workspace, _| workspace.active_pane().clone()) + .unwrap(); - fn pop_history(editor: &mut Editor, cx: &mut WindowContext) -> Option { - editor.nav_history.as_mut().unwrap().pop_backward(cx) - } + _ = workspace.update(cx, |_v, cx| { + cx.new_view(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); + let mut editor = build_editor(buffer.clone(), cx); + let handle = cx.view(); + editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle))); - // 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()); + fn pop_history(editor: &mut Editor, cx: &mut WindowContext) -> Option { + editor.nav_history.as_mut().unwrap().pop_backward(cx) + } - // 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. + // 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 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. + // 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.entity_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 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()); + // 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()); - // 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(); + // 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.entity_id()); + assert_eq!( + editor.selections.display_ranges(cx), + &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] + ); + assert!(pop_history(&mut editor, cx).is_none()); - // 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); + // Set scroll position to check later + editor.set_scroll_position(gpui::Point::::new(5.5, 5.5), cx); + let original_scroll_position = editor.scroll_manager.anchor(); - 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); + // Jump to the end of the document and adjust scroll + editor.move_to_end(&MoveToEnd, cx); + editor.set_scroll_position(gpui::Point::::new(-2.5, -0.5), cx); + assert_ne!(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) - ); + 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); - editor + // 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), + gpui::Point::new(0., editor.max_point(cx).row() as f32) + ); + + editor + }) }); } @@ -634,20 +664,28 @@ async fn test_navigation_history(cx: &mut TestAppContext) { 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); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); + build_editor(buffer, cx) + }); - view.update(cx, |view, 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.update_selection( + DisplayPoint::new(1, 1), + 0, + gpui::Point::::default(), + 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.update_selection( + DisplayPoint::new(0, 3), + 0, + gpui::Point::::default(), + cx, + ); view.end_selection(cx); assert_eq!( view.selections.display_ranges(cx), @@ -658,7 +696,7 @@ fn test_cancel(cx: &mut TestAppContext) { ); }); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.cancel(&Cancel, cx); assert_eq!( view.selections.display_ranges(cx), @@ -666,7 +704,7 @@ fn test_cancel(cx: &mut TestAppContext) { ); }); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.cancel(&Cancel, cx); assert_eq!( view.selections.display_ranges(cx), @@ -679,10 +717,9 @@ fn test_cancel(cx: &mut TestAppContext) { fn test_fold_action(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let view = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple( - &" + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple( + &" impl Foo { // Hello! @@ -699,14 +736,13 @@ fn test_fold_action(cx: &mut TestAppContext) { } } " - .unindent(), - cx, - ); - build_editor(buffer.clone(), cx) - }) - .root(cx); + .unindent(), + cx, + ); + build_editor(buffer.clone(), cx) + }); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)]); }); @@ -772,11 +808,9 @@ 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); + let view = cx.add_window(|cx| build_editor(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| { + _ = buffer.update(cx, |buffer, cx| { buffer.edit( vec![ (Point::new(1, 0)..Point::new(1, 0), "\t"), @@ -786,7 +820,7 @@ fn test_move_cursor(cx: &mut TestAppContext) { cx, ); }); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { assert_eq!( view.selections.display_ranges(cx), &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] @@ -849,17 +883,15 @@ fn test_move_cursor(cx: &mut TestAppContext) { 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); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε", cx); + build_editor(buffer.clone(), cx) + }); assert_eq!('ⓐ'.len_utf8(), 3); assert_eq!('α'.len_utf8(), 2); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.fold_ranges( vec![ Point::new(0, 6)..Point::new(0, 12), @@ -963,17 +995,16 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) { }); } +//todo!(finish editor tests) #[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| { + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); + build_editor(buffer.clone(), cx) + }); + _ = view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]); }); @@ -1019,13 +1050,11 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { 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| { + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\n def", cx); + build_editor(buffer, cx) + }); + _ = view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), @@ -1034,7 +1063,7 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { }); }); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); assert_eq!( view.selections.display_ranges(cx), @@ -1045,7 +1074,7 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { ); }); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); assert_eq!( view.selections.display_ranges(cx), @@ -1056,7 +1085,7 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { ); }); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); assert_eq!( view.selections.display_ranges(cx), @@ -1067,7 +1096,7 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { ); }); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.move_to_end_of_line(&MoveToEndOfLine, cx); assert_eq!( view.selections.display_ranges(cx), @@ -1079,7 +1108,7 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { }); // Moving to the end of line again is a no-op. - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.move_to_end_of_line(&MoveToEndOfLine, cx); assert_eq!( view.selections.display_ranges(cx), @@ -1090,7 +1119,7 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { ); }); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.move_left(&MoveLeft, cx); view.select_to_beginning_of_line( &SelectToBeginningOfLine { @@ -1107,7 +1136,7 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { ); }); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.select_to_beginning_of_line( &SelectToBeginningOfLine { stop_at_soft_wraps: true, @@ -1123,7 +1152,7 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { ); }); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.select_to_beginning_of_line( &SelectToBeginningOfLine { stop_at_soft_wraps: true, @@ -1139,7 +1168,7 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { ); }); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.select_to_end_of_line( &SelectToEndOfLine { stop_at_soft_wraps: true, @@ -1155,7 +1184,7 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { ); }); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.delete_to_end_of_line(&DeleteToEndOfLine, cx); assert_eq!(view.display_text(cx), "ab\n de"); assert_eq!( @@ -1167,7 +1196,7 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { ); }); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx); assert_eq!(view.display_text(cx), "\n"); assert_eq!( @@ -1184,13 +1213,11 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { 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| { + 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) + }); + _ = view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11), @@ -1234,20 +1261,18 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) { }); } +//todo!(finish editor tests) #[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); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); + build_editor(buffer, cx) + }); - view.update(cx, |view, cx| { - view.set_wrap_width(Some(140.), cx); + _ = view.update(cx, |view, cx| { + view.set_wrap_width(Some(140.0.into()), cx); assert_eq!( view.display_text(cx), "use one::{\n two::three::\n four::five\n};" @@ -1295,14 +1320,20 @@ fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { }); } +//todo!(simulate_resize) #[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); + let line_height = cx.editor(|editor, cx| { + editor + .style() + .unwrap() + .text + .line_height_in_pixels(cx.rem_size()) + }); + cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height)); cx.set_state( &r#"ˇone @@ -1399,9 +1430,15 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon 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 line_height = cx.editor(|editor, cx| { + editor + .style() + .unwrap() + .text + .line_height_in_pixels(cx.rem_size()) + }); let window = cx.window; - window.simulate_resize(vec2f(1000., 4. * line_height + 0.5), &mut cx); + cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5))); cx.set_state( &r#"ˇone @@ -1418,18 +1455,36 @@ async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) { ); cx.update_editor(|editor, cx| { - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.)); + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 0.) + ); editor.scroll_screen(&ScrollAmount::Page(1.), cx); - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 3.) + ); editor.scroll_screen(&ScrollAmount::Page(1.), cx); - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 6.)); + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 6.) + ); editor.scroll_screen(&ScrollAmount::Page(-1.), cx); - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 3.) + ); editor.scroll_screen(&ScrollAmount::Page(-0.5), cx); - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.)); + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 1.) + ); editor.scroll_screen(&ScrollAmount::Page(0.5), cx); - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 3.) + ); }); } @@ -1440,11 +1495,14 @@ async fn test_autoscroll(cx: &mut gpui::TestAppContext) { let line_height = cx.update_editor(|editor, cx| { editor.set_vertical_scroll_margin(2, cx); - editor.style(cx).text.line_height(cx.font_cache()) + editor + .style() + .unwrap() + .text + .line_height_in_pixels(cx.rem_size()) }); - let window = cx.window; - window.simulate_resize(vec2f(1000., 6.0 * line_height), &mut cx); + cx.simulate_window_resize(window, size(px(1000.), 6. * line_height)); cx.set_state( &r#"ˇone @@ -1460,7 +1518,10 @@ async fn test_autoscroll(cx: &mut gpui::TestAppContext) { "#, ); cx.update_editor(|editor, cx| { - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.0)); + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 0.0) + ); }); // Add a cursor below the visible area. Since both cursors cannot fit @@ -1475,7 +1536,10 @@ async fn test_autoscroll(cx: &mut gpui::TestAppContext) { }) }); cx.update_editor(|editor, cx| { - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.0)); + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 3.0) + ); }); // Move down. The editor cursor scrolls down to track the newest cursor. @@ -1483,7 +1547,10 @@ async fn test_autoscroll(cx: &mut gpui::TestAppContext) { editor.move_down(&Default::default(), cx); }); cx.update_editor(|editor, cx| { - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 4.0)); + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 4.0) + ); }); // Add a cursor above the visible area. Since both cursors fit on screen, @@ -1497,7 +1564,10 @@ async fn test_autoscroll(cx: &mut gpui::TestAppContext) { }) }); cx.update_editor(|editor, cx| { - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.0)); + assert_eq!( + editor.snapshot(cx).scroll_position(), + gpui::Point::new(0., 1.0) + ); }); } @@ -1506,10 +1576,15 @@ 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 line_height = cx.editor(|editor, cx| { + editor + .style() + .unwrap() + .text + .line_height_in_pixels(cx.rem_size()) + }); let window = cx.window; - window.simulate_resize(vec2f(100., 4. * line_height), &mut cx); - + cx.simulate_window_resize(window, size(px(100.), 4. * line_height)); cx.set_state( &r#" ˇone @@ -1632,14 +1707,12 @@ async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) { 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); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("one two three four", cx); + build_editor(buffer.clone(), cx) + }); - view.update(cx, |view, 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 @@ -1652,7 +1725,7 @@ fn test_delete_to_word_boundary(cx: &mut TestAppContext) { assert_eq!(view.buffer.read(cx).read(cx).text(), "e two te four"); }); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ // an empty selection - the following word fragment is deleted @@ -1670,14 +1743,12 @@ fn test_delete_to_word_boundary(cx: &mut TestAppContext) { 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); + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); + build_editor(buffer.clone(), cx) + }); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), @@ -1695,10 +1766,9 @@ fn test_newline(cx: &mut TestAppContext) { fn test_newline_with_old_selections(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let editor = cx - .add_window(|cx| { - let buffer = MultiBuffer::build_simple( - " + let editor = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple( + " a b( X @@ -1707,22 +1777,21 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) { 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); + .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 + }); - editor.update(cx, |editor, 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( @@ -1925,16 +1994,14 @@ async fn test_newline_comments(cx: &mut gpui::TestAppContext) { 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); + 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 + }); - editor.update(cx, |editor, 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); @@ -2280,14 +2347,14 @@ fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { 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 toml_buffer = cx.new_model(|cx| { + Buffer::new(0, cx.entity_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") + let rust_buffer = cx.new_model(|cx| { + Buffer::new(0, cx.entity_id().as_u64(), "const c: usize = 3;\n") .with_language(rust_language, cx) }); - let multibuffer = cx.add_model(|cx| { + let multibuffer = cx.new_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( toml_buffer.clone(), @@ -2441,13 +2508,11 @@ async fn test_delete(cx: &mut gpui::TestAppContext) { 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| { + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + build_editor(buffer, cx) + }); + _ = view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), @@ -2466,13 +2531,11 @@ fn test_delete_line(cx: &mut TestAppContext) { ); }); - 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| { + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + build_editor(buffer, cx) + }); + _ = view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)]) }); @@ -2485,6 +2548,7 @@ fn test_delete_line(cx: &mut TestAppContext) { }); } +//todo!(select_anchor_ranges) #[gpui::test] fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -2876,13 +2940,11 @@ async fn test_manipulate_text(cx: &mut TestAppContext) { 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| { + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + build_editor(buffer, cx) + }); + _ = view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), @@ -2904,13 +2966,11 @@ fn test_duplicate_line(cx: &mut TestAppContext) { ); }); - 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| { + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + build_editor(buffer, cx) + }); + _ = view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1), @@ -2933,13 +2993,11 @@ fn test_duplicate_line(cx: &mut TestAppContext) { 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| { + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); + build_editor(buffer, cx) + }); + _ = view.update(cx, |view, cx| { view.fold_ranges( vec![ Point::new(0, 2)..Point::new(1, 2), @@ -2978,7 +3036,7 @@ fn test_move_line_up_down(cx: &mut TestAppContext) { ); }); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.move_line_down(&MoveLineDown, cx); assert_eq!( view.display_text(cx), @@ -2995,7 +3053,7 @@ fn test_move_line_up_down(cx: &mut TestAppContext) { ); }); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.move_line_down(&MoveLineDown, cx); assert_eq!( view.display_text(cx), @@ -3012,7 +3070,7 @@ fn test_move_line_up_down(cx: &mut TestAppContext) { ); }); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.move_line_up(&MoveLineUp, cx); assert_eq!( view.display_text(cx), @@ -3034,13 +3092,11 @@ fn test_move_line_up_down(cx: &mut TestAppContext) { 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 editor = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); + build_editor(buffer, cx) + }); + _ = editor.update(cx, |editor, cx| { let snapshot = editor.buffer.read(cx).snapshot(cx); editor.insert_blocks( [BlockProperties { @@ -3048,7 +3104,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { position: snapshot.anchor_after(Point::new(2, 0)), disposition: BlockDisposition::Below, height: 1, - render: Arc::new(|_| Empty::new().into_any()), + render: Arc::new(|_| div().into_any()), }], Some(Autoscroll::fit()), cx, @@ -3060,13 +3116,14 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { }); } +//todo!(test_transpose) #[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.set_style(EditorStyle::default(), cx); editor.change_selections(None, cx, |s| s.select_ranges([1..1])); editor.transpose(&Default::default(), cx); assert_eq!(editor.text(cx), "bac"); @@ -3085,7 +3142,7 @@ fn test_transpose(cx: &mut TestAppContext) { _ = cx.add_window(|cx| { let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); - + editor.set_style(EditorStyle::default(), cx); editor.change_selections(None, cx, |s| s.select_ranges([3..3])); editor.transpose(&Default::default(), cx); assert_eq!(editor.text(cx), "acb\nde"); @@ -3109,7 +3166,7 @@ fn test_transpose(cx: &mut TestAppContext) { _ = cx.add_window(|cx| { let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); - + editor.set_style(EditorStyle::default(), 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"); @@ -3136,7 +3193,7 @@ fn test_transpose(cx: &mut TestAppContext) { _ = cx.add_window(|cx| { let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), cx); - + editor.set_style(EditorStyle::default(), cx); editor.change_selections(None, cx, |s| s.select_ranges([4..4])); editor.transpose(&Default::default(), cx); assert_eq!(editor.text(cx), "🏀🍐✋"); @@ -3218,7 +3275,10 @@ async fn test_clipboard(cx: &mut gpui::TestAppContext) { 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")); + assert_eq!( + cx.read_from_clipboard().map(|item| item.text().to_owned()), + Some("fox jumps over\n".to_owned()) + ); // 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. @@ -3354,13 +3414,11 @@ async fn test_paste_multiline(cx: &mut gpui::TestAppContext) { 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| { + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx); + build_editor(buffer, cx) + }); + _ = view.update(cx, |view, cx| { view.select_all(&SelectAll, cx); assert_eq!( view.selections.display_ranges(cx), @@ -3373,13 +3431,11 @@ fn test_select_all(cx: &mut TestAppContext) { 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| { + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx); + build_editor(buffer, cx) + }); + _ = view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), @@ -3398,7 +3454,7 @@ fn test_select_line(cx: &mut TestAppContext) { ); }); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.select_line(&SelectLine, cx); assert_eq!( view.selections.display_ranges(cx), @@ -3409,7 +3465,7 @@ fn test_select_line(cx: &mut TestAppContext) { ); }); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.select_line(&SelectLine, cx); assert_eq!( view.selections.display_ranges(cx), @@ -3422,13 +3478,11 @@ fn test_select_line(cx: &mut TestAppContext) { 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| { + let view = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); + build_editor(buffer, cx) + }); + _ = view.update(cx, |view, cx| { view.fold_ranges( vec![ Point::new(0, 2)..Point::new(1, 2), @@ -3449,7 +3503,7 @@ fn test_split_selection_into_lines(cx: &mut TestAppContext) { assert_eq!(view.display_text(cx), "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"); }); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.split_selection_into_lines(&SplitSelectionIntoLines, cx); assert_eq!( view.display_text(cx), @@ -3466,7 +3520,7 @@ fn test_split_selection_into_lines(cx: &mut TestAppContext) { ); }); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([DisplayPoint::new(5, 0)..DisplayPoint::new(0, 1)]) }); @@ -3492,200 +3546,256 @@ fn test_split_selection_into_lines(cx: &mut TestAppContext) { } #[gpui::test] -fn test_add_selection_above_below(cx: &mut TestAppContext) { +async 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); + let mut cx = EditorTestContext::new(cx).await; - 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) - ] - ); + // let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx); + cx.set_state(indoc!( + r#"abc + defˇghi + + jk + nlmo + "# + )); + + cx.update_editor(|editor, cx| { + editor.add_selection_above(&Default::default(), cx); }); - 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) - ] - ); + cx.assert_editor_state(indoc!( + r#"abcˇ + defˇghi + + jk + nlmo + "# + )); + + cx.update_editor(|editor, cx| { + editor.add_selection_above(&Default::default(), cx); }); - 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)] - ); + cx.assert_editor_state(indoc!( + r#"abcˇ + defˇghi - 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) - ] - ); + jk + nlmo + "# + )); - view.redo_selection(&RedoSelection, cx); - assert_eq!( - view.selections.display_ranges(cx), - vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] - ); + cx.update_editor(|view, cx| { + view.add_selection_below(&Default::default(), cx); }); - 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) - ] - ); + cx.assert_editor_state(indoc!( + r#"abc + defˇghi + + jk + nlmo + "# + )); + + cx.update_editor(|view, cx| { + view.undo_selection(&Default::default(), cx); }); - 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) - ] - ); + cx.assert_editor_state(indoc!( + r#"abcˇ + defˇghi + + jk + nlmo + "# + )); + + cx.update_editor(|view, cx| { + view.redo_selection(&Default::default(), cx); }); - 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) - ] - ); + cx.assert_editor_state(indoc!( + r#"abc + defˇghi + + jk + nlmo + "# + )); + + cx.update_editor(|view, cx| { + view.add_selection_below(&Default::default(), cx); }); - 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) - ] - ); + cx.assert_editor_state(indoc!( + r#"abc + defˇghi + + jk + nlmˇo + "# + )); + + cx.update_editor(|view, cx| { + view.add_selection_below(&Default::default(), cx); }); - 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)] - ); + cx.assert_editor_state(indoc!( + r#"abc + defˇghi + + jk + nlmˇo + "# + )); + + // change selections + cx.set_state(indoc!( + r#"abc + def«ˇg»hi + + jk + nlmo + "# + )); + + cx.update_editor(|view, cx| { + view.add_selection_below(&Default::default(), cx); }); - 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)] - ); + cx.assert_editor_state(indoc!( + r#"abc + def«ˇg»hi + + jk + nlm«ˇo» + "# + )); + + cx.update_editor(|view, cx| { + view.add_selection_below(&Default::default(), cx); }); - 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), - ] - ); + cx.assert_editor_state(indoc!( + r#"abc + def«ˇg»hi + + jk + nlm«ˇo» + "# + )); + + cx.update_editor(|view, cx| { + view.add_selection_above(&Default::default(), cx); }); - 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), - ] - ); + cx.assert_editor_state(indoc!( + r#"abc + def«ˇg»hi + + jk + nlmo + "# + )); + + cx.update_editor(|view, cx| { + view.add_selection_above(&Default::default(), cx); }); - 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), - ] - ); + cx.assert_editor_state(indoc!( + r#"abc + def«ˇg»hi + + jk + nlmo + "# + )); + + // Change selections again + cx.set_state(indoc!( + r#"a«bc + defgˇ»hi + + jk + nlmo + "# + )); + + cx.update_editor(|view, cx| { + view.add_selection_below(&Default::default(), cx); }); - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(4, 3)..DisplayPoint::new(1, 1)]) - }); + cx.assert_editor_state(indoc!( + r#"a«bcˇ» + d«efgˇ»hi + + j«kˇ» + nlmo + "# + )); + + cx.update_editor(|view, cx| { + view.add_selection_below(&Default::default(), cx); }); - 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), - ] - ); + cx.assert_editor_state(indoc!( + r#"a«bcˇ» + d«efgˇ»hi + + j«kˇ» + n«lmoˇ» + "# + )); + cx.update_editor(|view, cx| { + view.add_selection_above(&Default::default(), cx); }); - 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), - ] - ); + cx.assert_editor_state(indoc!( + r#"a«bcˇ» + d«efgˇ»hi + + j«kˇ» + nlmo + "# + )); + + // Change selections again + cx.set_state(indoc!( + r#"abc + d«ˇefghi + + jk + nlm»o + "# + )); + + cx.update_editor(|view, cx| { + view.add_selection_above(&Default::default(), cx); }); + + cx.assert_editor_state(indoc!( + r#"a«ˇbc» + d«ˇef»ghi + + j«ˇk» + n«ˇlm»o + "# + )); + + cx.update_editor(|view, cx| { + view.add_selection_below(&Default::default(), cx); + }); + + cx.assert_editor_state(indoc!( + r#"abc + d«ˇef»ghi + + j«ˇk» + n«ˇlm»o + "# + )); } #[gpui::test] @@ -3795,14 +3905,15 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { "# .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)) + let buffer = cx + .new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)); + let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + + view.condition::(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), @@ -3821,7 +3932,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { ] ); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); }); assert_eq!( @@ -3832,7 +3943,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { ] ); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); }); assert_eq!( @@ -3841,7 +3952,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { ); // Trying to expand the selected syntax node one more time has no effect. - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); }); assert_eq!( @@ -3849,7 +3960,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] ); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); }); assert_eq!( @@ -3860,7 +3971,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { ] ); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); }); assert_eq!( @@ -3872,7 +3983,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { ] ); - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); }); assert_eq!( @@ -3885,7 +3996,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { ); // Trying to shrink the selected syntax node one more time has no effect. - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); }); assert_eq!( @@ -3899,7 +4010,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { // Ensure that we keep expanding the selection if the larger selection starts or ends within // a fold. - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.fold_ranges( vec![ Point::new(0, 21)..Point::new(0, 24), @@ -3959,15 +4070,15 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { 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); + let buffer = cx + .new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)); + let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); editor - .condition(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) + .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) .await; - editor.update(cx, |editor, cx| { + _ = 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"); @@ -4523,14 +4634,14 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { "# .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)) + let buffer = cx + .new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)); + let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + view.condition::(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), @@ -4672,15 +4783,15 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { "# .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); + let buffer = cx + .new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)); + let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); editor - .condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + .condition::(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; - editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, cx| { editor.change_selections(None, cx, |s| { s.select_ranges([ Point::new(0, 1)..Point::new(0, 1), @@ -4751,6 +4862,7 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { }); } +// todo!(select_anchor_ranges) #[gpui::test] async fn test_snippets(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); @@ -4765,9 +4877,9 @@ async fn test_snippets(cx: &mut gpui::TestAppContext) { ); let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); - let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); + let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); - editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, cx| { let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap(); editor @@ -4881,25 +4993,27 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { })) .await; - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); 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))); + _ = 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(); + cx.executor().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 buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, 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)); + let save = editor + .update(cx, |editor, cx| editor.save(project.clone(), cx)) + .unwrap(); fake_server .handle_request::(move |params, _| async move { assert_eq!( @@ -4914,15 +5028,16 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { }) .next() .await; - cx.foreground().start_waiting(); - save.await.unwrap(); + cx.executor().start_waiting(); + let _x = save.await; + assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), + editor.update(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)); + _ = 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. @@ -4934,12 +5049,14 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { 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(); + let save = editor + .update(cx, |editor, cx| editor.save(project.clone(), cx)) + .unwrap(); + cx.executor().advance_clock(super::FORMAT_TIMEOUT); + cx.executor().start_waiting(); + save.await; assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), + editor.update(cx, |editor, cx| editor.text(cx)), "one\ntwo\nthree\n" ); assert!(!cx.read(|cx| editor.is_dirty(cx))); @@ -4955,7 +5072,9 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { ); }); - let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); + let save = editor + .update(cx, |editor, cx| editor.save(project.clone(), cx)) + .unwrap(); fake_server .handle_request::(move |params, _| async move { assert_eq!( @@ -4967,8 +5086,8 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { }) .next() .await; - cx.foreground().start_waiting(); - save.await.unwrap(); + cx.executor().start_waiting(); + save.await; } #[gpui::test] @@ -4993,25 +5112,27 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { })) .await; - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); 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))); + _ = 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(); + cx.executor().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 buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, 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)); + let save = editor + .update(cx, |editor, cx| editor.save(project.clone(), cx)) + .unwrap(); fake_server .handle_request::(move |params, _| async move { assert_eq!( @@ -5026,15 +5147,15 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { }) .next() .await; - cx.foreground().start_waiting(); - save.await.unwrap(); + cx.executor().start_waiting(); + save.await; assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), + editor.update(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)); + _ = 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. @@ -5048,12 +5169,14 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { 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(); + let save = editor + .update(cx, |editor, cx| editor.save(project.clone(), cx)) + .unwrap(); + cx.executor().advance_clock(super::FORMAT_TIMEOUT); + cx.executor().start_waiting(); + save.await; assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), + editor.update(cx, |editor, cx| editor.text(cx)), "one\ntwo\nthree\n" ); assert!(!cx.read(|cx| editor.is_dirty(cx))); @@ -5069,7 +5192,9 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { ); }); - let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); + let save = editor + .update(cx, |editor, cx| editor.save(project.clone(), cx)) + .unwrap(); fake_server .handle_request::(move |params, _| async move { assert_eq!( @@ -5081,8 +5206,8 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { }) .next() .await; - cx.foreground().start_waiting(); - save.await.unwrap(); + cx.executor().start_waiting(); + save.await; } #[gpui::test] @@ -5112,11 +5237,11 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { })) .await; - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); fs.insert_file("/file.rs", Default::default()).await; let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - project.update(cx, |project, _| { + _ = project.update(cx, |project, _| { project.languages().add(Arc::new(language)); }); let buffer = project @@ -5124,16 +5249,18 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { .await .unwrap(); - cx.foreground().start_waiting(); + cx.executor().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 buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, 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) - }); + let format = editor + .update(cx, |editor, cx| { + editor.perform_format(project.clone(), FormatTrigger::Manual, cx) + }) + .unwrap(); fake_server .handle_request::(move |params, _| async move { assert_eq!( @@ -5148,14 +5275,14 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { }) .next() .await; - cx.foreground().start_waiting(); - format.await.unwrap(); + cx.executor().start_waiting(); + format.await; assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), + editor.update(cx, |editor, cx| editor.text(cx)), "one, two\nthree\n" ); - editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + _ = 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!( @@ -5165,14 +5292,16 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { 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(); + let format = editor + .update(cx, |editor, cx| { + editor.perform_format(project, FormatTrigger::Manual, cx) + }) + .unwrap(); + cx.executor().advance_clock(super::FORMAT_TIMEOUT); + cx.executor().start_waiting(); + format.await; assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), + editor.update(cx, |editor, cx| editor.text(cx)), "one\ntwo\nthree\n" ); } @@ -5198,7 +5327,7 @@ async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) { // a newline and an indent before the `.` cx.lsp .handle_request::(move |_, cx| { - let executor = cx.background(); + let executor = cx.background_executor().clone(); async move { executor.timer(Duration::from_millis(100)).await; Ok(Some(vec![lsp::TextEdit { @@ -5212,19 +5341,19 @@ async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) { let format_1 = cx .update_editor(|editor, cx| editor.format(&Format, cx)) .unwrap(); - cx.foreground().run_until_parked(); + cx.executor().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(); + cx.executor().run_until_parked(); // Wait for both format requests to complete - cx.foreground().advance_clock(Duration::from_millis(200)); - cx.foreground().start_waiting(); + cx.executor().advance_clock(Duration::from_millis(200)); + cx.executor().start_waiting(); format_1.await.unwrap(); - cx.foreground().start_waiting(); + cx.executor().start_waiting(); format_2.await.unwrap(); // The formatting edits only happens once. @@ -5492,8 +5621,8 @@ async fn test_completion(cx: &mut gpui::TestAppContext) { handle_resolve_completion_request(&mut cx, None).await; apply_additional_edits.await.unwrap(); - cx.update(|cx| { - cx.update_global::(|settings, cx| { + _ = cx.update(|cx| { + cx.update_global::(|settings, cx| { settings.update_user_settings::(cx, |settings| { settings.show_completions_on_input = Some(false); }); @@ -5872,7 +6001,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { "# .unindent(), ); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); cx.assert_editor_state( &r#" @@ -5888,8 +6017,8 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { 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 buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a'))); + let multibuffer = cx.new_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( buffer.clone(), @@ -5909,8 +6038,8 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { multibuffer }); - let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); - view.update(cx, |view, cx| { + let (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx)); + _ = view.update(cx, |view, cx| { assert_eq!(view.text(cx), "aaaa\nbbbb"); view.change_selections(None, cx, |s| { s.select_ranges([ @@ -5972,15 +6101,15 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { 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 buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), initial_text)); + let multibuffer = cx.new_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 (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx)); + _ = view.update(cx, |view, cx| { let (expected_text, selection_ranges) = marked_text_ranges( indoc! {" aaaa @@ -6030,9 +6159,9 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { 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 buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a'))); let mut excerpt1_id = None; - let multibuffer = cx.add_model(|cx| { + let multibuffer = cx.new_model(|cx| { let mut multibuffer = MultiBuffer::new(0); excerpt1_id = multibuffer .push_excerpts( @@ -6055,27 +6184,25 @@ fn test_refresh_selections(cx: &mut TestAppContext) { 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); + 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 + }); // Refreshing selections is a no-op when excerpts haven't changed. - editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, cx| { editor.change_selections(None, cx, |s| s.refresh()); assert_eq!( editor.selections.ranges(cx), @@ -6086,10 +6213,10 @@ fn test_refresh_selections(cx: &mut TestAppContext) { ); }); - multibuffer.update(cx, |multibuffer, cx| { + _ = multibuffer.update(cx, |multibuffer, cx| { multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx); }); - editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, cx| { // Removing an excerpt causes the first selection to become degenerate. assert_eq!( editor.selections.ranges(cx), @@ -6117,9 +6244,9 @@ fn test_refresh_selections(cx: &mut TestAppContext) { 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 buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a'))); let mut excerpt1_id = None; - let multibuffer = cx.add_model(|cx| { + let multibuffer = cx.new_model(|cx| { let mut multibuffer = MultiBuffer::new(0); excerpt1_id = multibuffer .push_excerpts( @@ -6142,23 +6269,21 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { 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); + 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 + }); - multibuffer.update(cx, |multibuffer, cx| { + _ = multibuffer.update(cx, |multibuffer, cx| { multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx); }); - editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, cx| { assert_eq!( editor.selections.ranges(cx), [Point::new(0, 0)..Point::new(0, 0)] @@ -6214,14 +6339,14 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { "{{} }\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)) + let buffer = cx + .new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)); + let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + view.condition::(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; - view.update(cx, |view, cx| { + _ = view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), @@ -6253,14 +6378,12 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { 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); + let editor = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); + build_editor(buffer.clone(), cx) + }); - editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, cx| { struct Type1; struct Type2; @@ -6276,7 +6399,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) { anchor_range(Point::new(6, 3)..Point::new(6, 5)), anchor_range(Point::new(8, 4)..Point::new(8, 6)), ], - |_| Color::red(), + |_| Hsla::red(), cx, ); editor.highlight_background::( @@ -6286,7 +6409,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) { anchor_range(Point::new(7, 4)..Point::new(7, 7)), anchor_range(Point::new(9, 5)..Point::new(9, 8)), ], - |_| Color::green(), + |_| Hsla::green(), cx, ); @@ -6294,29 +6417,29 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) { 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(), + cx.theme().colors(), ); // Enforce a consistent ordering based on color without relying on the ordering of the - // highlight's `TypeId` which is non-deterministic. + // highlight's `TypeId` which is non-executor. 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(), + Hsla::red(), ), ( DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), - Color::red(), + Hsla::red(), + ), + ( + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5), + Hsla::green(), + ), + ( + DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6), + Hsla::green(), ), ] ); @@ -6324,125 +6447,137 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) { editor.background_highlights_in_range( anchor_range(Point::new(5, 6)..Point::new(6, 4)), &snapshot, - theme::current(cx).as_ref(), + cx.theme().colors(), ), &[( DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), - Color::red(), + Hsla::red(), )] ); }); } +// todo!(following) #[gpui::test] async fn test_following(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); 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)) + cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)) + }); + let leader = cx.add_window(|cx| build_editor(buffer.clone(), cx)); + let follower = cx.update(|cx| { + cx.open_window( + WindowOptions { + bounds: WindowBounds::Fixed(Bounds::from_corners( + gpui::Point::new((0. as f64).into(), (0. as f64).into()), + gpui::Point::new((10. as f64).into(), (80. as f64).into()), + )), + ..Default::default() + }, + |cx| cx.new_view(|cx| build_editor(buffer.clone(), 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, { + _ = 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); - }) + cx.subscribe( + &leader.root_view(cx).unwrap(), + 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; - } - }) + cx.subscribe( + &follower.root_view(cx).unwrap(), + move |_, _, event: &EditorEvent, _cx| { + if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) { + *is_still_following.borrow_mut() = false; + } + + if let EditorEvent::BufferEdited = event { + *follower_edit_event_count.borrow_mut() += 1; + } + }, + ) .detach(); } }); // Update the selections only - leader.update(cx, |leader, cx| { + _ = 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) }) + .unwrap() .await .unwrap(); - follower.read_with(cx, |follower, cx| { + _ = follower.update(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); + _ = leader.update(cx, |leader, cx| { + leader.set_scroll_position(gpui::Point::new(1.5, 3.5), cx); }); follower .update(cx, |follower, cx| { follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) }) + .unwrap() .await .unwrap(); assert_eq!( - follower.update(cx, |follower, cx| follower.scroll_position(cx)), - vec2f(1.5, 3.5) + follower + .update(cx, |follower, cx| follower.scroll_position(cx)) + .unwrap(), + gpui::Point::new(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.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); + leader.set_scroll_position(gpui::Point::new(1.5, 3.5), cx); }); follower .update(cx, |follower, cx| { follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) }) + .unwrap() .await .unwrap(); - follower.update(cx, |follower, cx| { - assert_eq!(follower.scroll_position(cx), vec2f(1.5, 0.0)); + _ = follower.update(cx, |follower, cx| { + assert_eq!(follower.scroll_position(cx), gpui::Point::new(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.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); }); @@ -6450,34 +6585,36 @@ async fn test_following(cx: &mut gpui::TestAppContext) { .update(cx, |follower, cx| { follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) }) + .unwrap() .await .unwrap(); - follower.read_with(cx, |follower, cx| { + _ = follower.update(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.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) }) + .unwrap() .await .unwrap(); - follower.read_with(cx, |follower, cx| { + _ = follower.update(cx, |follower, cx| { assert_eq!(follower.selections.ranges(cx), vec![0..2]); }); // Scrolling locally breaks the follow - follower.update(cx, |follower, cx| { + _ = 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), + offset: gpui::Point::new(0.0, 0.5), }, cx, ); @@ -6489,25 +6626,27 @@ async fn test_following(cx: &mut gpui::TestAppContext) { async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); 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 workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let pane = workspace + .update(cx, |workspace, _| workspace.active_pane().clone()) + .unwrap(); + + let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); let leader = pane.update(cx, |_, cx| { - let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); - cx.add_view(|cx| build_editor(multibuffer.clone(), cx)) + let multibuffer = cx.new_model(|_| MultiBuffer::new(0)); + cx.new_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| { + .update_window(*workspace.deref(), |_, cx| { Editor::from_state_proto( pane.clone(), - workspace.clone(), + workspace.root_view(cx).unwrap(), ViewId { creator: Default::default(), id: 0, @@ -6517,6 +6656,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { ) }) .unwrap() + .unwrap() .await .unwrap(); @@ -6545,7 +6685,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { }); // Insert some excerpts. - leader.update(cx, |leader, cx| { + _ = leader.update(cx, |leader, cx| { leader.buffer.update(cx, |multibuffer, cx| { let excerpt_ids = multibuffer.push_excerpts( buffer_1.clone(), @@ -6591,18 +6731,18 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { .await .unwrap(); assert_eq!( - follower_1.read_with(cx, |editor, cx| editor.text(cx)), - leader.read_with(cx, |editor, cx| editor.text(cx)) + follower_1.update(cx, |editor, cx| editor.text(cx)), + leader.update(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| { + .update_window(*workspace.deref(), |_, cx| { Editor::from_state_proto( pane.clone(), - workspace.clone(), + workspace.root_view(cx).unwrap().clone(), ViewId { creator: Default::default(), id: 0, @@ -6612,15 +6752,16 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { ) }) .unwrap() + .unwrap() .await .unwrap(); assert_eq!( - follower_2.read_with(cx, |editor, cx| editor.text(cx)), - leader.read_with(cx, |editor, cx| editor.text(cx)) + follower_2.update(cx, |editor, cx| editor.text(cx)), + leader.update(cx, |editor, cx| editor.text(cx)) ); // Remove some excerpts. - leader.update(cx, |leader, cx| { + _ = 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); @@ -6643,83 +6784,14 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { .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() - }, - ), - ] + follower_1.update(cx, |editor, cx| editor.text(cx)), + leader.update(cx, |editor, cx| editor.text(cx)) ); } #[gpui::test] async fn go_to_prev_overlapping_diagnostic( - deterministic: Arc, + executor: BackgroundExecutor, cx: &mut gpui::TestAppContext, ) { init_test(cx, |_| {}); @@ -6732,8 +6804,8 @@ async fn go_to_prev_overlapping_diagnostic( } "}); - cx.update(|cx| { - project.update(cx, |project, cx| { + _ = cx.update(|cx| { + _ = project.update(cx, |project, cx| { project .update_diagnostics( LanguageServerId(0), @@ -6774,7 +6846,7 @@ async fn go_to_prev_overlapping_diagnostic( }); }); - deterministic.run_until_parked(); + executor.run_until_parked(); cx.update_editor(|editor, cx| { editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); @@ -6814,7 +6886,7 @@ async fn go_to_prev_overlapping_diagnostic( } #[gpui::test] -async fn go_to_hunk(deterministic: Arc, cx: &mut gpui::TestAppContext) { +async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; @@ -6849,7 +6921,7 @@ async fn go_to_hunk(deterministic: Arc, cx: &mut gpui::TestAppCon ); cx.set_diff_base(Some(&diff_base)); - deterministic.run_until_parked(); + executor.run_until_parked(); cx.update_editor(|editor, cx| { //Wrap around the bottom of the buffer @@ -7024,12 +7096,14 @@ async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) { ); } +// todo!(completions) #[gpui::test(iterations = 10)] -async fn test_copilot(deterministic: Arc, cx: &mut gpui::TestAppContext) { +async fn test_copilot(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { + // flaky init_test(cx, |_| {}); let (copilot, copilot_lsp) = Copilot::fake(cx); - cx.update(|cx| cx.set_global(copilot)); + _ = cx.update(|cx| cx.set_global(copilot)); let mut cx = EditorLspTestContext::new_rust( lsp::ServerCapabilities { completion_provider: Some(lsp::CompletionOptions { @@ -7067,7 +7141,7 @@ async fn test_copilot(deterministic: Arc, cx: &mut gpui::TestAppC }], vec![], ); - deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { assert!(editor.context_menu_visible()); assert!(!editor.has_active_copilot_suggestion(cx)); @@ -7109,7 +7183,7 @@ async fn test_copilot(deterministic: Arc, cx: &mut gpui::TestAppC }], vec![], ); - deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { assert!(!editor.context_menu_visible()); assert!(editor.has_active_copilot_suggestion(cx)); @@ -7142,7 +7216,7 @@ async fn test_copilot(deterministic: Arc, cx: &mut gpui::TestAppC }], vec![], ); - deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { assert!(editor.context_menu_visible()); assert!(!editor.has_active_copilot_suggestion(cx)); @@ -7157,7 +7231,7 @@ async fn test_copilot(deterministic: Arc, cx: &mut gpui::TestAppC // Ensure existing completion is interpolated when inserting again. cx.simulate_keystroke("c"); - deterministic.run_until_parked(); + executor.run_until_parked(); cx.update_editor(|editor, cx| { assert!(!editor.context_menu_visible()); assert!(editor.has_active_copilot_suggestion(cx)); @@ -7175,7 +7249,7 @@ async fn test_copilot(deterministic: Arc, cx: &mut gpui::TestAppC }], vec![], ); - deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { assert!(!editor.context_menu_visible()); assert!(editor.has_active_copilot_suggestion(cx)); @@ -7254,7 +7328,7 @@ async fn test_copilot(deterministic: Arc, cx: &mut gpui::TestAppC ); cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx)); - deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + executor.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}"); @@ -7276,13 +7350,13 @@ async fn test_copilot(deterministic: Arc, cx: &mut gpui::TestAppC #[gpui::test] async fn test_copilot_completion_invalidation( - deterministic: Arc, + executor: BackgroundExecutor, cx: &mut gpui::TestAppContext, ) { init_test(cx, |_| {}); let (copilot, copilot_lsp) = Copilot::fake(cx); - cx.update(|cx| cx.set_global(copilot)); + _ = cx.update(|cx| cx.set_global(copilot)); let mut cx = EditorLspTestContext::new_rust( lsp::ServerCapabilities { completion_provider: Some(lsp::CompletionOptions { @@ -7311,7 +7385,7 @@ async fn test_copilot_completion_invalidation( vec![], ); cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx)); - deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + executor.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"); @@ -7342,18 +7416,15 @@ async fn test_copilot_completion_invalidation( } #[gpui::test] -async fn test_copilot_multibuffer( - deterministic: Arc, - cx: &mut gpui::TestAppContext, -) { +async fn test_copilot_multibuffer(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let (copilot, copilot_lsp) = Copilot::fake(cx); - cx.update(|cx| cx.set_global(copilot)); + _ = 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 buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "a = 1\nb = 2\n")); + let buffer_2 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "c = 3\nd = 4\n")); + let multibuffer = cx.new_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( buffer_1.clone(), @@ -7373,7 +7444,7 @@ async fn test_copilot_multibuffer( ); multibuffer }); - let editor = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); + let editor = cx.add_window(|cx| build_editor(multibuffer, cx)); handle_copilot_completion_request( &copilot_lsp, @@ -7384,15 +7455,15 @@ async fn test_copilot_multibuffer( }], vec![], ); - editor.update(cx, |editor, cx| { + _ = 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| { + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + _ = editor.update(cx, |editor, cx| { assert!(editor.has_active_copilot_suggestion(cx)); assert_eq!( editor.display_text(cx), @@ -7410,7 +7481,7 @@ async fn test_copilot_multibuffer( }], vec![], ); - editor.update(cx, |editor, cx| { + _ = 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)]) @@ -7433,8 +7504,8 @@ async fn test_copilot_multibuffer( }); // Ensure the new suggestion is displayed when the debounce timeout expires. - deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - editor.update(cx, |editor, cx| { + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + _ = editor.update(cx, |editor, cx| { assert!(editor.has_active_copilot_suggestion(cx)); assert_eq!( editor.display_text(cx), @@ -7445,10 +7516,7 @@ async fn test_copilot_multibuffer( } #[gpui::test] -async fn test_copilot_disabled_globs( - deterministic: Arc, - cx: &mut gpui::TestAppContext, -) { +async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { settings .copilot @@ -7457,9 +7525,9 @@ async fn test_copilot_disabled_globs( }); let (copilot, copilot_lsp) = Copilot::fake(cx); - cx.update(|cx| cx.set_global(copilot)); + _ = cx.update(|cx| cx.set_global(copilot)); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/test", json!({ @@ -7483,7 +7551,7 @@ async fn test_copilot_disabled_globs( .await .unwrap(); - let multibuffer = cx.add_model(|cx| { + let multibuffer = cx.new_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( private_buffer.clone(), @@ -7503,7 +7571,7 @@ async fn test_copilot_disabled_globs( ); multibuffer }); - let editor = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); + let editor = cx.add_window(|cx| build_editor(multibuffer, cx)); let mut copilot_requests = copilot_lsp .handle_request::(move |_params, _cx| async move { @@ -7516,24 +7584,24 @@ async fn test_copilot_disabled_globs( }) }); - editor.update(cx, |editor, cx| { + _ = 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); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); assert!(copilot_requests.try_next().is_err()); - editor.update(cx, |editor, cx| { + _ = 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); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); assert!(copilot_requests.try_next().is_ok()); } @@ -7571,7 +7639,7 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { })) .await; - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/a", json!({ @@ -7581,15 +7649,18 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { ) .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() + _ = project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let worktree_id = workspace + .update(cx, |workspace, cx| { + workspace.project().update(cx, |project, cx| { + project.worktrees().next().unwrap().read(cx).id() + }) }) - }); + .unwrap(); let buffer = project .update(cx, |project, cx| { @@ -7597,13 +7668,14 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { }) .await .unwrap(); - cx.foreground().run_until_parked(); - cx.foreground().start_waiting(); + cx.executor().run_until_parked(); + cx.executor().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) }) + .unwrap() .await .unwrap() .downcast::() @@ -7626,16 +7698,16 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { }); editor_handle.update(cx, |editor, cx| { - cx.focus(&editor_handle); + editor.focus(cx); 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(); + cx.executor().run_until_parked(); - buffer.read_with(cx, |buffer, _| { + _ = buffer.update(cx, |buffer, _| { assert_eq!( buffer.text(), "fn main() { let a = {5}; }", @@ -7678,7 +7750,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test })) .await; - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/a", json!({ @@ -7688,7 +7760,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test ) .await; let project = Project::test(fs, ["/a".as_ref()], cx).await; - project.update(cx, |project, _| project.languages().add(Arc::new(language))); + _ = 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| { @@ -7706,7 +7778,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test }, ); }); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); assert_eq!( server_restarts.load(atomic::Ordering::Acquire), 0, @@ -7723,7 +7795,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test }, ); }); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); assert_eq!( server_restarts.load(atomic::Ordering::Acquire), 0, @@ -7740,7 +7812,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test }, ); }); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); assert_eq!( server_restarts.load(atomic::Ordering::Acquire), 1, @@ -7757,7 +7829,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test }, ); }); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); assert_eq!( server_restarts.load(atomic::Ordering::Acquire), 1, @@ -7772,7 +7844,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test }, ); }); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); assert_eq!( server_restarts.load(atomic::Ordering::Acquire), 2, @@ -7929,7 +8001,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui: // 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.executor().run_until_parked(); cx.update_editor(|editor, _| { if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { assert_eq!( @@ -7942,7 +8014,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui: }); cx.simulate_keystroke("l"); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); cx.update_editor(|editor, _| { if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { assert_eq!( @@ -7958,7 +8030,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui: // be the start of a subword. cx.set_state(r#"

"#); cx.simulate_keystroke("l"); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); cx.update_editor(|editor, _| { if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { assert_eq!( @@ -7995,12 +8067,12 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { })) .await; - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); 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.update(cx, |project, _| { project.languages().add(Arc::new(language)); }); let buffer = project @@ -8009,16 +8081,18 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { .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 buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, 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(); + editor + .update(cx, |editor, cx| { + editor.perform_format(project.clone(), FormatTrigger::Manual, cx) + }) + .unwrap() + .await; assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), + editor.update(cx, |editor, cx| editor.text(cx)), buffer_text.to_string() + prettier_format_suffix, "Test prettier formatting was not applied to the original buffer text", ); @@ -8031,7 +8105,7 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { }); format.await.unwrap(); assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), + editor.update(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", ); @@ -8160,8 +8234,8 @@ pub(crate) fn update_test_language_settings( cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent), ) { - cx.update(|cx| { - cx.update_global::(|store, cx| { + _ = cx.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { store.update_user_settings::(cx, f); }); }); @@ -8171,19 +8245,18 @@ pub(crate) fn update_test_project_settings( cx: &mut TestAppContext, f: impl Fn(&mut ProjectSettings), ) { - cx.update(|cx| { - cx.update_global::(|store, cx| { + _ = cx.update(|cx| { + cx.update_global(|store: &mut SettingsStore, 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); + _ = cx.update(|cx| { + let store = SettingsStore::test(cx); + cx.set_global(store); + theme::init(theme::LoadThemes::JustBase, cx); client::init_settings(cx); language::init(cx); Project::init_settings(cx); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 7b1155890f..2b5c97bac5 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1,50 +1,47 @@ -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}, + display_map::{ + BlockContext, BlockStyle, DisplaySnapshot, FoldStatus, HighlightedChunk, ToDisplayPoint, + 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, + self, 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, + go_to_fetched_definition, go_to_fetched_type_definition, show_link_definition, + update_go_to_definition_link, update_inlay_link_and_hover_points, GoToDefinitionTrigger, + LinkGoToDefinitionState, }, - mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt, + mouse_context_menu, + scroll::scroll_amount::ScrollAmount, + CursorShape, DisplayPoint, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, + HalfPageDown, HalfPageUp, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, SelectPhase, + Selection, SoftWrap, ToPoint, MAX_LINE_LEN, }; +use anyhow::Result; 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, + div, fill, outline, overlay, point, px, quad, relative, size, transparent_black, Action, + AnchorCorner, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Corners, + CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Hsla, InteractiveBounds, + InteractiveElement, IntoElement, ModifiersChangedEvent, MouseButton, MouseDownEvent, + MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, ScrollWheelEvent, ShapedLine, + SharedString, Size, StackingOrder, StatefulInteractiveElement, Style, Styled, TextRun, + TextStyle, View, ViewContext, WindowContext, }; use itertools::Itertools; -use json::json; -use language::{ - language_settings::ShowWhitespaceSetting, Bias, CursorShape, OffsetUtf16, Selection, -}; +use language::language_settings::ShowWhitespaceSetting; +use multi_buffer::Anchor; use project::{ project_settings::{GitGutterSetting, ProjectSettings}, ProjectPath, }; +use settings::Settings; use smallvec::SmallVec; use std::{ + any::TypeId, borrow::Cow, cmp::{self, Ordering}, fmt::Write, @@ -52,12 +49,13 @@ use std::{ ops::Range, sync::Arc, }; -use text::Point; -use theme::SelectionStyle; +use sum_tree::Bias; +use theme::{ActiveTheme, PlayerColor}; +use ui::prelude::*; +use ui::{h_stack, ButtonLike, ButtonStyle, IconButton, Tooltip}; +use util::ResultExt; use workspace::item::Item; -enum FoldMarkers {} - struct SelectionLayout { head: DisplayPoint, cursor_shape: CursorShape, @@ -119,176 +117,288 @@ impl SelectionLayout { } pub struct EditorElement { - style: Arc, + editor: View, + style: EditorStyle, } impl EditorElement { - pub fn new(style: EditorStyle) -> Self { + pub fn new(editor: &View, style: EditorStyle) -> Self { Self { - style: Arc::new(style), + editor: editor.clone(), + style, } } - fn attach_mouse_handlers( - position_map: &Arc, - has_popovers: bool, - visible_bounds: RectF, - text_bounds: RectF, - gutter_bounds: RectF, - bounds: RectF, + fn register_actions(&self, cx: &mut WindowContext) { + let view = &self.editor; + view.update(cx, |editor, cx| { + for action in editor.editor_actions.iter() { + (action)(cx) + } + }); + + crate::rust_analyzer_ext::apply_related_actions(view, cx); + register_action(view, cx, Editor::move_left); + register_action(view, cx, Editor::move_right); + register_action(view, cx, Editor::move_down); + register_action(view, cx, Editor::move_up); + register_action(view, cx, Editor::cancel); + register_action(view, cx, Editor::newline); + register_action(view, cx, Editor::newline_above); + register_action(view, cx, Editor::newline_below); + register_action(view, cx, Editor::backspace); + register_action(view, cx, Editor::delete); + register_action(view, cx, Editor::tab); + register_action(view, cx, Editor::tab_prev); + register_action(view, cx, Editor::indent); + register_action(view, cx, Editor::outdent); + register_action(view, cx, Editor::delete_line); + register_action(view, cx, Editor::join_lines); + register_action(view, cx, Editor::sort_lines_case_sensitive); + register_action(view, cx, Editor::sort_lines_case_insensitive); + register_action(view, cx, Editor::reverse_lines); + register_action(view, cx, Editor::shuffle_lines); + register_action(view, cx, Editor::convert_to_upper_case); + register_action(view, cx, Editor::convert_to_lower_case); + register_action(view, cx, Editor::convert_to_title_case); + register_action(view, cx, Editor::convert_to_snake_case); + register_action(view, cx, Editor::convert_to_kebab_case); + register_action(view, cx, Editor::convert_to_upper_camel_case); + register_action(view, cx, Editor::convert_to_lower_camel_case); + register_action(view, cx, Editor::delete_to_previous_word_start); + register_action(view, cx, Editor::delete_to_previous_subword_start); + register_action(view, cx, Editor::delete_to_next_word_end); + register_action(view, cx, Editor::delete_to_next_subword_end); + register_action(view, cx, Editor::delete_to_beginning_of_line); + register_action(view, cx, Editor::delete_to_end_of_line); + register_action(view, cx, Editor::cut_to_end_of_line); + register_action(view, cx, Editor::duplicate_line); + register_action(view, cx, Editor::move_line_up); + register_action(view, cx, Editor::move_line_down); + register_action(view, cx, Editor::transpose); + register_action(view, cx, Editor::cut); + register_action(view, cx, Editor::copy); + register_action(view, cx, Editor::paste); + register_action(view, cx, Editor::undo); + register_action(view, cx, Editor::redo); + register_action(view, cx, Editor::move_page_up); + register_action(view, cx, Editor::move_page_down); + register_action(view, cx, Editor::next_screen); + register_action(view, cx, Editor::scroll_cursor_top); + register_action(view, cx, Editor::scroll_cursor_center); + register_action(view, cx, Editor::scroll_cursor_bottom); + register_action(view, cx, |editor, _: &LineDown, cx| { + editor.scroll_screen(&ScrollAmount::Line(1.), cx) + }); + register_action(view, cx, |editor, _: &LineUp, cx| { + editor.scroll_screen(&ScrollAmount::Line(-1.), cx) + }); + register_action(view, cx, |editor, _: &HalfPageDown, cx| { + editor.scroll_screen(&ScrollAmount::Page(0.5), cx) + }); + register_action(view, cx, |editor, _: &HalfPageUp, cx| { + editor.scroll_screen(&ScrollAmount::Page(-0.5), cx) + }); + register_action(view, cx, |editor, _: &PageDown, cx| { + editor.scroll_screen(&ScrollAmount::Page(1.), cx) + }); + register_action(view, cx, |editor, _: &PageUp, cx| { + editor.scroll_screen(&ScrollAmount::Page(-1.), cx) + }); + register_action(view, cx, Editor::move_to_previous_word_start); + register_action(view, cx, Editor::move_to_previous_subword_start); + register_action(view, cx, Editor::move_to_next_word_end); + register_action(view, cx, Editor::move_to_next_subword_end); + register_action(view, cx, Editor::move_to_beginning_of_line); + register_action(view, cx, Editor::move_to_end_of_line); + register_action(view, cx, Editor::move_to_start_of_paragraph); + register_action(view, cx, Editor::move_to_end_of_paragraph); + register_action(view, cx, Editor::move_to_beginning); + register_action(view, cx, Editor::move_to_end); + register_action(view, cx, Editor::select_up); + register_action(view, cx, Editor::select_down); + register_action(view, cx, Editor::select_left); + register_action(view, cx, Editor::select_right); + register_action(view, cx, Editor::select_to_previous_word_start); + register_action(view, cx, Editor::select_to_previous_subword_start); + register_action(view, cx, Editor::select_to_next_word_end); + register_action(view, cx, Editor::select_to_next_subword_end); + register_action(view, cx, Editor::select_to_beginning_of_line); + register_action(view, cx, Editor::select_to_end_of_line); + register_action(view, cx, Editor::select_to_start_of_paragraph); + register_action(view, cx, Editor::select_to_end_of_paragraph); + register_action(view, cx, Editor::select_to_beginning); + register_action(view, cx, Editor::select_to_end); + register_action(view, cx, Editor::select_all); + register_action(view, cx, |editor, action, cx| { + editor.select_all_matches(action, cx).log_err(); + }); + register_action(view, cx, Editor::select_line); + register_action(view, cx, Editor::split_selection_into_lines); + register_action(view, cx, Editor::add_selection_above); + register_action(view, cx, Editor::add_selection_below); + register_action(view, cx, |editor, action, cx| { + editor.select_next(action, cx).log_err(); + }); + register_action(view, cx, |editor, action, cx| { + editor.select_previous(action, cx).log_err(); + }); + register_action(view, cx, Editor::toggle_comments); + register_action(view, cx, Editor::select_larger_syntax_node); + register_action(view, cx, Editor::select_smaller_syntax_node); + register_action(view, cx, Editor::move_to_enclosing_bracket); + register_action(view, cx, Editor::undo_selection); + register_action(view, cx, Editor::redo_selection); + register_action(view, cx, Editor::go_to_diagnostic); + register_action(view, cx, Editor::go_to_prev_diagnostic); + register_action(view, cx, Editor::go_to_hunk); + register_action(view, cx, Editor::go_to_prev_hunk); + register_action(view, cx, Editor::go_to_definition); + register_action(view, cx, Editor::go_to_definition_split); + register_action(view, cx, Editor::go_to_type_definition); + register_action(view, cx, Editor::go_to_type_definition_split); + register_action(view, cx, Editor::fold); + register_action(view, cx, Editor::fold_at); + register_action(view, cx, Editor::unfold_lines); + register_action(view, cx, Editor::unfold_at); + register_action(view, cx, Editor::fold_selected_ranges); + register_action(view, cx, Editor::show_completions); + register_action(view, cx, Editor::toggle_code_actions); + register_action(view, cx, Editor::open_excerpts); + register_action(view, cx, Editor::toggle_soft_wrap); + register_action(view, cx, Editor::toggle_inlay_hints); + register_action(view, cx, hover_popover::hover); + register_action(view, cx, Editor::reveal_in_finder); + register_action(view, cx, Editor::copy_path); + register_action(view, cx, Editor::copy_relative_path); + register_action(view, cx, Editor::copy_highlight_json); + register_action(view, cx, |editor, action, cx| { + if let Some(task) = editor.format(action, cx) { + task.detach_and_log_err(cx); + } else { + cx.propagate(); + } + }); + register_action(view, cx, Editor::restart_language_server); + register_action(view, cx, Editor::show_character_palette); + register_action(view, cx, |editor, action, cx| { + if let Some(task) = editor.confirm_completion(action, cx) { + task.detach_and_log_err(cx); + } else { + cx.propagate(); + } + }); + register_action(view, cx, |editor, action, cx| { + if let Some(task) = editor.confirm_code_action(action, cx) { + task.detach_and_log_err(cx); + } else { + cx.propagate(); + } + }); + register_action(view, cx, |editor, action, cx| { + if let Some(task) = editor.rename(action, cx) { + task.detach_and_log_err(cx); + } else { + cx.propagate(); + } + }); + register_action(view, cx, |editor, action, cx| { + if let Some(task) = editor.confirm_rename(action, cx) { + task.detach_and_log_err(cx); + } else { + cx.propagate(); + } + }); + register_action(view, cx, |editor, action, cx| { + if let Some(task) = editor.find_all_references(action, cx) { + task.detach_and_log_err(cx); + } else { + cx.propagate(); + } + }); + register_action(view, cx, Editor::next_copilot_suggestion); + register_action(view, cx, Editor::previous_copilot_suggestion); + register_action(view, cx, Editor::copilot_suggest); + register_action(view, cx, Editor::context_menu_first); + register_action(view, cx, Editor::context_menu_prev); + register_action(view, cx, Editor::context_menu_next); + register_action(view, cx, Editor::context_menu_last); + } + + fn register_key_listeners(&self, cx: &mut WindowContext) { + cx.on_key_event({ + let editor = self.editor.clone(); + move |event: &ModifiersChangedEvent, phase, cx| { + if phase != DispatchPhase::Bubble { + return; + } + + if editor.update(cx, |editor, cx| Self::modifiers_changed(editor, event, cx)) { + cx.stop_propagation(); + } + } + }); + } + + pub(crate) fn modifiers_changed( + editor: &mut Editor, + event: &ModifiersChangedEvent, + cx: &mut ViewContext, + ) -> bool { + let pending_selection = editor.has_pending_selection(); + + if let Some(point) = &editor.link_go_to_definition_state.last_trigger_point { + if event.command && !pending_selection { + let point = point.clone(); + let snapshot = editor.snapshot(cx); + let kind = point.definition_kind(event.shift); + + show_link_definition(kind, editor, point, snapshot, cx); + return false; + } + } + + { + 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); + } + + false + } + + fn mouse_left_down( + editor: &mut Editor, + event: &MouseDownEvent, + position_map: &PositionMap, + text_bounds: Bounds, + gutter_bounds: Bounds, + stacking_order: &StackingOrder, 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; - } + let mut click_count = event.click_count; + let modifiers = event.modifiers; - 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) { + if gutter_bounds.contains(&event.position) { click_count = 3; // Simulate triple-click when clicking the gutter to select lines - } else if !text_bounds.contains_point(position) { - return false; + } else if !text_bounds.contains(&event.position) { + return; + } + if !cx.was_top_layer(&event.position, stacking_order) { + return; } - let point_for_position = position_map.point_for_position(text_bounds, position); + let point_for_position = position_map.point_for_position(text_bounds, event.position); let position = point_for_position.previous_valid; - if shift && alt { + if modifiers.shift && modifiers.alt { editor.select( SelectPhase::BeginColumnar { position, @@ -296,7 +406,7 @@ impl EditorElement { }, cx, ); - } else if shift && !ctrl && !alt && !cmd { + } else if modifiers.shift && !modifiers.control && !modifiers.alt && !modifiers.command { editor.select( SelectPhase::Extend { position, @@ -308,46 +418,44 @@ impl EditorElement { editor.select( SelectPhase::Begin { position, - add: alt, + add: modifiers.alt, click_count, }, cx, ); } - true + cx.stop_propagation(); } fn mouse_right_down( editor: &mut Editor, - position: Vector2F, + event: &MouseDownEvent, position_map: &PositionMap, - text_bounds: RectF, - cx: &mut EventContext, - ) -> bool { - if !text_bounds.contains_point(position) { - return false; + text_bounds: Bounds, + cx: &mut ViewContext, + ) { + if !text_bounds.contains(&event.position) { + return; } - let point_for_position = position_map.point_for_position(text_bounds, position); + let point_for_position = position_map.point_for_position(text_bounds, event.position); mouse_context_menu::deploy_context_menu( editor, - position, + event.position, point_for_position.previous_valid, cx, ); - true + cx.stop_propagation(); } fn mouse_up( editor: &mut Editor, - position: Vector2F, - cmd: bool, - shift: bool, - alt: bool, + event: &MouseUpEvent, position_map: &PositionMap, - text_bounds: RectF, - cx: &mut EventContext, - ) -> bool { + text_bounds: Bounds, + stacking_order: &StackingOrder, + cx: &mut ViewContext, + ) { let end_selection = editor.has_pending_selection(); let pending_nonempty_selections = editor.has_pending_nonempty_selection(); @@ -355,118 +463,99 @@ impl EditorElement { 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); + if !pending_nonempty_selections + && event.modifiers.command + && text_bounds.contains(&event.position) + && cx.was_top_layer(&event.position, stacking_order) + { + let point = position_map.point_for_position(text_bounds, event.position); let could_be_inlay = point.as_valid().is_none(); - if shift || could_be_inlay { - go_to_fetched_type_definition(editor, point, alt, cx); + let split = event.modifiers.alt; + if event.modifiers.shift || could_be_inlay { + go_to_fetched_type_definition(editor, point, split, cx); } else { - go_to_fetched_definition(editor, point, alt, cx); + go_to_fetched_definition(editor, point, split, cx); } - return true; + cx.stop_propagation(); + } else if end_selection { + cx.stop_propagation(); } - - end_selection } fn mouse_dragged( editor: &mut Editor, - MouseMovedEvent { - modifiers: Modifiers { cmd, shift, .. }, - position, - .. - }: MouseMovedEvent, + event: &MouseMoveEvent, 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 - }; + text_bounds: Bounds, + _gutter_bounds: Bounds, + _stacking_order: &StackingOrder, + cx: &mut ViewContext, + ) { + if !editor.has_pending_selection() { + return; + } - update_go_to_definition_link( - editor, - point.map(GoToDefinitionTrigger::Text), - cmd, - shift, + let point_for_position = position_map.point_for_position(text_bounds, event.position); + let mut scroll_delta = gpui::Point::::default(); + let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0); + let top = text_bounds.origin.y + vertical_margin; + let bottom = text_bounds.lower_left().y - vertical_margin; + if event.position.y < top { + scroll_delta.y = -scale_vertical_mouse_autoscroll_delta(top - event.position.y); + } + if event.position.y > bottom { + scroll_delta.y = scale_vertical_mouse_autoscroll_delta(event.position.y - bottom); + } + + let horizontal_margin = position_map.line_height.min(text_bounds.size.width / 3.0); + let left = text_bounds.origin.x + horizontal_margin; + let right = text_bounds.upper_right().x - horizontal_margin; + if event.position.x < left { + scroll_delta.x = -scale_horizontal_mouse_autoscroll_delta(left - event.position.x); + } + if event.position.x > right { + scroll_delta.x = scale_horizontal_mouse_autoscroll_delta(event.position.x - right); + } + + 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::default(), &position_map.scroll_max), + }, 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, + event: &MouseMoveEvent, position_map: &PositionMap, - text_bounds: RectF, + text_bounds: Bounds, + gutter_bounds: Bounds, + stacking_order: &StackingOrder, cx: &mut ViewContext, - ) -> bool { - // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed + ) { + let modifiers = event.modifiers; + let text_hovered = text_bounds.contains(&event.position); + let gutter_hovered = gutter_bounds.contains(&event.position); + let was_top = cx.was_top_layer(&event.position, stacking_order); + + editor.set_gutter_hovered(gutter_hovered, cx); + // 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); + if text_hovered && was_top { + let point_for_position = position_map.point_for_position(text_bounds, event.position); + match point_for_position.as_valid() { Some(point) => { update_go_to_definition_link( editor, Some(GoToDefinitionTrigger::Text(point)), - cmd, - shift, + modifiers.command, + modifiers.shift, cx, ); hover_at(editor, Some(point), cx); @@ -476,76 +565,69 @@ impl EditorElement { &position_map.snapshot, point_for_position, editor, - cmd, - shift, + modifiers.command, + modifiers.shift, cx, ); } } } else { - update_go_to_definition_link(editor, None, cmd, shift, cx); + update_go_to_definition_link(editor, None, modifiers.command, modifiers.shift, cx); hover_at(editor, None, cx); + if gutter_hovered && was_top { + cx.stop_propagation(); + } } - - true } fn scroll( editor: &mut Editor, - position: Vector2F, - mut delta: Vector2F, - precise: bool, + event: &ScrollWheelEvent, position_map: &PositionMap, - bounds: RectF, + bounds: &InteractiveBounds, cx: &mut ViewContext, - ) -> bool { - if !bounds.contains_point(position) { - return false; + ) { + if !bounds.visibly_contains(&event.position, cx) { + return; } let line_height = position_map.line_height; let max_glyph_width = position_map.em_width; + let (delta, axis) = match event.delta { + gpui::ScrollDelta::Pixels(mut pixels) => { + //Trackpad + let axis = position_map.snapshot.ongoing_scroll.filter(&mut pixels); + (pixels, axis) + } - 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 + gpui::ScrollDelta::Lines(lines) => { + //Not trackpad + let pixels = point(lines.x * max_glyph_width, lines.y * line_height); + (pixels, None) + } }; 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); + let x = f32::from((scroll_position.x * max_glyph_width - delta.x) / max_glyph_width); + let y = f32::from((scroll_position.y * line_height - delta.y) / line_height); + let scroll_position = point(x, y).clamp(&point(0., 0.), &position_map.scroll_max); editor.scroll(scroll_position, axis, cx); - - true + cx.stop_propagation(); } fn paint_background( &self, - gutter_bounds: RectF, - text_bounds: RectF, + gutter_bounds: Bounds, + text_bounds: Bounds, layout: &LayoutState, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { - let bounds = gutter_bounds.union_rect(text_bounds); + let bounds = gutter_bounds.union(&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(), - }); + layout.position_map.snapshot.scroll_position().y * layout.position_map.line_height; + let gutter_bg = cx.theme().colors().editor_gutter_background; + cx.paint_quad(fill(gutter_bounds, gutter_bg)); + cx.paint_quad(fill(text_bounds, self.style.background)); if let EditorMode::Full = layout.mode { let mut active_rows = layout.active_rows.iter().peekable(); @@ -559,90 +641,77 @@ impl EditorElement { } if !contains_non_empty_selection { - let origin = vec2f( - bounds.origin_x(), - bounds.origin_y() + (layout.position_map.line_height * *start_row as f32) + let origin = point( + bounds.origin.x, + bounds.origin.y + (layout.position_map.line_height * *start_row as f32) - scroll_top, ); - let size = vec2f( - bounds.width(), + let size = size( + bounds.size.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(), - }); + let active_line_bg = cx.theme().colors().editor_active_line_background; + cx.paint_quad(fill(Bounds { origin, size }, active_line_bg)); } } if let Some(highlighted_rows) = &layout.highlighted_rows { - let origin = vec2f( - bounds.origin_x(), - bounds.origin_y() + let origin = point( + bounds.origin.x, + bounds.origin.y + (layout.position_map.line_height * highlighted_rows.start as f32) - scroll_top, ); - let size = vec2f( - bounds.width(), + let size = size( + bounds.size.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 highlighted_line_bg = cx.theme().colors().editor_highlighted_line_background; + cx.paint_quad(fill(Bounds { origin, size }, highlighted_line_bg)); } let scroll_left = - layout.position_map.snapshot.scroll_position().x() * layout.position_map.em_width; + 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; + let x = (text_bounds.origin.x + *wrap_position + layout.position_map.em_width / 2.) + - scroll_left; - if x < text_bounds.origin_x() + if x < text_bounds.origin.x || (layout.show_scrollbars && x > self.scrollbar_left(&bounds)) { continue; } let color = if *active { - self.style.active_wrap_guide + cx.theme().colors().editor_active_wrap_guide } else { - self.style.wrap_guide + cx.theme().colors().editor_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(), - }); + cx.paint_quad(fill( + Bounds { + origin: point(x, text_bounds.origin.y), + size: size(px(1.), text_bounds.size.height), + }, + color, + )); } } } fn paint_gutter( &mut self, - bounds: RectF, - visible_bounds: RectF, + bounds: Bounds, layout: &mut LayoutState, - editor: &mut Editor, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { 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 scroll_top = scroll_position.y * line_height; let show_gutter = matches!( - settings::get::(cx).git.git_gutter, + ProjectSettings::get_global(cx).git.git_gutter, Some(GitGutterSetting::TrackedFiles) ); @@ -650,50 +719,66 @@ impl EditorElement { Self::paint_diff_hunks(bounds, layout, cx); } - for (ix, line) in layout.line_number_layouts.iter().enumerate() { + for (ix, line) in layout.line_numbers.iter().enumerate() { if let Some(line) = line { - let line_origin = bounds.origin() - + vec2f( - bounds.width() - line.width() - layout.gutter_padding, + let line_origin = bounds.origin + + point( + bounds.size.width - line.width - layout.gutter_padding, ix as f32 * line_height - (scroll_top % line_height), ); - line.paint(line_origin, visible_bounds, line_height, cx); + line.paint(line_origin, line_height, cx).log_err(); } } - 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., - ); + cx.with_z_index(1, |cx| { + for (ix, fold_indicator) in layout.fold_indicators.drain(..).enumerate() { + if let Some(fold_indicator) = fold_indicator { + let mut fold_indicator = fold_indicator.into_any_element(); + let available_space = size( + AvailableSpace::MinContent, + AvailableSpace::Definite(line_height * 0.55), + ); + let fold_indicator_size = fold_indicator.measure(available_space, cx); - let indicator_origin = bounds.origin() + position + centering_offset; - - indicator.paint(indicator_origin, visible_bounds, editor, cx); + let position = point( + bounds.size.width - layout.gutter_padding, + ix as f32 * line_height - (scroll_top % line_height), + ); + let centering_offset = point( + (layout.gutter_padding + layout.gutter_margin - fold_indicator_size.width) + / 2., + (line_height - fold_indicator_size.height) / 2., + ); + let origin = bounds.origin + position + centering_offset; + fold_indicator.draw(origin, available_space, 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); - } + if let Some(indicator) = layout.code_actions_indicator.take() { + let mut button = indicator.button.into_any_element(); + let available_space = size( + AvailableSpace::MinContent, + AvailableSpace::Definite(line_height), + ); + let indicator_size = button.measure(available_space, cx); + + let mut x = Pixels::ZERO; + let mut y = indicator.row as f32 * line_height - scroll_top; + // Center indicator. + x += ((layout.gutter_padding + layout.gutter_margin) - indicator_size.width) / 2.; + y += (line_height - indicator_size.height) / 2.; + + button.draw(bounds.origin + point(x, y), available_space, cx); + } + }); } - fn paint_diff_hunks(bounds: RectF, layout: &mut LayoutState, cx: &mut ViewContext) { - let diff_style = &theme::current(cx).editor.diff.clone(); + fn paint_diff_hunks(bounds: Bounds, layout: &LayoutState, cx: &mut WindowContext) { 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 scroll_top = scroll_position.y * line_height; for hunk in &layout.display_hunks { let (display_row_range, status) = match hunk { @@ -702,17 +787,17 @@ impl EditorElement { 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(), - }); + let width = 0.275 * line_height; + let highlight_origin = bounds.origin + point(-width, start_y); + let highlight_size = size(width * 2., end_y - start_y); + let highlight_bounds = Bounds::new(highlight_origin, highlight_size); + cx.paint_quad(quad( + highlight_bounds, + Corners::all(1. * line_height), + gpui::yellow(), // todo!("use the right color") + Edges::default(), + transparent_black(), + )); continue; } @@ -724,8 +809,8 @@ impl EditorElement { }; let color = match status { - DiffHunkStatus::Added => diff_style.inserted, - DiffHunkStatus::Modified => diff_style.modified, + DiffHunkStatus::Added => cx.theme().status().created, + DiffHunkStatus::Modified => cx.theme().status().modified, //TODO: This rendering is entirely a horrible hack DiffHunkStatus::Removed => { @@ -735,17 +820,17 @@ impl EditorElement { 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(), - }); + let width = 0.275 * line_height; + let highlight_origin = bounds.origin + point(-width, start_y); + let highlight_size = size(width * 2., end_y - start_y); + let highlight_bounds = Bounds::new(highlight_origin, highlight_size); + cx.paint_quad(quad( + highlight_bounds, + Corners::all(1. * line_height), + cx.theme().status().deleted, + Edges::default(), + transparent_black(), + )); continue; } @@ -757,257 +842,307 @@ impl EditorElement { 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(), - }); + let width = 0.275 * line_height; + let highlight_origin = bounds.origin + point(-width, start_y); + let highlight_size = size(width * 2., end_y - start_y); + let highlight_bounds = Bounds::new(highlight_origin, highlight_size); + cx.paint_quad(quad( + highlight_bounds, + Corners::all(0.05 * line_height), + color, // todo!("use the right color") + Edges::default(), + transparent_black(), + )); } } fn paint_text( &mut self, - bounds: RectF, - visible_bounds: RectF, + text_bounds: Bounds, layout: &mut LayoutState, - editor: &mut Editor, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { - 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 content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO); let line_end_overshoot = 0.15 * layout.position_map.line_height; - let whitespace_setting = editor.buffer.read(cx).settings_at(0, cx).show_whitespaces; + let whitespace_setting = self + .editor + .read(cx) + .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()) + cx.with_content_mask( + Some(ContentMask { + bounds: text_bounds, + }), + |cx| { + let interactive_text_bounds = InteractiveBounds { + bounds: text_bounds, + stacking_order: cx.stacking_order().clone(), + }; + if interactive_text_bounds.visibly_contains(&cx.mouse_position(), cx) { + if self + .editor + .read(cx) + .link_go_to_definition_state + .definitions + .is_empty() { - 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, - }); + cx.set_cursor_style(CursorStyle::IBeam); + } else { + cx.set_cursor_style(CursorStyle::PointingHand); } } - } - } - 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, - ) - } - } + let fold_corner_radius = 0.15 * layout.position_map.line_height; + cx.with_element_id(Some("folds"), |cx| { + let snapshot = &layout.position_map.snapshot; + for fold in snapshot.folds_in_range(layout.visible_anchor_range.clone()) { + let fold_range = fold.range.clone(); + let display_range = fold.range.start.to_display_point(&snapshot) + ..fold.range.end.to_display_point(&snapshot); + debug_assert_eq!(display_range.start.row(), display_range.end.row()); + let row = display_range.start.row(); - cx.scene().push_layer(Some(bounds)); - for cursor in cursors { - cursor.paint(content_origin, cx); - } - cx.scene().pop_layer(); + let line_layout = &layout.position_map.line_layouts + [(row - layout.visible_display_row_range.start) as usize] + .line; + let start_x = content_origin.x + + line_layout.x_for_index(display_range.start.column() as usize) + - layout.position_map.scroll_position.x; + let start_y = content_origin.y + + row as f32 * layout.position_map.line_height + - layout.position_map.scroll_position.y; + let end_x = content_origin.x + + line_layout.x_for_index(display_range.end.column() as usize) + - layout.position_map.scroll_position.x; + + let fold_bounds = Bounds { + origin: point(start_x, start_y), + size: size(end_x - start_x, layout.position_map.line_height), + }; + + let fold_background = cx.with_z_index(1, |cx| { + div() + .id(fold.id) + .size_full() + .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) + .on_click(cx.listener_for( + &self.editor, + move |editor: &mut Editor, _, cx| { + editor.unfold_ranges( + [fold_range.start..fold_range.end], + true, + false, + cx, + ); + cx.stop_propagation(); + }, + )) + .draw_and_update_state( + fold_bounds.origin, + fold_bounds.size, + cx, + |fold_element_state, cx| { + if fold_element_state.is_active() { + cx.theme().colors().ghost_element_active + } else if fold_bounds.contains(&cx.mouse_position()) { + cx.theme().colors().ghost_element_hover + } else { + cx.theme().colors().ghost_element_background + } + }, + ) + }); + + self.paint_highlighted_range( + display_range.clone(), + fold_background, + fold_corner_radius, + fold_corner_radius * 2., + layout, + content_origin, + text_bounds, + cx, + ); + } + }); + + for (range, color) in &layout.highlighted_ranges { + self.paint_highlighted_range( + range.clone(), + *color, + Pixels::ZERO, + line_end_overshoot, + layout, + content_origin, + text_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, + text_bounds, + cx, + ); + + if selection.is_local && !selection.range.is_empty() { + invisible_display_ranges.push(selection.range.clone()); + } + + if !selection.is_local || self.editor.read(cx).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 == Pixels::ZERO { + 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, _)| { + // todo!() currently shape_line panics if text conatins newlines + let text = if character == '\n' { + SharedString::from(" ") + } else { + SharedString::from(character.to_string()) + }; + let len = text.len(); + cx.text_system() + .shape_line( + text, + cursor_row_layout.font_size, + &[TextRun { + len, + font: self.style.text.font(), + color: self.style.background, + background_color: None, + underline: None, + }], + ) + .log_err() + }) + } else { + None + }; + + let x = cursor_character_x - layout.position_map.scroll_position.x; + let y = cursor_position.row() as f32 + * layout.position_map.line_height + - layout.position_map.scroll_position.y; + if selection.is_newest { + self.editor.update(cx, |editor, _| { + editor.pixel_position_of_newest_cursor = Some(point( + text_bounds.origin.x + x + block_width / 2., + text_bounds.origin.y + + y + + layout.position_map.line_height / 2., + )) + }); + } + cursors.push(Cursor { + color: selection_style.cursor, + block_width, + origin: point(x, y), + line_height: layout.position_map.line_height, + shape: selection.cursor_shape, + block_text, + }); + } + } + } + } + + 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, + content_origin, + whitespace_setting, + &invisible_display_ranges, + cx, + ) + } + + cx.with_z_index(0, |cx| { + for cursor in cursors { + cursor.paint(content_origin, cx); + } + }); + }, + ) + } + + fn paint_overlays( + &mut self, + text_bounds: Bounds, + layout: &mut LayoutState, + cx: &mut WindowContext, + ) { + let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO); + let start_row = layout.visible_display_row_range.start; + if let Some((position, mut context_menu)) = layout.context_menu.take() { + let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); + let context_menu_size = context_menu.measure(available_space, cx); - 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(); + let x = cursor_row_layout.x_for_index(position.column() as usize) + - layout.position_map.scroll_position.x; + let y = (position.row() + 1) as f32 * layout.position_map.line_height + - layout.position_map.scroll_position.y; + let mut list_origin = content_origin + point(x, y); + let list_width = context_menu_size.width; + let list_height = context_menu_size.height; // 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.x + list_width > cx.viewport_size().width { + list_origin.x = (cx.viewport_size().width - list_width).max(Pixels::ZERO); } - if list_origin.y() + list_height > bounds.max_y() { - list_origin.set_y(list_origin.y() - layout.position_map.line_height - list_height); + if list_origin.y + list_height > text_bounds.lower_right().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(); + cx.break_content_mask(|cx| context_menu.draw(list_origin, available_space, cx)); } - if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() { - cx.scene().push_stacking_context(None, None); + if let Some((position, mut hover_popovers)) = layout.hover_popovers.take() { + let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); // This is safe because we check on layout whether the required row is available let hovered_row_layout = @@ -1016,163 +1151,158 @@ impl EditorElement { // 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; + let first_size = hover_popovers[0].measure(available_space, cx); + let height_to_reserve = + first_size.height + 1.5 * MIN_POPOVER_LINE_HEIGHT * 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); + let x = hovered_row_layout.x_for_index(position.column() as usize) + - layout.position_map.scroll_position.x; + let y = position.row() as f32 * layout.position_map.line_height + - layout.position_map.scroll_position.y; + let hovered_point = content_origin + point(x, y); - if hovered_point.y() - height_to_reserve > 0.0 { + if hovered_point.y - height_to_reserve > Pixels::ZERO { // 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 mut current_y = hovered_point.y; + for mut hover_popover in hover_popovers { + let size = hover_popover.measure(available_space, cx); + let mut popover_origin = point(hovered_point.x, current_y - size.height); - 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); + let x_out_of_bounds = + text_bounds.upper_right().x - (popover_origin.x + size.width); + if x_out_of_bounds < Pixels::ZERO { + popover_origin.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, - ); + cx.break_content_mask(|cx| { + hover_popover.draw(popover_origin, available_space, cx) + }); - current_y = popover_origin.y() - HOVER_POPOVER_GAP; + 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 mut current_y = hovered_point.y + layout.position_map.line_height; + for mut hover_popover in hover_popovers { + let size = hover_popover.measure(available_space, cx); + let mut popover_origin = point(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); + let x_out_of_bounds = + text_bounds.upper_right().x - (popover_origin.x + size.width); + if x_out_of_bounds < Pixels::ZERO { + popover_origin.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, - ); + hover_popover.draw(popover_origin, available_space, cx); - current_y = popover_origin.y() + size.y() + HOVER_POPOVER_GAP; + current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP; } } - - cx.scene().pop_stacking_context(); } - cx.scene().pop_layer(); + if let Some(mouse_context_menu) = self.editor.read(cx).mouse_context_menu.as_ref() { + let element = overlay() + .position(mouse_context_menu.position) + .child(mouse_context_menu.context_menu.clone()) + .anchor(AnchorCorner::TopLeft) + .snap_to_window(); + element.into_any().draw( + gpui::Point::default(), + size(AvailableSpace::MinContent, AvailableSpace::MinContent), + cx, + ); + } } - fn scrollbar_left(&self, bounds: &RectF) -> f32 { - bounds.max_x() - self.style.theme.scrollbar.width + fn scrollbar_left(&self, bounds: &Bounds) -> Pixels { + bounds.upper_right().x - self.style.scrollbar_width } fn paint_scrollbar( &mut self, - bounds: RectF, + bounds: Bounds, layout: &mut LayoutState, - editor: &Editor, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { - 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 top = bounds.origin.y; + let bottom = bounds.lower_left().y; + let right = bounds.lower_right().x; let left = self.scrollbar_left(&bounds); - let row_range = &layout.scrollbar_row_range; + let row_range = layout.scrollbar_row_range.clone(); 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; + let mut height = bounds.size.height; + let mut first_row_y_offset = px(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 min_thumb_height = layout.position_map.line_height; 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 y_for_row = |row: f32| -> Pixels { 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)); + let track_bounds = Bounds::from_corners(point(left, top), point(right, bottom)); + let thumb_bounds = Bounds::from_corners(point(left, thumb_top), point(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; + cx.paint_quad(quad( + track_bounds, + Corners::default(), + cx.theme().colors().scrollbar_track_background, + Edges { + top: Pixels::ZERO, + right: Pixels::ZERO, + bottom: Pixels::ZERO, + left: px(1.), + }, + cx.theme().colors().scrollbar_track_border, + )); + let scrollbar_settings = EditorSettings::get_global(cx).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 + let background_ranges = self + .editor + .read(cx) .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); + for range in background_ranges { + let start_y = y_for_row(range.start().row() as f32); + let mut end_y = y_for_row(range.end().row() as f32); + if end_y - start_y < px(1.) { + end_y = start_y + px(1.); + } + let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y)); + cx.paint_quad(quad( + bounds, + Corners::default(), + cx.theme().status().info, + Edges { + top: Pixels::ZERO, + right: px(1.), + bottom: Pixels::ZERO, + left: px(1.), + }, + cx.theme().colors().scrollbar_thumb_border, + )); } } if layout.is_singleton && scrollbar_settings.git_diff { - let diff_style = scrollbar_theme.git.clone(); for hunk in layout .position_map .snapshot @@ -1190,106 +1320,146 @@ impl EditorElement { y_for_row((end_display.row()) as f32) }; - if end_y - start_y < 1. { - end_y = start_y + 1.; + if end_y - start_y < px(1.) { + end_y = start_y + px(1.); } - let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y)); + let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y)); let color = match hunk.status() { - DiffHunkStatus::Added => diff_style.inserted, - DiffHunkStatus::Modified => diff_style.modified, - DiffHunkStatus::Removed => diff_style.deleted, + DiffHunkStatus::Added => cx.theme().status().created, + DiffHunkStatus::Modified => cx.theme().status().modified, + DiffHunkStatus::Removed => cx.theme().status().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 { + cx.paint_quad(quad( bounds, - background: Some(color), - border: border.into(), - corner_radii: style.thumb.corner_radii.into(), - }) + Corners::default(), + color, + Edges { + top: Pixels::ZERO, + right: px(1.), + bottom: Pixels::ZERO, + left: px(1.), + }, + cx.theme().colors().scrollbar_thumb_border, + )); } } - 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.paint_quad(quad( + thumb_bounds, + Corners::default(), + cx.theme().colors().scrollbar_thumb_background, + Edges { + top: Pixels::ZERO, + right: px(1.), + bottom: Pixels::ZERO, + left: px(1.), + }, + cx.theme().colors().scrollbar_thumb_border, + )); } - cx.scene().push_cursor_region(CursorRegion { + let interactive_track_bounds = InteractiveBounds { 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); + stacking_order: cx.stacking_order().clone(), + }; + let mut mouse_position = cx.mouse_position(); + if interactive_track_bounds.visibly_contains(&mouse_position, cx) { + cx.set_cursor_style(CursorStyle::Arrow); + } + + cx.on_mouse_event({ + let editor = self.editor.clone(); + move |event: &MouseMoveEvent, phase, cx| { + if phase == DispatchPhase::Capture { + return; + } + + editor.update(cx, |editor, cx| { + if event.pressed_button == Some(MouseButton::Left) + && editor.scroll_manager.is_dragging_scrollbar() + { + let y = mouse_position.y; + let new_y = event.position.y; + if (track_bounds.top()..track_bounds.bottom()).contains(&y) { let mut position = editor.scroll_position(cx); - position.set_y(top_row as f32); + position.y += (new_y - y) * (max_row as f32) / height; + if position.y < 0.0 { + position.y = 0.0; + } editor.set_scroll_position(position, cx); - } else { + } + + mouse_position = event.position; + cx.stop_propagation(); + } else { + editor.scroll_manager.set_is_dragging_scrollbar(false, cx); + if interactive_track_bounds.visibly_contains(&event.position, cx) { 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); - } + if self.editor.read(cx).scroll_manager.is_dragging_scrollbar() { + cx.on_mouse_event({ + let editor = self.editor.clone(); + move |_: &MouseUpEvent, phase, cx| { + if phase == DispatchPhase::Capture { + return; } - }), - ); + + editor.update(cx, |editor, cx| { + editor.scroll_manager.set_is_dragging_scrollbar(false, cx); + cx.stop_propagation(); + }); + } + }); + } else { + cx.on_mouse_event({ + let editor = self.editor.clone(); + move |event: &MouseDownEvent, phase, cx| { + if phase == DispatchPhase::Capture { + return; + } + + editor.update(cx, |editor, cx| { + if track_bounds.contains(&event.position) { + editor.scroll_manager.set_is_dragging_scrollbar(true, 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.y = top_row as f32; + editor.set_scroll_position(position, cx); + } else { + editor.scroll_manager.show_scrollbar(cx); + } + + cx.stop_propagation(); + } + }); + } + }); + } } #[allow(clippy::too_many_arguments)] fn paint_highlighted_range( &self, range: Range, - color: Color, - corner_radius: f32, - line_end_overshoot: f32, + color: Hsla, + corner_radius: Pixels, + line_end_overshoot: Pixels, layout: &LayoutState, - content_origin: Vector2F, - scroll_top: f32, - scroll_left: f32, - bounds: RectF, - cx: &mut ViewContext, + content_origin: gpui::Point, + bounds: Bounds, + cx: &mut WindowContext, ) { let start_row = layout.visible_display_row_range.start; let end_row = layout.visible_display_row_range.end; @@ -1304,9 +1474,9 @@ impl EditorElement { color, line_height: layout.position_map.line_height, corner_radius, - start_y: content_origin.y() + start_y: content_origin.y + row_range.start as f32 * layout.position_map.line_height - - scroll_top, + - layout.position_map.scroll_position.y, lines: row_range .into_iter() .map(|row| { @@ -1314,19 +1484,19 @@ impl EditorElement { &layout.position_map.line_layouts[(row - start_row) as usize].line; HighlightedRangeLine { start_x: if row == range.start.row() { - content_origin.x() + content_origin.x + line_layout.x_for_index(range.start.column() as usize) - - scroll_left + - layout.position_map.scroll_position.x } else { - content_origin.x() - scroll_left + content_origin.x - layout.position_map.scroll_position.x }, end_x: if row == range.end.row() { - content_origin.x() + content_origin.x + line_layout.x_for_index(range.end.column() as usize) - - scroll_left + - layout.position_map.scroll_position.x } else { - content_origin.x() + line_layout.width() + line_end_overshoot - - scroll_left + content_origin.x + line_layout.width + line_end_overshoot + - layout.position_map.scroll_position.x }, } }) @@ -1339,49 +1509,49 @@ impl EditorElement { fn paint_blocks( &mut self, - bounds: RectF, - visible_bounds: RectF, + bounds: Bounds, layout: &mut LayoutState, - editor: &mut Editor, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { 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; + 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., + for mut block in layout.blocks.drain(..) { + let mut origin = bounds.origin + + point( + Pixels::ZERO, block.row as f32 * layout.position_map.line_height - scroll_top, ); if !matches!(block.style, BlockStyle::Sticky) { - origin += vec2f(-scroll_left, 0.); + origin += point(-scroll_left, Pixels::ZERO); } - block.element.paint(origin, visible_bounds, editor, cx); + block.element.draw(origin, block.available_space, cx); } } - fn column_pixels(&self, column: usize, cx: &ViewContext) -> f32 { + fn column_pixels(&self, column: usize, cx: &WindowContext) -> Pixels { 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(), - }, - )], + let font_size = style.text.font_size.to_pixels(cx.rem_size()); + let layout = cx + .text_system() + .shape_line( + SharedString::from(" ".repeat(column)), + font_size, + &[TextRun { + len: column, + font: style.text.font(), + color: Hsla::default(), + background_color: None, + underline: None, + }], ) - .width() + .unwrap(); + + layout.width } - fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext) -> f32 { + fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &WindowContext) -> Pixels { let digit_count = (snapshot.max_buffer_row() as f32 + 1.).log10().floor() as usize + 1; self.column_pixels(digit_count, cx) } @@ -1459,7 +1629,7 @@ impl EditorElement { relative_rows } - fn layout_line_numbers( + fn shape_line_numbers( &self, rows: Range, active_rows: &BTreeMap, @@ -1468,15 +1638,15 @@ impl EditorElement { snapshot: &EditorSnapshot, cx: &ViewContext, ) -> ( - Vec>, + Vec>, Vec>, ) { - let style = &self.style; + let font_size = self.style.text.font_size.to_pixels(cx.rem_size()); let include_line_numbers = snapshot.mode == EditorMode::Full; - let mut line_number_layouts = Vec::with_capacity(rows.len()); + let mut shaped_line_numbers = 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 is_relative = EditorSettings::get_global(cx).relative_line_numbers; let relative_to = if is_relative { Some(newest_selection_head.row()) } else { @@ -1492,9 +1662,9 @@ impl EditorElement { { let display_row = rows.start + ix as u32; let (active, color) = if active_rows.contains_key(&display_row) { - (true, style.line_number_active) + (true, cx.theme().colors().editor_active_line_number) } else { - (false, style.line_number) + (false, cx.theme().colors().editor_line_number) }; if let Some(buffer_row) = row { if include_line_numbers { @@ -1504,18 +1674,18 @@ impl EditorElement { .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(), - }, - )], - ))); + let run = TextRun { + len: line_number.len(), + font: self.style.text.font(), + color, + background_color: None, + underline: None, + }; + let shaped_line = cx + .text_system() + .shape_line(line_number.clone().into(), font_size, &[run]) + .unwrap(); + shaped_line_numbers.push(Some(shaped_line)); fold_statuses.push( is_singleton .then(|| { @@ -1528,17 +1698,17 @@ impl EditorElement { } } else { fold_statuses.push(None); - line_number_layouts.push(None); + shaped_line_numbers.push(None); } } - (line_number_layouts, fold_statuses) + (shaped_line_numbers, fold_statuses) } fn layout_lines( - &mut self, + &self, rows: Range, - line_number_layouts: &[Option], + line_number_layouts: &[Option], snapshot: &EditorSnapshot, cx: &ViewContext, ) -> Vec { @@ -1546,14 +1716,12 @@ impl EditorElement { return Vec::new(); } - // When the editor is empty and unfocused, then show the placeholder. + // Show the placeholder when the editor is empty if snapshot.is_empty() { - let placeholder_style = self - .style - .placeholder_text - .as_ref() - .unwrap_or(&self.style.text); + let font_size = self.style.text.font_size.to_pixels(cx.rem_size()); + let placeholder_color = cx.theme().colors().text_placeholder; let placeholder_text = snapshot.placeholder_text(); + let placeholder_lines = placeholder_text .as_ref() .map_or("", AsRef::as_ref) @@ -1562,19 +1730,17 @@ impl EditorElement { .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(), - }, - )], - ) + .filter_map(move |line| { + let run = TextRun { + len: line.len(), + font: self.style.text.font(), + color: placeholder_color, + background_color: None, + underline: Default::default(), + }; + cx.text_system() + .shape_line(line.to_string().into(), font_size, &[run]) + .log_err() }) .map(|line| LineWithInvisibles { line, @@ -1582,48 +1748,471 @@ impl EditorElement { }) .collect() } else { - let style = &self.style; - let chunks = snapshot.highlighted_chunks(rows.clone(), true, style); - + let chunks = snapshot.highlighted_chunks(rows.clone(), true, &self.style); LineWithInvisibles::from_chunks( chunks, - &style.text, - cx.text_layout_cache(), - cx.font_cache(), + &self.style.text, MAX_LINE_LEN, rows.len() as usize, line_number_layouts, snapshot.mode, + cx, ) } } + fn compute_layout(&mut self, bounds: Bounds, cx: &mut WindowContext) -> LayoutState { + self.editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + let style = self.style.clone(); + + let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); + let font_size = style.text.font_size.to_pixels(cx.rem_size()); + let line_height = style.text.line_height_in_pixels(cx.rem_size()); + let em_width = cx + .text_system() + .typographic_bounds(font_id, font_size, 'm') + .unwrap() + .size + .width; + let em_advance = cx + .text_system() + .advance(font_id, font_size, 'm') + .unwrap() + .width; + + let gutter_padding; + let gutter_width; + let gutter_margin; + if snapshot.show_gutter { + let descent = cx.text_system().descent(font_id, font_size); + + let gutter_padding_factor = 3.5; + gutter_padding = (em_width * gutter_padding_factor).round(); + gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0; + gutter_margin = -descent; + } else { + gutter_padding = Pixels::ZERO; + gutter_width = Pixels::ZERO; + gutter_margin = Pixels::ZERO; + }; + + editor.gutter_width = gutter_width; + + let text_width = bounds.size.width - gutter_width; + let overscroll = size(em_width, px(0.)); + let _snapshot = { + editor.set_visible_line_count((bounds.size.height / line_height).into(), cx); + + let editor_width = text_width - gutter_margin - overscroll.width - 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 gutter_size = size(gutter_width, bounds.size.height); + let text_size = size(text_width, bounds.size.height); + + let autoscroll_horizontally = + editor.autoscroll_vertically(bounds.size.height, 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 = f32::from(bounds.size.height / 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<(PlayerColor, Vec)> = Vec::new(); + let mut active_rows = BTreeMap::new(); + let is_singleton = editor.is_singleton(cx); + + let highlighted_rows = editor.highlighted_rows(); + let highlighted_ranges = editor.background_highlights_in_range( + start_anchor..end_anchor, + &snapshot.display_snapshot, + cx.theme().colors(), + ); + + 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.local_player, 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 = cx + .theme() + .players() + .color_for_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 { + cx.theme() + .players() + .color_for_participant(participant_index.0) + } else { + cx.theme().players().absent() + }; + + // 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 = EditorSettings::get_global(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 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_numbers, fold_statuses) = self.shape_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 = Pixels::ZERO; + let line_layouts = self.layout_lines(start_row..end_row, &line_numbers, &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 longest_line_width = layout_line(snapshot.longest_row(), &snapshot, &style, cx) + .unwrap() + .width; + let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.width; + + let (scroll_width, blocks) = cx.with_element_id(Some("editor_blocks"), |cx| { + self.layout_blocks( + start_row..end_row, + &snapshot, + bounds.size.width, + scroll_width, + gutter_padding, + gutter_width, + em_width, + gutter_width + gutter_margin, + line_height, + &style, + &line_layouts, + editor, + cx, + ) + }); + + let scroll_max = point( + f32::from((scroll_width - text_size.width) / 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.width, + scroll_width, + em_width, + &line_layouts, + cx, + ) + } else { + false + }; + + if clamped || autoscrolled { + snapshot = editor.snapshot(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() { + let max_height = (12. * line_height).min((bounds.size.height - line_height) / 2.); + context_menu = + editor.render_context_menu(newest_selection_head, &self.style, max_height, 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(|element| CodeActionsIndicator { + row: newest_selection_head.row(), + button: element, + }); + } + } + + let visible_rows = start_row..start_row + line_layouts.len() as u32; + let max_size = size( + (120. * em_width) // Default size + .min(bounds.size.width / 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(bounds.size.height / 2.) // Shrink to half of the editor height + .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines + ); + + let hover = editor.hover_state.render( + &snapshot, + &style, + visible_rows, + max_size, + editor.workspace.as_ref().map(|(w, _)| w.clone()), + cx, + ); + + let fold_indicators = cx.with_element_id(Some("gutter_fold_indicators"), |cx| { + editor.render_fold_indicators( + fold_statuses, + &style, + editor.gutter_hovered, + line_height, + gutter_margin, + cx, + ) + }); + + let invisible_symbol_font_size = font_size / 2.; + let tab_invisible = cx + .text_system() + .shape_line( + "→".into(), + invisible_symbol_font_size, + &[TextRun { + len: "→".len(), + font: self.style.text.font(), + color: cx.theme().colors().editor_invisible, + background_color: None, + underline: None, + }], + ) + .unwrap(); + let space_invisible = cx + .text_system() + .shape_line( + "•".into(), + invisible_symbol_font_size, + &[TextRun { + len: "•".len(), + font: self.style.text.font(), + color: cx.theme().colors().editor_invisible, + background_color: None, + underline: None, + }], + ) + .unwrap(); + + LayoutState { + mode: snapshot.mode, + position_map: Arc::new(PositionMap { + size: bounds.size, + scroll_position: point( + scroll_position.x * em_width, + scroll_position.y * line_height, + ), + scroll_max, + line_layouts, + line_height, + em_width, + em_advance, + snapshot, + }), + visible_anchor_range: start_anchor..end_anchor, + 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, + line_numbers, + display_hunks, + blocks, + selections, + context_menu, + code_actions_indicator, + fold_indicators, + tab_invisible, + space_invisible, + hover_popovers: hover, + } + }) + } + #[allow(clippy::too_many_arguments)] fn layout_blocks( - &mut self, + &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, + editor_width: Pixels, + scroll_width: Pixels, + gutter_padding: Pixels, + gutter_width: Pixels, + em_width: Pixels, + text_x: Pixels, + line_height: Pixels, style: &EditorStyle, line_layouts: &[LineWithInvisibles], editor: &mut Editor, cx: &mut ViewContext, - ) -> (f32, Vec) { + ) -> (Pixels, 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 render_block = |block: &TransformBlock, + available_space: Size, + block_id: usize, + editor: &mut Editor, + cx: &mut ViewContext| { let mut element = match block { TransformBlock::Custom(block) => { let align_to = block @@ -1636,7 +2225,8 @@ impl EditorElement { .line .x_for_index(align_to.column() as usize) } else { - layout_line(align_to.row(), snapshot, style, cx.text_layout_cache()) + layout_line(align_to.row(), snapshot, style, cx) + .unwrap() .x_for_index(align_to.column() as usize) }; @@ -1645,26 +2235,26 @@ impl EditorElement { anchor_x, gutter_padding, line_height, - scroll_x, gutter_width, em_width, block_id, + editor_style: &self.style, }) } + 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_handler = project::File::from_dyn(buffer.file()).map(|file| { let jump_path = ProjectPath { worktree_id: file.worktree_id(cx), path: file.path.clone(), @@ -1675,121 +2265,142 @@ impl EditorElement { .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() + let jump_handler = cx.listener_for(&self.editor, move |editor, _, cx| { + editor.jump(jump_path.clone(), jump_position, jump_anchor, cx); + }); + + jump_handler }); - 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 element = if *starts_new_buffer { 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() + "/"); + parent_path = path + .parent() + .map(|p| SharedString::from(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(), + div() + .id(("path header container", block_id)) + .size_full() + .p_1p5() + .child( + h_stack() + .id("path header block") + .py_1p5() + .pl_3() + .pr_2() + .rounded_lg() + .shadow_md() + .border() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().editor_subheader_background) + .justify_between() + .hover(|style| style.bg(cx.theme().colors().element_hover)) + .child( + h_stack().gap_3().child( + h_stack() + .gap_2() + .child( + filename + .map(SharedString::from) + .unwrap_or_else(|| "untitled".into()), + ) + .when_some(parent_path, |then, path| { + then.child( + div().child(path).text_color( + cx.theme().colors().text_muted, + ), + ) + }), + ), + ) + .when_some(jump_handler, |this, jump_handler| { + this.cursor_pointer() + .tooltip(|cx| { + Tooltip::for_action( + "Jump to Buffer", + &OpenExcerpts, + cx, + ) + }) + .on_mouse_down(MouseButton::Left, |_, cx| { + cx.stop_propagation() + }) + .on_click(jump_handler) + }), ) - .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") - } + h_stack() + .id(("collapsed context", block_id)) + .size_full() + .gap(gutter_padding) + .child( + h_stack() + .justify_end() + .flex_none() + .w(gutter_width - gutter_padding) + .h_full() + .text_buffer(cx) + .text_color(cx.theme().colors().editor_line_number) + .child("..."), + ) + .map(|this| { + if let Some(jump_handler) = jump_handler { + this.child( + ButtonLike::new("jump to collapsed context") + .style(ButtonStyle::Transparent) + .full_width() + .on_click(jump_handler) + .tooltip(|cx| { + Tooltip::for_action( + "Jump to Buffer", + &OpenExcerpts, + cx, + ) + }) + .child( + div() + .h_px() + .w_full() + .bg(cx.theme().colors().border_variant) + .group_hover("", |style| { + style.bg(cx.theme().colors().border) + }), + ), + ) + } else { + this.child(div().size_full().bg(gpui::green())) + } + }) + }; + element.into_any() } }; - element.layout( - SizeConstraint { - min: Vector2F::zero(), - max: vec2f(width, block.height() as f32 * line_height), - }, - editor, - cx, - ); - element + let size = element.measure(available_space, cx); + (element, size) }; - let mut fixed_block_max_width = 0f32; + let mut fixed_block_max_width = Pixels::ZERO; let mut blocks = Vec::new(); for (row, block) in fixed_blocks { - let element = render_block(block, f32::INFINITY, block_id); + let available_space = size( + AvailableSpace::MinContent, + AvailableSpace::Definite(block.height() as f32 * line_height), + ); + let (element, element_size) = + render_block(block, available_space, block_id, editor, cx); block_id += 1; - fixed_block_max_width = fixed_block_max_width.max(element.size().x() + em_width); + fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width); blocks.push(BlockLayout { row, element, + available_space, style: BlockStyle::Fixed, }); } @@ -1805,11 +2416,16 @@ impl EditorElement { .max(gutter_width + scroll_width), BlockStyle::Fixed => unreachable!(), }; - let element = render_block(block, width, block_id); + let available_space = size( + AvailableSpace::Definite(width), + AvailableSpace::Definite(block.height() as f32 * line_height), + ); + let (element, _) = render_block(block, available_space, block_id, editor, cx); block_id += 1; blocks.push(BlockLayout { row, element, + available_space, style, }); } @@ -1818,11 +2434,133 @@ impl EditorElement { blocks, ) } + + fn paint_mouse_listeners( + &mut self, + bounds: Bounds, + gutter_bounds: Bounds, + text_bounds: Bounds, + layout: &LayoutState, + cx: &mut WindowContext, + ) { + let interactive_bounds = InteractiveBounds { + bounds: bounds.intersect(&cx.content_mask().bounds), + stacking_order: cx.stacking_order().clone(), + }; + + cx.on_mouse_event({ + let position_map = layout.position_map.clone(); + let editor = self.editor.clone(); + let interactive_bounds = interactive_bounds.clone(); + + move |event: &ScrollWheelEvent, phase, cx| { + if phase == DispatchPhase::Bubble + && interactive_bounds.visibly_contains(&event.position, cx) + { + editor.update(cx, |editor, cx| { + Self::scroll(editor, event, &position_map, &interactive_bounds, cx) + }); + } + } + }); + + cx.on_mouse_event({ + let position_map = layout.position_map.clone(); + let editor = self.editor.clone(); + let stacking_order = cx.stacking_order().clone(); + let interactive_bounds = interactive_bounds.clone(); + + move |event: &MouseDownEvent, phase, cx| { + if phase == DispatchPhase::Bubble + && interactive_bounds.visibly_contains(&event.position, cx) + { + match event.button { + MouseButton::Left => editor.update(cx, |editor, cx| { + Self::mouse_left_down( + editor, + event, + &position_map, + text_bounds, + gutter_bounds, + &stacking_order, + cx, + ); + }), + MouseButton::Right => editor.update(cx, |editor, cx| { + Self::mouse_right_down(editor, event, &position_map, text_bounds, cx); + }), + _ => {} + }; + } + } + }); + + cx.on_mouse_event({ + let position_map = layout.position_map.clone(); + let editor = self.editor.clone(); + let stacking_order = cx.stacking_order().clone(); + let interactive_bounds = interactive_bounds.clone(); + + move |event: &MouseUpEvent, phase, cx| { + if phase == DispatchPhase::Bubble + && interactive_bounds.visibly_contains(&event.position, cx) + { + editor.update(cx, |editor, cx| { + Self::mouse_up( + editor, + event, + &position_map, + text_bounds, + &stacking_order, + cx, + ) + }); + } + } + }); + cx.on_mouse_event({ + let position_map = layout.position_map.clone(); + let editor = self.editor.clone(); + let stacking_order = cx.stacking_order().clone(); + + move |event: &MouseMoveEvent, phase, cx| { + // if editor.has_pending_selection() && event.pressed_button == Some(MouseButton::Left) { + + if phase == DispatchPhase::Bubble { + editor.update(cx, |editor, cx| { + if event.pressed_button == Some(MouseButton::Left) { + Self::mouse_dragged( + editor, + event, + &position_map, + text_bounds, + gutter_bounds, + &stacking_order, + cx, + ) + } + + if interactive_bounds.visibly_contains(&event.position, cx) { + Self::mouse_moved( + editor, + event, + &position_map, + text_bounds, + gutter_bounds, + &stacking_order, + cx, + ) + } + }); + } + } + }); + } } #[derive(Debug)] pub struct LineWithInvisibles { - pub line: Line, + pub line: ShapedLine, invisibles: Vec, } @@ -1830,12 +2568,11 @@ 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], + line_number_layouts: &[Option], editor_mode: EditorMode, + cx: &WindowContext, ) -> Vec { let mut layouts = Vec::with_capacity(max_line_count); let mut line = String::new(); @@ -1844,6 +2581,8 @@ impl LineWithInvisibles { let mut non_whitespace_added = false; let mut row = 0; let mut line_exceeded_max_len = false; + let font_size = text_style.font_size.to_pixels(cx.rem_size()); + for highlighted_chunk in chunks.chain([HighlightedChunk { chunk: "\n", style: None, @@ -1851,8 +2590,12 @@ impl LineWithInvisibles { }]) { for (ix, mut line_chunk) in highlighted_chunk.chunk.split('\n').enumerate() { if ix > 0 { + let shaped_line = cx + .text_system() + .shape_line(line.clone().into(), font_size, &styles) + .unwrap(); layouts.push(Self { - line: text_layout_cache.layout_str(&line, text_style.font_size, &styles), + line: shaped_line, invisibles: invisibles.drain(..).collect(), }); @@ -1868,11 +2611,7 @@ impl LineWithInvisibles { 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)) + Cow::Owned(text_style.clone().highlight(style)) } else { Cow::Borrowed(text_style) }; @@ -1886,14 +2625,13 @@ impl LineWithInvisibles { 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, - }, - )); + styles.push(TextRun { + len: line_chunk.len(), + font: text_style.font(), + color: text_style.color, + background_color: text_style.background_color, + underline: text_style.underline, + }); if editor_mode == EditorMode::Full { // Line wrap pads its contents with fake whitespaces, @@ -1938,33 +2676,28 @@ impl LineWithInvisibles { &self, layout: &LayoutState, row: u32, - scroll_top: f32, - content_origin: Vector2F, - scroll_left: f32, - visible_text_bounds: RectF, + content_origin: gpui::Point, whitespace_setting: ShowWhitespaceSetting, selection_ranges: &[Range], - visible_bounds: RectF, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { let line_height = layout.position_map.line_height; - let line_y = row as f32 * line_height - scroll_top; + let line_y = line_height * row as f32 - layout.position_map.scroll_position.y; - self.line.paint( - content_origin + vec2f(-scroll_left, line_y), - visible_text_bounds, - line_height, - cx, - ); + self.line + .paint( + content_origin + gpui::point(-layout.position_map.scroll_position.x, line_y), + line_height, + cx, + ) + .log_err(); self.draw_invisibles( &selection_ranges, layout, content_origin, - scroll_left, line_y, row, - visible_bounds, line_height, whitespace_setting, cx, @@ -1975,14 +2708,12 @@ impl LineWithInvisibles { &self, selection_ranges: &[Range], layout: &LayoutState, - content_origin: Vector2F, - scroll_left: f32, - line_y: f32, + content_origin: gpui::Point, + line_y: Pixels, row: u32, - visible_bounds: RectF, - line_height: f32, + line_height: Pixels, whitespace_setting: ShowWhitespaceSetting, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { let allowed_invisibles_regions = match whitespace_setting { ShowWhitespaceSetting::None => return, @@ -1998,8 +2729,12 @@ impl LineWithInvisibles { 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); + (layout.position_map.em_width - invisible_symbol.width).max(Pixels::ZERO) / 2.0; + let origin = content_origin + + gpui::point( + x_offset + invisible_offset - layout.position_map.scroll_position.x, + line_y, + ); if let Some(allowed_regions) = allowed_invisibles_regions { let invisible_point = DisplayPoint::new(row, token_offset as u32); @@ -2010,7 +2745,7 @@ impl LineWithInvisibles { continue; } } - invisible_symbol.paint(origin, visible_bounds, line_height, cx); + invisible_symbol.paint(origin, line_height, cx).log_err(); } } } @@ -2021,601 +2756,134 @@ enum Invisible { Whitespace { line_offset: usize }, } -impl Element for EditorElement { - type LayoutState = LayoutState; - type PaintState = (); +impl Element for EditorElement { + type State = (); - fn layout( + fn request_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"); - } + _element_state: Option, + cx: &mut gpui::WindowContext, + ) -> (gpui::LayoutId, Self::State) { + self.editor.update(cx, |editor, cx| { + editor.set_style(self.style.clone(), cx); - 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), + let layout_id = match editor.mode { + EditorMode::SingleLine => { + let rem_size = cx.rem_size(); + let mut style = Style::default(); + style.size.width = relative(1.).into(); + style.size.height = self.style.text.line_height_in_pixels(rem_size).into(); + cx.request_layout(&style, None) + } + EditorMode::AutoHeight { max_lines } => { + let editor_handle = cx.view().clone(); + let max_line_number_width = + self.max_line_number_width(&editor.snapshot(cx), cx); + cx.request_measured_layout(Style::default(), move |known_dimensions, _, cx| { + editor_handle + .update(cx, |editor, cx| { + compute_auto_height_layout( + editor, + max_lines, + max_line_number_width, + known_dimensions, + cx, + ) + }) + .unwrap_or_default() + }) + } + EditorMode::Full => { + let mut style = Style::default(); + style.size.width = relative(1.).into(); + style.size.height = relative(1.).into(); + cx.request_layout(&style, None) + } }; - 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, - }, - ) + (layout_id, ()) + }) } 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)); + bounds: Bounds, + _element_state: &mut Self::State, + cx: &mut gpui::WindowContext, + ) { + let editor = self.editor.clone(); - 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, + cx.with_text_style( + Some(gpui::TextStyleRefinement { + font_size: Some(self.style.text.font_size), + ..Default::default() + }), + |cx| { + let mut layout = self.compute_layout(bounds, cx); + let gutter_bounds = Bounds { + origin: bounds.origin, + size: layout.gutter_size, + }; + let text_bounds = Bounds { + origin: gutter_bounds.upper_right(), + size: layout.text_size, + }; + + let focus_handle = editor.focus_handle(cx); + let key_context = self.editor.read(cx).key_context(cx); + cx.with_key_dispatch(Some(key_context), Some(focus_handle.clone()), |_, cx| { + self.register_actions(cx); + self.register_key_listeners(cx); + + cx.with_content_mask(Some(ContentMask { bounds }), |cx| { + let input_handler = + ElementInputHandler::new(bounds, self.editor.clone(), cx); + cx.handle_input(&focus_handle, input_handler); + + self.paint_background(gutter_bounds, text_bounds, &layout, cx); + if layout.gutter_size.width > Pixels::ZERO { + self.paint_gutter(gutter_bounds, &mut layout, cx); + } + self.paint_text(text_bounds, &mut layout, cx); + + cx.with_z_index(0, |cx| { + self.paint_mouse_listeners( + bounds, + gutter_bounds, + text_bounds, + &layout, + cx, + ); + }); + if !layout.blocks.is_empty() { + cx.with_z_index(0, |cx| { + cx.with_element_id(Some("editor_blocks"), |cx| { + self.paint_blocks(bounds, &mut layout, cx); + }); + }) + } + + cx.with_z_index(1, |cx| { + self.paint_overlays(text_bounds, &mut layout, cx); + }); + + cx.with_z_index(2, |cx| self.paint_scrollbar(bounds, &mut layout, cx)); + }); + }) + }, ); + } +} - Self::attach_mouse_handlers( - &layout.position_map, - layout.hover_popovers.is_some(), - visible_bounds, - text_bounds, - gutter_bounds, - bounds, - cx, - ); +impl IntoElement for EditorElement { + type Element = Self; - 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 element_id(&self) -> Option { + self.editor.element_id() } - 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() - }) + fn into_element(self) -> Self::Element { + self } } @@ -2623,39 +2891,45 @@ type BufferRow = u32; pub struct LayoutState { position_map: Arc, - gutter_size: Vector2F, - gutter_padding: f32, - gutter_margin: f32, - text_size: Vector2F, + gutter_size: Size, + gutter_padding: Pixels, + gutter_margin: Pixels, + text_size: gpui::Size, mode: EditorMode, - wrap_guides: SmallVec<[(f32, bool); 2]>, + wrap_guides: SmallVec<[(Pixels, bool); 2]>, + visible_anchor_range: Range, visible_display_row_range: Range, active_rows: BTreeMap, highlighted_rows: Option>, - line_number_layouts: Vec>, + line_numbers: Vec>, display_hunks: Vec, blocks: Vec, - highlighted_ranges: Vec<(Range, Color)>, - fold_ranges: Vec<(BufferRow, Range, Color)>, - selections: Vec<(SelectionStyle, Vec)>, + highlighted_ranges: Vec<(Range, Hsla)>, + selections: Vec<(PlayerColor, 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, + context_menu: Option<(DisplayPoint, AnyElement)>, + code_actions_indicator: Option, + hover_popovers: Option<(DisplayPoint, Vec)>, + fold_indicators: Vec>, + tab_invisible: ShapedLine, + space_invisible: ShapedLine, +} + +struct CodeActionsIndicator { + row: u32, + button: IconButton, } struct PositionMap { - size: Vector2F, - line_height: f32, - scroll_max: Vector2F, - em_width: f32, - em_advance: f32, + size: Size, + line_height: Pixels, + scroll_position: gpui::Point, + scroll_max: gpui::Point, + em_width: Pixels, + em_advance: Pixels, line_layouts: Vec, snapshot: EditorSnapshot, } @@ -2689,21 +2963,26 @@ 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()); - 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.height); + 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() as usize) - .map(|line_with_spaces| &line_with_spaces.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, 0.0) + (ix as u32, px(0.)) } else { - (line.len() as u32, 0f32.max(x - line.width())) + (line.len as u32, px(0.).max(x - line.width)) } } else { (0, x) @@ -2726,7 +3005,8 @@ impl PositionMap { struct BlockLayout { row: u32, - element: AnyElement, + element: AnyElement, + available_space: Size, style: BlockStyle, } @@ -2734,8 +3014,8 @@ fn layout_line( row: u32, snapshot: &EditorSnapshot, style: &EditorStyle, - layout_cache: &TextLayoutCache, -) -> text_layout::Line { + cx: &WindowContext, +) -> Result { let mut line = snapshot.line(row); if line.len() > MAX_LINE_LEN { @@ -2747,38 +3027,37 @@ fn layout_line( 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(), - }, - )], + cx.text_system().shape_line( + line.into(), + style.text.font_size.to_pixels(cx.rem_size()), + &[TextRun { + len: snapshot.line_len(row) as usize, + font: style.text.font(), + color: Hsla::default(), + background_color: None, + underline: None, + }], ) } #[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, + block_text: Option, } impl Cursor { pub fn new( - 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, + block_text: Option, ) -> Cursor { Cursor { origin, @@ -2790,45 +3069,44 @@ impl Cursor { } } - pub fn bounding_rect(&self, origin: Vector2F) -> RectF { - RectF::new( - self.origin + origin, - vec2f(self.block_width, self.line_height), - ) + pub fn bounding_rect(&self, origin: gpui::Point) -> Bounds { + Bounds { + origin: self.origin + origin, + size: size(self.block_width, self.line_height), + } } - pub fn paint(&self, origin: Vector2F, cx: &mut WindowContext) { + pub fn paint(&self, origin: gpui::Point, 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), - ), + CursorShape::Bar => Bounds { + origin: self.origin + origin, + size: size(px(2.0), self.line_height), + }, + CursorShape::Block | CursorShape::Hollow => Bounds { + origin: self.origin + origin, + size: size(self.block_width, self.line_height), + }, + CursorShape::Underscore => Bounds { + origin: self.origin + + origin + + gpui::Point::new(Pixels::ZERO, self.line_height - px(2.0)), + size: size(self.block_width, px(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(), - }); + let cursor = if matches!(self.shape, CursorShape::Hollow) { + outline(bounds, self.color) } else { - cx.scene().push_quad(Quad { - bounds, - background: Some(self.color), - border: Default::default(), - corner_radii: Default::default(), - }); - } + fill(bounds, self.color) + }; + + cx.paint_quad(cursor); if let Some(block_text) = &self.block_text { - block_text.paint(self.origin + origin, bounds, self.line_height, cx); + block_text + .paint(self.origin + origin, self.line_height, cx) + .log_err(); } } @@ -2839,21 +3117,21 @@ impl Cursor { #[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)] pub struct HighlightedRangeLine { - pub start_x: f32, - pub end_x: f32, + pub start_x: Pixels, + pub end_x: Pixels, } impl HighlightedRange { - pub fn paint(&self, bounds: RectF, cx: &mut WindowContext) { + 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( @@ -2869,24 +3147,23 @@ impl HighlightedRange { fn paint_lines( &self, - start_y: f32, + start_y: Pixels, lines: &[HighlightedRangeLine], - bounds: RectF, + _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 first_top_left = point(first_line.start_x, start_y); + let first_top_right = point(first_line.end_x, start_y); - let curve_height = vec2f(0., self.corner_radius); - let curve_width = |start_x: f32, end_x: f32| { + let curve_height = point(Pixels::ZERO, self.corner_radius); + let curve_width = |start_x: Pixels, end_x: Pixels| { let max = (end_x - start_x) / 2.; let width = if max < self.corner_radius { max @@ -2894,43 +3171,43 @@ impl HighlightedRange { self.corner_radius }; - vec2f(width, 0.) + point(width, Pixels::ZERO) }; let top_curve_width = curve_width(first_line.start_x, first_line.end_x); - path.reset(first_top_right - top_curve_width); + let mut path = gpui::Path::new(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); + let bottom_right = point(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()); + let next_top_right = point(next_line.end_x, bottom_right.y); - match next_top_right.x().partial_cmp(&bottom_right.x()).unwrap() { + 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()); + let curve_width = curve_width(next_top_right.x, bottom_right.x); path.line_to(bottom_right - curve_height); - if self.corner_radius > 0. { + if self.corner_radius > Pixels::ZERO { path.curve_to(bottom_right - curve_width, bottom_right); } path.line_to(next_top_right + curve_width); - if self.corner_radius > 0. { + if self.corner_radius > Pixels::ZERO { 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()); + let curve_width = curve_width(bottom_right.x, next_top_right.x); path.line_to(bottom_right - curve_height); - if self.corner_radius > 0. { + if self.corner_radius > Pixels::ZERO { path.curve_to(bottom_right + curve_width, bottom_right); } path.line_to(next_top_right - curve_width); - if self.corner_radius > 0. { + if self.corner_radius > Pixels::ZERO { path.curve_to(next_top_right + curve_height, next_top_right); } } @@ -2938,13 +3215,13 @@ impl HighlightedRange { } else { let curve_width = curve_width(line.start_x, line.end_x); path.line_to(bottom_right - curve_height); - if self.corner_radius > 0. { + if self.corner_radius > Pixels::ZERO { path.curve_to(bottom_right - curve_width, bottom_right); } - let bottom_left = vec2f(line.start_x, bottom_right.y()); + let bottom_left = point(line.start_x, bottom_right.y); path.line_to(bottom_left + curve_width); - if self.corner_radius > 0. { + if self.corner_radius > Pixels::ZERO { path.curve_to(bottom_left - curve_height, bottom_left); } } @@ -2952,86 +3229,34 @@ impl HighlightedRange { 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); + let second_top_left = point(last_line.start_x, start_y + self.line_height); path.line_to(second_top_left + curve_height); - if self.corner_radius > 0. { + if self.corner_radius > Pixels::ZERO { path.curve_to(second_top_left + curve_width, second_top_left); } - let first_bottom_left = vec2f(first_line.start_x, second_top_left.y()); + let first_bottom_left = point(first_line.start_x, second_top_left.y); path.line_to(first_bottom_left - curve_width); - if self.corner_radius > 0. { + if self.corner_radius > Pixels::ZERO { path.curve_to(first_bottom_left - curve_height, first_bottom_left); } } path.line_to(first_top_left + curve_height); - if self.corner_radius > 0. { + if self.corner_radius > Pixels::ZERO { 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))); + cx.paint_path(path, self.color); } } -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: Pixels) -> f32 { + (delta.pow(1.5) / 100.0).into() } -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 +fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 { + (delta.pow(1.2) / 300.0).into() } #[cfg(test)] @@ -3049,35 +3274,40 @@ mod tests { use util::test::sample_text; #[gpui::test] - fn test_layout_line_numbers(cx: &mut TestAppContext) { + fn test_shape_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 + let window = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); + Editor::new(EditorMode::Full, buffer, None, cx) }); + + let editor = window.root(cx).unwrap(); + let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); + let element = EditorElement::new(&editor, style); + + let layouts = window + .update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + element + .shape_line_numbers( + 0..6, + &Default::default(), + DisplayPoint::new(0, 0), + false, + &snapshot, + cx, + ) + .0 + }) + .unwrap(); 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)) - }); + let relative_rows = window + .update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + element.calculate_relative_line_numbers(&snapshot, &(0..6), Some(3)) + }) + .unwrap(); assert_eq!(relative_rows[&0], 3); assert_eq!(relative_rows[&1], 2); assert_eq!(relative_rows[&2], 1); @@ -3086,22 +3316,26 @@ mod tests { 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); + let relative_rows = window + .update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); - element.calculate_relative_line_numbers(&snapshot, &(3..6), Some(1)) - }); + element.calculate_relative_line_numbers(&snapshot, &(3..6), Some(1)) + }) + .unwrap(); 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); + let relative_rows = window + .update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); - element.calculate_relative_line_numbers(&snapshot, &(0..3), Some(6)) - }); + element.calculate_relative_line_numbers(&snapshot, &(0..3), Some(6)) + }) + .unwrap(); assert_eq!(relative_rows.len(), 3); assert_eq!(relative_rows[&0], 5); assert_eq!(relative_rows[&1], 4); @@ -3112,28 +3346,38 @@ mod tests { 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, - ) + let window = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx); + Editor::new(EditorMode::Full, buffer, None, cx) }); + let editor = window.root(cx).unwrap(); + let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); + let mut element = EditorElement::new(&editor, style); + + window + .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), + ]); + }); + }) + .unwrap(); + let state = cx + .update_window(window.into(), |_, cx| { + element.compute_layout( + Bounds { + origin: point(px(500.), px(500.)), + size: size(px(500.), px(500.)), + }, + cx, + ) + }) + .unwrap(); + assert_eq!(state.selections.len(), 1); let local_selections = &state.selections[0].1; assert_eq!(local_selections.len(), 3); @@ -3182,29 +3426,29 @@ mod tests { // 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| { + let window = 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, cx) + }); + let editor = window.root(cx).unwrap(); + let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); + let mut element = EditorElement::new(&editor, style); + let _state = window.update(cx, |editor, cx| { editor.cursor_shape = CursorShape::Block; editor.change_selections(None, cx, |s| { s.select_display_ranges([ @@ -3212,13 +3456,19 @@ mod tests { DisplayPoint::new(10, 0)..DisplayPoint::new(13, 0), ]); }); - element.layout( - SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)), - editor, - cx, - ) }); + let state = cx + .update_window(window.into(), |_, cx| { + element.compute_layout( + Bounds { + origin: point(px(500.), px(500.)), + size: size(px(500.), px(500.)), + }, + cx, + ) + }) + .unwrap(); assert_eq!(state.selections.len(), 1); let local_selections = &state.selections[0].1; assert_eq!(local_selections.len(), 2); @@ -3230,7 +3480,6 @@ mod tests { 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!( @@ -3244,44 +3493,50 @@ mod tests { 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) + let window = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("", cx); + Editor::new(EditorMode::Full, buffer, None, cx) + }); + let editor = window.root(cx).unwrap(); + let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); + window + .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(|_| div().into_any()), + }], + None, + cx, + ); + + // Blur the editor so that it displays placeholder text. + cx.blur(); }) - .root(cx); + .unwrap(); - 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, - ) - }); + let mut element = EditorElement::new(&editor, style); + let state = cx + .update_window(window.into(), |_, cx| { + element.compute_layout( + Bounds { + origin: point(px(500.), px(500.)), + size: size(px(500.), px(500.)), + }, + cx, + ) + }) + .unwrap(); + let size = state.position_map.size; assert_eq!(state.position_map.line_layouts.len(), 4); assert_eq!( state - .line_number_layouts + .line_numbers .iter() .map(Option::is_some) .collect::>(), @@ -3289,10 +3544,11 @@ mod tests { ); // Don't panic. - let bounds = RectF::new(Default::default(), size); - editor.update(cx, |editor, cx| { - element.paint(bounds, bounds, &mut state, editor, cx); - }); + let bounds = Bounds::::new(Default::default(), size); + cx.update_window(window.into(), |_, cx| { + element.paint(bounds, &mut (), cx); + }) + .unwrap() } #[gpui::test] @@ -3335,7 +3591,7 @@ mod tests { }); let actual_invisibles = - collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, 500.0); + collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, px(500.0)); assert_eq!(expected_invisibles, actual_invisibles); } @@ -3355,10 +3611,10 @@ mod tests { cx, editor_mode_without_invisibles, "\t\t\t| | a b", - 500.0, + px(500.0), ); assert!(invisibles.is_empty(), - "For editor mode {editor_mode_without_invisibles:?} no invisibles was expected but got {invisibles:?}"); + "For editor mode {editor_mode_without_invisibles:?} no invisibles was expected but got {invisibles:?}"); } } @@ -3409,8 +3665,12 @@ mod tests { s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength); }); - let actual_invisibles = - collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, editor_width); + let actual_invisibles = collect_invisibles_from_new_editor( + cx, + EditorMode::Full, + &input_text, + px(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). @@ -3442,29 +3702,36 @@ mod tests { cx: &mut TestAppContext, editor_mode: EditorMode, input_text: &str, - editor_width: f32, + editor_width: Pixels, ) -> Vec { info!( - "Creating editor with mode {editor_mode:?}, width {editor_width} and text '{input_text}'" + "Creating editor with mode {editor_mode:?}, width {}px and text '{input_text}'", + editor_width.0 ); - 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, - ) + let window = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple(&input_text, cx); + Editor::new(editor_mode, buffer, None, cx) }); + let editor = window.root(cx).unwrap(); + let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); + let mut element = EditorElement::new(&editor, style); + window + .update(cx, |editor, cx| { + editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx); + editor.set_wrap_width(Some(editor_width), cx); + }) + .unwrap(); + let layout_state = cx + .update_window(window.into(), |_, cx| { + element.compute_layout( + Bounds { + origin: point(px(500.), px(500.)), + size: size(px(500.), px(500.)), + }, + cx, + ) + }) + .unwrap(); layout_state .position_map @@ -3476,3 +3743,73 @@ mod tests { .collect() } } + +pub fn register_action( + view: &View, + cx: &mut WindowContext, + listener: impl Fn(&mut Editor, &T, &mut ViewContext) + 'static, +) { + let view = view.clone(); + cx.on_action(TypeId::of::(), move |action, phase, cx| { + let action = action.downcast_ref().unwrap(); + if phase == DispatchPhase::Bubble { + view.update(cx, |editor, cx| { + listener(editor, action, cx); + }) + } + }) +} + +fn compute_auto_height_layout( + editor: &mut Editor, + max_lines: usize, + max_line_number_width: Pixels, + known_dimensions: Size>, + cx: &mut ViewContext, +) -> Option> { + let width = known_dimensions.width?; + if let Some(height) = known_dimensions.height { + return Some(size(width, height)); + } + + let style = editor.style.as_ref().unwrap(); + let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); + let font_size = style.text.font_size.to_pixels(cx.rem_size()); + let line_height = style.text.line_height_in_pixels(cx.rem_size()); + let em_width = cx + .text_system() + .typographic_bounds(font_id, font_size, 'm') + .unwrap() + .size + .width; + + let mut snapshot = editor.snapshot(cx); + let gutter_width; + let gutter_margin; + if snapshot.show_gutter { + let descent = cx.text_system().descent(font_id, font_size); + let gutter_padding_factor = 3.5; + let gutter_padding = (em_width * gutter_padding_factor).round(); + gutter_width = max_line_number_width + gutter_padding * 2.0; + gutter_margin = -descent; + } else { + gutter_width = Pixels::ZERO; + gutter_margin = Pixels::ZERO; + }; + + editor.gutter_width = gutter_width; + let text_width = width - gutter_width; + let overscroll = size(em_width, px(0.)); + + let editor_width = text_width - gutter_margin - overscroll.width - em_width; + if editor.set_wrap_width(Some(editor_width), cx) { + snapshot = editor.snapshot(cx); + } + + let scroll_height = Pixels::from(snapshot.max_point().row() + 1) * line_height; + let height = scroll_height + .max(line_height) + .min(line_height * max_lines as f32); + + Some(size(width, height)) +} diff --git a/crates/editor/src/git.rs b/crates/editor/src/git.rs index f8c6ef9a1f..e1715aa3b2 100644 --- a/crates/editor/src/git.rs +++ b/crates/editor/src/git.rs @@ -60,8 +60,8 @@ pub fn diff_hunk_to_display(hunk: DiffHunk, snapshot: &DisplaySnapshot) -> 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 containing_fold = snapshot.folds_in_range(folds_range).find(|fold| { + 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); @@ -72,7 +72,7 @@ pub fn diff_hunk_to_display(hunk: DiffHunk, snapshot: &DisplaySnapshot) -> }); if let Some(fold) = containing_fold { - let row = fold.start.to_display_point(snapshot).row(); + let row = fold.range.start.to_display_point(snapshot).row(); DisplayDiffHunk::Folded { display_row: row } } else { let start = hunk_start_point.to_display_point(snapshot).row(); @@ -88,11 +88,11 @@ pub fn diff_hunk_to_display(hunk: DiffHunk, snapshot: &DisplaySnapshot) -> } } -#[cfg(any(test, feature = "test_support"))] +#[cfg(test)] mod tests { use crate::editor_tests::init_test; use crate::Point; - use gpui::TestAppContext; + use gpui::{Context, TestAppContext}; use multi_buffer::{ExcerptRange, MultiBuffer}; use project::{FakeFs, Project}; use unindent::Unindent; @@ -101,7 +101,7 @@ mod tests { use git::diff::DiffHunkStatus; init_test(cx, |_| {}); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.background_executor.clone()); let project = Project::test(fs, [], cx).await; // buffer has two modified hunks with two rows each @@ -180,9 +180,9 @@ mod tests { ); }); - cx.foreground().run_until_parked(); + cx.background_executor.run_until_parked(); - let multibuffer = cx.add_model(|cx| { + let multibuffer = cx.new_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( buffer_1.clone(), diff --git a/crates/editor/src/highlight_matching_bracket.rs b/crates/editor/src/highlight_matching_bracket.rs index a0baf6882f..1ed7700f37 100644 --- a/crates/editor/src/highlight_matching_bracket.rs +++ b/crates/editor/src/highlight_matching_bracket.rs @@ -24,7 +24,7 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon opening_range.to_anchors(&snapshot.buffer_snapshot), closing_range.to_anchors(&snapshot.buffer_snapshot), ], - |theme| theme.editor.document_highlight_read_background, + |theme| theme.editor_document_highlight_read_background, cx, ) } diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 5b3985edf9..6fdd53f43a 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -6,16 +6,17 @@ use crate::{ }; 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, + actions, div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, Model, + MouseButton, ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled, + Task, ViewContext, WeakView, }; +use language::{markdown, Bias, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown}; + +use lsp::DiagnosticSeverity; use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; +use settings::Settings; use std::{ops::Range, sync::Arc, time::Duration}; +use ui::{StyledExt, Tooltip}; use util::TryFutureExt; use workspace::Workspace; @@ -23,15 +24,11 @@ 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.; +pub const MIN_POPOVER_LINE_HEIGHT: Pixels = px(4.); +pub const HOVER_POPOVER_GAP: Pixels = px(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(); @@ -41,7 +38,7 @@ pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext) { /// 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 +76,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; } @@ -100,14 +97,14 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie let task = cx.spawn(|this, mut cx| { async move { - cx.background() + 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 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; @@ -122,7 +119,7 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie // Highlight the selected symbol using a background highlight this.highlight_inlay_background::( vec![inlay_hover.range], - |theme| theme.editor.hover_popover.highlight, + |theme| theme.element_hover, // todo!("use a proper background here") cx, ); this.hover_state.info_popover = Some(hover_popover); @@ -239,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 @@ -252,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; @@ -310,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; @@ -334,7 +332,7 @@ fn show_hover( // Highlight the selected symbol using a background highlight this.highlight_background::( vec![symbol_range], - |theme| theme.editor.hover_popover.highlight, + |theme| theme.element_hover, // todo! update theme cx, ); } else { @@ -423,9 +421,10 @@ impl HoverState { snapshot: &EditorSnapshot, style: &EditorStyle, visible_rows: Range, - workspace: Option>, + max_size: Size, + workspace: Option>, cx: &mut ViewContext, - ) -> Option<(DisplayPoint, Vec>)> { + ) -> 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 @@ -450,10 +449,10 @@ impl HoverState { let mut elements = Vec::new(); if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() { - elements.push(diagnostic_popover.render(style, cx)); + elements.push(diagnostic_popover.render(style, max_size, cx)); } if let Some(info_popover) = self.info_popover.as_mut() { - elements.push(info_popover.render(style, workspace, cx)); + elements.push(info_popover.render(style, max_size, workspace, cx)); } Some((point, elements)) @@ -462,7 +461,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,29 +471,28 @@ impl InfoPopover { pub fn render( &mut self, style: &EditorStyle, - workspace: Option>, + max_size: Size, + 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() + ) -> AnyElement { + div() + .id("info_popover") + .elevation_2(cx) + .p_2() + .overflow_y_scroll() + .max_w(max_size.width) + .max_h(max_size.height) + // Prevent a mouse move on the popover from being propagated to the editor, + // because that would dismiss the popover. + .on_mouse_move(|_, cx| cx.stop_propagation()) + .child(crate::render_parsed_markdown( + "content", + &self.parsed_content, + style, + workspace, + cx, + )) + .into_any_element() } } @@ -505,56 +503,74 @@ 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(); - + pub fn render( + &self, + style: &EditorStyle, + max_size: Size, + cx: &mut ViewContext, + ) -> AnyElement { 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), + Some(source) => format!("{source}: {}", self.local_diagnostic.diagnostic.message), + None => self.local_diagnostic.diagnostic.message.clone(), }; - 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, + struct DiagnosticColors { + pub text: Hsla, + pub background: Hsla, + pub border: Hsla, + } + + let diagnostic_colors = match self.local_diagnostic.diagnostic.severity { + DiagnosticSeverity::ERROR => DiagnosticColors { + text: style.status.error, + background: style.status.error_background, + border: style.status.error_border, + }, + DiagnosticSeverity::WARNING => DiagnosticColors { + text: style.status.warning, + background: style.status.warning_background, + border: style.status.warning_border, + }, + DiagnosticSeverity::INFORMATION => DiagnosticColors { + text: style.status.info, + background: style.status.info_background, + border: style.status.info_border, + }, + DiagnosticSeverity::HINT => DiagnosticColors { + text: style.status.hint, + background: style.status.hint_background, + border: style.status.hint_border, + }, + _ => DiagnosticColors { + text: style.status.ignored, + background: style.status.ignored_background, + border: style.status.ignored_border, + }, }; - 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() + div() + .id("diagnostic") + .overflow_y_scroll() + .px_2() + .py_1() + .bg(diagnostic_colors.background) + .text_color(diagnostic_colors.text) + .border_1() + .border_color(diagnostic_colors.border) + .rounded_md() + .max_w(max_size.width) + .max_h(max_size.height) + .cursor(CursorStyle::PointingHand) + .tooltip(move |cx| Tooltip::for_action("Go To Diagnostic", &crate::GoToDiagnostic, cx)) + // Prevent a mouse move on the popover from being propagated to the editor, + // because that would dismiss the popover. + .on_mouse_move(|_, cx| cx.stop_propagation()) + // Prevent a mouse down on the popover from being propagated to the editor, + // because that would move the cursor. + .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) + .on_click(cx.listener(|editor, _, cx| editor.go_to_diagnostic(&Default::default(), cx))) + .child(SharedString::from(text)) + .into_any_element() } pub fn activation_info(&self) -> (usize, Anchor) { @@ -579,7 +595,7 @@ mod tests { InlayId, }; use collections::BTreeSet; - use gpui::fonts::{HighlightStyle, Underline, Weight}; + use gpui::{FontWeight, HighlightStyle, UnderlineStyle}; use indoc::indoc; use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; use lsp::LanguageServerId; @@ -626,7 +642,7 @@ mod tests { range: Some(symbol_range), })) }); - cx.foreground() + cx.background_executor .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); requests.next().await; @@ -649,7 +665,7 @@ mod tests { .lsp .handle_request::(|_, _| async move { Ok(None) }); cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx)); - cx.foreground() + cx.background_executor .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); request.next().await; cx.editor(|editor, _| { @@ -853,7 +869,7 @@ mod tests { // Hover pops diagnostic immediately cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); - cx.foreground().run_until_parked(); + cx.background_executor.run_until_parked(); cx.editor(|Editor { hover_state, .. }, _| { assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none()) @@ -872,10 +888,10 @@ mod tests { range: Some(range), })) }); - cx.foreground() + cx.background_executor .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); - cx.foreground().run_until_parked(); + cx.background_executor.run_until_parked(); cx.editor(|Editor { hover_state, .. }, _| { hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some() }); @@ -885,48 +901,49 @@ mod tests { 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); + let editor = cx.add_window(|cx| Editor::single_line(cx)); + editor + .update(cx, |editor, _cx| { + let style = editor.style.clone().unwrap(); - struct Row { - blocks: Vec, - expected_marked_text: String, - expected_styles: Vec, - } + 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(), + 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 { + font_weight: Some(FontWeight::BOLD), ..Default::default() - }), - ..Default::default() - }], - }, - // Lists - Row { - blocks: vec![HoverBlock { - text: " + }], + }, + // 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(UnderlineStyle { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + }], + }, + // Lists + Row { + blocks: vec![HoverBlock { + text: " lists: * one - a @@ -934,10 +951,10 @@ mod tests { * two - [c](https://the-url) - d" - .unindent(), - kind: HoverBlockKind::Markdown, - }], - expected_marked_text: " + .unindent(), + kind: HoverBlockKind::Markdown, + }], + expected_marked_text: " lists: - one - a @@ -945,19 +962,19 @@ mod tests { - two - «c» - d" - .unindent(), - expected_styles: vec![HighlightStyle { - underline: Some(Underline { - thickness: 1.0.into(), + .unindent(), + expected_styles: vec![HighlightStyle { + underline: Some(UnderlineStyle { + thickness: 1.0.into(), + ..Default::default() + }), ..Default::default() - }), - ..Default::default() - }], - }, - // Multi-paragraph list items - Row { - blocks: vec![HoverBlock { - text: " + }], + }, + // Multi-paragraph list items + Row { + blocks: vec![HoverBlock { + text: " * one two three @@ -968,10 +985,10 @@ mod tests { nine * ten * six" - .unindent(), - kind: HoverBlockKind::Markdown, - }], - expected_marked_text: " + .unindent(), + kind: HoverBlockKind::Markdown, + }], + expected_marked_text: " - one two three - four five - six seven eight @@ -979,52 +996,51 @@ mod tests { nine - ten - six" - .unindent(), - expected_styles: vec![HighlightStyle { - underline: Some(Underline { - thickness: 1.0.into(), + .unindent(), + expected_styles: vec![HighlightStyle { + underline: Some(UnderlineStyle { + thickness: 1.0.into(), + ..Default::default() + }), ..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)); + 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 (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(); + 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 - }); + assert_eq!( + rendered_highlights, expected_highlights, + "wrong highlights for input {blocks:?}" + ); + } + }) + .unwrap(); } #[gpui::test] @@ -1127,7 +1143,7 @@ mod tests { }) .next() .await; - cx.foreground().run_until_parked(); + cx.background_executor.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)); @@ -1236,7 +1252,7 @@ mod tests { ) .next() .await; - cx.foreground().run_until_parked(); + cx.background_executor.run_until_parked(); cx.update_editor(|editor, cx| { update_inlay_link_and_hover_points( @@ -1248,9 +1264,9 @@ mod tests { cx, ); }); - cx.foreground() + cx.background_executor .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); - cx.foreground().run_until_parked(); + cx.background_executor.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()); @@ -1301,9 +1317,9 @@ mod tests { cx, ); }); - cx.foreground() + cx.background_executor .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); - cx.foreground().run_until_parked(); + cx.background_executor.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()); diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 47d8a4cf1f..d7dfa01b21 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/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}; @@ -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, @@ -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(); @@ -579,10 +579,10 @@ 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>, + 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 @@ -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 { @@ -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(); @@ -789,7 +789,7 @@ fn new_update_task( )) .await; - let hint_delay = cx.background().timer(Duration::from_millis( + let hint_delay = cx.background_executor().timer(Duration::from_millis( INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS, )); @@ -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>, @@ -846,7 +846,7 @@ async fn fetch_and_update_hints( invalidate: bool, fetch_range: Range, lsp_request_limiter: Arc, - mut cx: gpui::AsyncAppContext, + mut cx: gpui::AsyncWindowContext, ) -> anyhow::Result<()> { let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() { (None, false) @@ -927,7 +927,7 @@ async fn fetch_and_update_hints( let background_task_buffer_snapshot = buffer_snapshot.clone(); let backround_fetch_range = fetch_range.clone(); let new_update = cx - .background() + .background_executor() .spawn(async move { calculate_hint_updates( query.excerpt_id, @@ -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 @@ -1200,11 +1200,10 @@ pub mod tests { use crate::{ scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount}, - serde_json::json, ExcerptRange, }; use futures::StreamExt; - use gpui::{executor::Deterministic, TestAppContext, ViewHandle}; + use gpui::{Context, TestAppContext, WindowHandle}; use itertools::Itertools; use language::{ language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig, @@ -1212,9 +1211,9 @@ pub mod tests { use lsp::FakeLanguageServer; use parking_lot::Mutex; use project::{FakeFs, Project}; + use serde_json::json; use settings::SettingsStore; use text::{Point, ToPoint}; - use workspace::Workspace; use crate::editor_tests::update_test_language_settings; @@ -1270,10 +1269,10 @@ pub mod tests { }) .next() .await; - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); let mut edits_made = 1; - editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, cx| { let expected_hints = vec!["0".to_string()]; assert_eq!( expected_hints, @@ -1292,13 +1291,13 @@ pub mod tests { ); }); - editor.update(cx, |editor, cx| { + _ = 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| { + cx.executor().run_until_parked(); + _ = editor.update(cx, |editor, cx| { let expected_hints = vec!["0".to_string(), "1".to_string()]; assert_eq!( expected_hints, @@ -1322,8 +1321,8 @@ pub mod tests { .await .expect("inlay refresh request failed"); edits_made += 1; - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { + cx.executor().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, @@ -1380,10 +1379,10 @@ pub mod tests { }) .next() .await; - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); let mut edits_made = 1; - editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, cx| { let expected_hints = vec!["0".to_string()]; assert_eq!( expected_hints, @@ -1405,16 +1404,16 @@ pub mod tests { }) .await .expect("work done progress create request failed"); - cx.foreground().run_until_parked(); + cx.executor().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(); + cx.executor().run_until_parked(); - editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, cx| { let expected_hints = vec!["0".to_string()]; assert_eq!( expected_hints, @@ -1435,10 +1434,10 @@ pub mod tests { lsp::WorkDoneProgressEnd::default(), )), }); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); edits_made += 1; - editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, cx| { let expected_hints = vec!["1".to_string()]; assert_eq!( expected_hints, @@ -1465,7 +1464,7 @@ pub mod tests { }) }); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/a", json!({ @@ -1475,14 +1474,6 @@ pub mod tests { ) .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; @@ -1515,23 +1506,17 @@ pub mod tests { }); } - let _rs_buffer = project + 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(); + cx.executor().run_until_parked(); + cx.executor().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_editor = + cx.add_window(|cx| Editor::for_buffer(rs_buffer, Some(project.clone()), cx)); let rs_lsp_request_count = Arc::new(AtomicU32::new(0)); rs_fake_server .handle_request::(move |params, _| { @@ -1556,8 +1541,8 @@ pub mod tests { }) .next() .await; - cx.foreground().run_until_parked(); - rs_editor.update(cx, |editor, cx| { + cx.executor().run_until_parked(); + _ = rs_editor.update(cx, |editor, cx| { let expected_hints = vec!["0".to_string()]; assert_eq!( expected_hints, @@ -1572,24 +1557,17 @@ pub mod tests { ); }); - cx.foreground().run_until_parked(); - let _md_buffer = project + cx.executor().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(); + cx.executor().run_until_parked(); + cx.executor().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_editor = cx.add_window(|cx| Editor::for_buffer(md_buffer, Some(project), cx)); let md_lsp_request_count = Arc::new(AtomicU32::new(0)); md_fake_server .handle_request::(move |params, _| { @@ -1614,8 +1592,8 @@ pub mod tests { }) .next() .await; - cx.foreground().run_until_parked(); - md_editor.update(cx, |editor, cx| { + cx.executor().run_until_parked(); + _ = md_editor.update(cx, |editor, cx| { let expected_hints = vec!["0".to_string()]; assert_eq!( expected_hints, @@ -1626,12 +1604,12 @@ pub mod tests { assert_eq!(editor.inlay_hint_cache().version, 1); }); - rs_editor.update(cx, |editor, cx| { + _ = 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| { + cx.executor().run_until_parked(); + _ = rs_editor.update(cx, |editor, cx| { let expected_hints = vec!["1".to_string()]; assert_eq!( expected_hints, @@ -1645,7 +1623,7 @@ pub mod tests { "Every time hint cache changes, cache version should be incremented" ); }); - md_editor.update(cx, |editor, cx| { + _ = md_editor.update(cx, |editor, cx| { let expected_hints = vec!["0".to_string()]; assert_eq!( expected_hints, @@ -1656,12 +1634,12 @@ pub mod tests { assert_eq!(editor.inlay_hint_cache().version, 1); }); - md_editor.update(cx, |editor, cx| { + _ = 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| { + cx.executor().run_until_parked(); + _ = md_editor.update(cx, |editor, cx| { let expected_hints = vec!["1".to_string()]; assert_eq!( expected_hints, @@ -1671,7 +1649,7 @@ pub mod tests { assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, 2); }); - rs_editor.update(cx, |editor, cx| { + _ = rs_editor.update(cx, |editor, cx| { let expected_hints = vec!["1".to_string()]; assert_eq!( expected_hints, @@ -1743,10 +1721,10 @@ pub mod tests { }) .next() .await; - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); let mut edits_made = 1; - editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, cx| { assert_eq!( lsp_request_count.load(Ordering::Relaxed), 1, @@ -1780,8 +1758,8 @@ pub mod tests { .request::(()) .await .expect("inlay refresh request failed"); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { + cx.executor().run_until_parked(); + _ = editor.update(cx, |editor, cx| { assert_eq!( lsp_request_count.load(Ordering::Relaxed), 2, @@ -1852,8 +1830,8 @@ pub mod tests { show_other_hints: new_allowed_hint_kinds.contains(&None), }) }); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { + cx.executor().run_until_parked(); + _ = editor.update(cx, |editor, cx| { assert_eq!( lsp_request_count.load(Ordering::Relaxed), 2, @@ -1896,8 +1874,8 @@ pub mod tests { show_other_hints: another_allowed_hint_kinds.contains(&None), }) }); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { + cx.executor().run_until_parked(); + _ = editor.update(cx, |editor, cx| { assert_eq!( lsp_request_count.load(Ordering::Relaxed), 2, @@ -1926,8 +1904,8 @@ pub mod tests { .request::(()) .await .expect("inlay refresh request failed"); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { + cx.executor().run_until_parked(); + _ = editor.update(cx, |editor, cx| { assert_eq!( lsp_request_count.load(Ordering::Relaxed), 2, @@ -1952,8 +1930,8 @@ pub mod tests { show_other_hints: final_allowed_hint_kinds.contains(&None), }) }); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { + cx.executor().run_until_parked(); + _ = editor.update(cx, |editor, cx| { assert_eq!( lsp_request_count.load(Ordering::Relaxed), 3, @@ -1988,8 +1966,8 @@ pub mod tests { .request::(()) .await .expect("inlay refresh request failed"); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { + cx.executor().run_until_parked(); + _ = editor.update(cx, |editor, cx| { assert_eq!( lsp_request_count.load(Ordering::Relaxed), 4, @@ -2056,16 +2034,16 @@ pub mod tests { "initial change #2", "initial change #3", ] { - editor.update(cx, |editor, cx| { + _ = 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(); + cx.executor().run_until_parked(); - editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, cx| { let current_text = editor.text(cx); for change in &expected_changes { assert!( @@ -2099,18 +2077,17 @@ pub mod tests { ] { 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| { + edits.push(cx.spawn(|mut cx| async move { + _ = task_editor.update(&mut 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(); + cx.executor().run_until_parked(); - editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, cx| { let current_text = editor.text(cx); for change in &expected_changes { assert!( @@ -2166,7 +2143,7 @@ pub mod tests { ..Default::default() })) .await; - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/a", json!({ @@ -2177,32 +2154,16 @@ pub mod tests { .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 + 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(); + cx.executor().run_until_parked(); + cx.executor().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 editor = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx)); 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); @@ -2233,13 +2194,16 @@ pub mod tests { }) .next() .await; + fn editor_visible_range( - editor: &ViewHandle, + editor: &WindowHandle, cx: &mut gpui::TestAppContext, ) -> Range { - let ranges = editor.update(cx, |editor, cx| { - editor.excerpts_for_inlay_hints_query(None, cx) - }); + let ranges = editor + .update(cx, |editor, cx| { + editor.excerpts_for_inlay_hints_query(None, cx) + }) + .unwrap(); assert_eq!( ranges.len(), 1, @@ -2262,10 +2226,10 @@ pub mod tests { // 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( + cx.executor().advance_clock(Duration::from_millis( INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, )); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); let initial_visible_range = editor_visible_range(&editor, cx); let lsp_initial_visible_range = lsp::Range::new( lsp::Position::new( @@ -2281,7 +2245,7 @@ pub mod tests { 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| { + _ = 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:?}"); @@ -2308,39 +2272,41 @@ pub mod tests { ); }); - editor.update(cx, |editor, cx| { + _ = 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( + cx.executor().advance_clock(Duration::from_millis( INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, )); - cx.foreground().run_until_parked(); + cx.executor().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!( + let visible_line_count = editor + .update(cx, |editor, _| editor.visible_line_count().unwrap()) + .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!( + assert_eq!( second_scroll.end, lsp::Position::new( visible_range_after_scrolls.end.row @@ -2350,41 +2316,42 @@ pub mod tests { "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 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 - }); + 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 + }) + .unwrap(); - editor.update(cx, |editor, cx| { + _ = 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( + cx.executor().advance_clock(Duration::from_millis( INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, )); - cx.foreground().run_until_parked(); - editor.update(cx, |_, _| { + cx.executor().run_until_parked(); + _ = editor.update(cx, |_, _| { let ranges = lsp_request_ranges .lock() .drain(..) @@ -2394,14 +2361,14 @@ pub mod tests { assert_eq!(lsp_request_count.load(Ordering::Acquire), 4); }); - editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, cx| { editor.handle_input("++++more text++++", cx); }); - cx.foreground().advance_clock(Duration::from_millis( + cx.executor().advance_clock(Duration::from_millis( INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, )); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { + cx.executor().run_until_parked(); + _ = editor.update(cx, |editor, cx| { let mut ranges = lsp_request_ranges.lock().drain(..).collect::>(); ranges.sort_by_key(|r| r.start); @@ -2434,10 +2401,7 @@ pub mod tests { } #[gpui::test(iterations = 10)] - async fn test_multiple_excerpts_large_multibuffer( - deterministic: Arc, - cx: &mut gpui::TestAppContext, - ) { + async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, @@ -2465,26 +2429,21 @@ pub mod tests { })) .await; let language = Arc::new(language); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.background_executor.clone()); 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; + "/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 worktree_id = project.update(cx, |project, cx| { + project.worktrees().next().unwrap().read(cx).id() }); let buffer_1 = project @@ -2499,7 +2458,7 @@ pub mod tests { }) .await .unwrap(); - let multibuffer = cx.add_model(|cx| { + let multibuffer = cx.new_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( buffer_1.clone(), @@ -2564,11 +2523,9 @@ pub mod tests { 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); + cx.executor().run_until_parked(); + let editor = + cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), 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); @@ -2637,25 +2594,27 @@ pub mod tests { }) .next() .await; - cx.foreground().run_until_parked(); + cx.executor().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| { + 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(), + ]; + 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.update(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::Next), cx, |s| { s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) }); @@ -2666,93 +2625,94 @@ pub mod tests { 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"); - }); + cx.executor().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.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( + cx.executor().advance_clock(Duration::from_millis( INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, )); - cx.foreground().run_until_parked(); + cx.executor().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() - }); + 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() + }).unwrap(); - editor.update(cx, |editor, cx| { + _ = 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"); - }); + cx.executor().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.update(cx, |editor, cx| { editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(56, 0)..Point::new(56, 0)]) + // TODO if this gets set to hint boundary (e.g. 56) we sometimes get an extra cache version bump, why? + s.select_ranges([Point::new(57, 0)..Point::new(57, 0)]) }); editor.handle_input("++++more text++++", cx); }); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { + cx.executor().run_until_parked(); + _ = editor.update(cx, |editor, cx| { let expected_hints = vec![ "main hint(edited) #0".to_string(), "main hint(edited) #1".to_string(), @@ -2767,24 +2727,21 @@ pub mod tests { 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" + 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" + assert_eq!( + current_cache_version, + last_scroll_update_version + expected_hints.len(), + "We should have updated cache N times == N of new hints arrived (separately from each excerpt)" ); }); } #[gpui::test] - async fn test_excerpts_removed( - deterministic: Arc, - cx: &mut gpui::TestAppContext, - ) { + async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, @@ -2812,7 +2769,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" })) .await; let language = Arc::new(language); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/a", json!({ @@ -2825,13 +2782,8 @@ all hints should be invalidated and requeried for all of its visible excerpts" 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 worktree_id = project.update(cx, |project, cx| { + project.worktrees().next().unwrap().read(cx).id() }); let buffer_1 = project @@ -2846,7 +2798,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" }) .await .unwrap(); - let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); + let multibuffer = cx.new_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(), @@ -2870,11 +2822,9 @@ all hints should be invalidated and requeried for all of its visible 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); + cx.executor().run_until_parked(); + let editor = + cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), 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); @@ -2942,9 +2892,9 @@ all hints should be invalidated and requeried for all of its visible excerpts" }) .next() .await; - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); - editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, cx| { assert_eq!( vec!["main hint #0".to_string(), "other hint #0".to_string()], cached_hint_labels(editor), @@ -2961,13 +2911,13 @@ all hints should be invalidated and requeried for all of its visible excerpts" ); }); - editor.update(cx, |editor, cx| { + _ = 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| { + cx.executor().run_until_parked(); + _ = editor.update(cx, |editor, cx| { assert_eq!( vec!["main hint #0".to_string()], cached_hint_labels(editor), @@ -2992,8 +2942,8 @@ all hints should be invalidated and requeried for all of its visible excerpts" show_other_hints: true, }) }); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { + cx.executor().run_until_parked(); + _ = editor.update(cx, |editor, cx| { let expected_hints = vec!["main hint #0".to_string()]; assert_eq!( expected_hints, @@ -3041,7 +2991,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" ..Default::default() })) .await; - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/a", json!({ @@ -3052,32 +3002,16 @@ all hints should be invalidated and requeried for all of its visible excerpts" .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 + 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(); + cx.executor().run_until_parked(); + cx.executor().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 editor = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx)); let lsp_request_count = Arc::new(AtomicU32::new(0)); let closure_lsp_request_count = Arc::clone(&lsp_request_count); fake_server @@ -3105,14 +3039,14 @@ all hints should be invalidated and requeried for all of its visible excerpts" .next() .await; - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { + cx.executor().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| { + cx.executor().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)); @@ -3133,10 +3067,10 @@ all hints should be invalidated and requeried for all of its visible excerpts" let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; - editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, cx| { editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) }); - cx.foreground().start_waiting(); + cx.executor().start_waiting(); let lsp_request_count = Arc::new(AtomicU32::new(0)); let closure_lsp_request_count = Arc::clone(&lsp_request_count); fake_server @@ -3163,8 +3097,8 @@ all hints should be invalidated and requeried for all of its visible excerpts" }) .next() .await; - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { + cx.executor().run_until_parked(); + _ = editor.update(cx, |editor, cx| { let expected_hints = vec!["1".to_string()]; assert_eq!( expected_hints, @@ -3179,11 +3113,11 @@ all hints should be invalidated and requeried for all of its visible excerpts" ); }); - editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, cx| { editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) }); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { + cx.executor().run_until_parked(); + _ = editor.update(cx, |editor, cx| { assert!( cached_hint_labels(editor).is_empty(), "Should clear hints after 2nd toggle" @@ -3200,8 +3134,8 @@ all hints should be invalidated and requeried for all of its visible excerpts" show_other_hints: true, }) }); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { + cx.executor().run_until_parked(); + _ = editor.update(cx, |editor, cx| { let expected_hints = vec!["2".to_string()]; assert_eq!( expected_hints, @@ -3212,11 +3146,11 @@ all hints should be invalidated and requeried for all of its visible excerpts" assert_eq!(editor.inlay_hint_cache().version, 3); }); - editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, cx| { editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) }); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { + cx.executor().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" @@ -3225,11 +3159,11 @@ all hints should be invalidated and requeried for all of its visible excerpts" assert_eq!(editor.inlay_hint_cache().version, 4); }); - editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, cx| { editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) }); - cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { + cx.executor().run_until_parked(); + _ = editor.update(cx, |editor, cx| { let expected_hints = vec!["3".to_string()]; assert_eq!( expected_hints, @@ -3242,11 +3176,10 @@ all hints should be invalidated and requeried for all of its visible excerpts" } 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); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + theme::init(theme::LoadThemes::JustBase, cx); client::init_settings(cx); language::init(cx); Project::init_settings(cx); @@ -3259,7 +3192,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" async fn prepare_test_objects( cx: &mut TestAppContext, - ) -> (&'static str, ViewHandle, FakeLanguageServer) { + ) -> (&'static str, WindowHandle, FakeLanguageServer) { let mut language = Language::new( LanguageConfig { name: "Rust".into(), @@ -3278,7 +3211,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" })) .await; - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/a", json!({ @@ -3289,35 +3222,19 @@ all hints should be invalidated and requeried for all of its visible excerpts" .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 + _ = project.update(cx, |project, _| project.languages().add(Arc::new(language))); + 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(); + cx.executor().run_until_parked(); + cx.executor().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 editor = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx)); - editor.update(cx, |editor, cx| { + _ = 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); @@ -3339,7 +3256,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" labels } - pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec { + pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, Editor>) -> Vec { let mut hints = editor .visible_inlay_hints(cx) .into_iter() diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 4c45904c50..31c4e24659 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,16 +1,15 @@ use crate::{ editor_settings::SeedQuerySetting, link_go_to_definition::hide_link_definition, - persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, EditorSettings, Event, + persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, }; -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context as _, 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, + div, point, AnyElement, AppContext, AsyncWindowContext, Context, Entity, EntityId, + EventEmitter, IntoElement, Model, ParentElement, Pixels, Render, SharedString, Styled, + Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use language::{ proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt, @@ -18,27 +17,29 @@ use language::{ }; use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath}; use rpc::proto::{self, update_view, PeerId}; -use smallvec::SmallVec; +use settings::Settings; + +use std::fmt::Write; 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, ItemHandle}; +use theme::{ActiveTheme, Theme}; +use ui::{h_stack, prelude::*, Label}; +use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt}; use workspace::{ - item::{FollowableItem, Item, ItemEvent, ProjectItem}, + item::{BreadcrumbText, FollowEvent, FollowableItemHandle}, + StatusItemView, +}; +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; @@ -49,12 +50,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>>> { + cx: &mut WindowContext, + ) -> Option>>> { let project = workspace.read(cx).project().to_owned(); let Some(proto::view::Variant::Editor(_)) = state else { return None; @@ -80,7 +81,7 @@ impl FollowableItem for Editor { 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 editor = pane.update(&mut cx, |pane, cx| { let mut editors = pane.items_of_type::(); editors.find(|editor| { let ids_match = editor.remote_id(&client, cx) == Some(remote_id); @@ -95,7 +96,7 @@ impl FollowableItem for Editor { editor } else { pane.update(&mut cx, |_, cx| { - let multibuffer = cx.add_model(|cx| { + let multibuffer = cx.new_model(|cx| { let mut multibuffer; if state.singleton && buffers.len() == 1 { multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx) @@ -128,7 +129,7 @@ impl FollowableItem for Editor { multibuffer }); - cx.add_view(|cx| { + cx.new_view(|cx| { let mut editor = Editor::for_multibuffer(multibuffer, Some(project.clone()), cx); editor.remote_id = Some(remote_id); @@ -162,22 +163,20 @@ impl FollowableItem for Editor { self.buffer.update(cx, |buffer, cx| { buffer.remove_active_selections(cx); }); - } else { + } else if self.focus_handle.is_focused(cx) { 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, - ); - } + 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 { + fn to_state_proto(&self, cx: &WindowContext) -> Option { let buffer = self.buffer.read(cx); let scroll_anchor = self.scroll_manager.anchor(); let excerpts = buffer @@ -204,8 +203,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() @@ -220,18 +219,33 @@ impl FollowableItem for Editor { })) } + fn to_follow_event(event: &EditorEvent) -> Option { + match event { + EditorEvent::Edited => Some(FollowEvent::Unfollow), + EditorEvent::SelectionsChanged { local } + | EditorEvent::ScrollPositionChanged { local, .. } => { + if *local { + Some(FollowEvent::Unfollow) + } else { + None + } + } + _ => None, + } + } + fn add_event_to_update_proto( &self, - event: &Self::Event, + event: &EditorEvent, update: &mut Option, - cx: &AppContext, + cx: &WindowContext, ) -> 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 { + EditorEvent::ExcerptsAdded { buffer, predecessor, excerpts, @@ -252,20 +266,20 @@ impl FollowableItem for Editor { } true } - Event::ExcerptsRemoved { ids } => { + EditorEvent::ExcerptsRemoved { ids } => { update .deleted_excerpts .extend(ids.iter().map(ExcerptId::to_proto)); true } - Event::ScrollPositionChanged { .. } => { + EditorEvent::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 { .. } => { + EditorEvent::SelectionsChanged { .. } => { update.selections = self .selections .disjoint_anchors() @@ -286,7 +300,7 @@ impl FollowableItem for Editor { fn apply_update_proto( &mut self, - project: &ModelHandle, + project: &Model, message: update_view::Variant, cx: &mut ViewContext, ) -> Task> { @@ -297,25 +311,16 @@ impl FollowableItem for Editor { }) } - 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 { + fn is_project_item(&self, _cx: &WindowContext) -> bool { true } } async fn update_editor_from_message( - this: WeakViewHandle, - project: ModelHandle, + this: WeakView, + project: Model, message: proto::update_view::Editor, - cx: &mut AsyncAppContext, + cx: &mut AsyncWindowContext, ) -> Result<()> { // Open all of the buffers of which excerpts were added to the editor. let inserted_excerpt_buffer_ids = message @@ -328,7 +333,7 @@ async fn update_editor_from_message( .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. @@ -353,7 +358,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 +435,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, ); @@ -516,6 +521,8 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor) } impl Item for Editor { + type Event = EditorEvent; + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { if let Ok(data) = data.downcast::() { let newest_selection = self.selections.newest::(cx); @@ -551,7 +558,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) @@ -566,53 +573,66 @@ 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()), - 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, - 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(), - ) + fn tab_content(&self, detail: Option, selected: bool, cx: &WindowContext) -> AnyElement { + let _theme = cx.theme(); + + let description = detail.and_then(|detail| { + let path = path_for_buffer(&self.buffer, detail, false, cx)?; + let description = path.to_string_lossy(); + let description = description.trim(); + + if description.is_empty() { + return None; + } + + Some(util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN)) + }); + + h_stack() + .gap_2() + .child(Label::new(self.title(cx).to_string()).color(if selected { + Color::Default + } else { + Color::Muted })) - .align_children_center() - .into_any() + .when_some(description, |this, description| { + this.child( + Label::new(description) + .size(LabelSize::XSmall) + .color(Color::Muted), + ) + }) + .into_any_element() } - 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 { 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, { - Some(self.clone(cx)) + Some(cx.new_view(|cx| self.clone(cx))) } fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { @@ -646,11 +666,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(); @@ -659,28 +675,34 @@ 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| { - let version = buffer.saved_version().clone(); - let fingerprint = buffer.saved_version_fingerprint(); - let mtime = buffer.saved_mtime(); - buffer.did_save(version, fingerprint, mtime, cx); - }); + 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(); } } @@ -690,7 +712,7 @@ impl Item for Editor { fn save_as( &mut self, - project: ModelHandle, + project: Model, abs_path: PathBuf, cx: &mut ViewContext, ) -> Task> { @@ -710,11 +732,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 = @@ -724,60 +742,36 @@ impl Item for Editor { 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); + buffer + .update(&mut cx, |buffer, cx| { + if let Some(transaction) = transaction { + if !buffer.is_singleton() { + buffer.push_transaction(&transaction.0, cx); + } } - } - }); + }) + .ok(); 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> { + 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 } fn breadcrumb_location(&self) -> ToolbarItemLocation { - ToolbarItemLocation::PrimaryLeft { flex: None } + ToolbarItemLocation::PrimaryLeft } - fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option> { + fn breadcrumbs(&self, variant: &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)?; + multibuffer.symbols_containing(cursor, Some(&variant.syntax()), cx)?; let buffer = multibuffer.buffer(buffer_id)?; let buffer = buffer.read(cx); @@ -806,11 +800,11 @@ 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().item_id().as_u64() as ItemId; self.workspace = Some((workspace.weak_handle(), workspace.database_id())); fn serialize( - buffer: ModelHandle, + buffer: Model, workspace_id: WorkspaceId, item_id: ItemId, cx: &mut AppContext, @@ -818,7 +812,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 @@ -834,7 +828,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().item_id().as_u64() as ItemId, + cx, + ); } } }) @@ -846,13 +845,47 @@ impl Item for Editor { Some("Editor") } + fn to_item_events(event: &EditorEvent, mut f: impl FnMut(ItemEvent)) { + match event { + EditorEvent::Closed => f(ItemEvent::CloseItem), + + EditorEvent::Saved | EditorEvent::TitleChanged => { + f(ItemEvent::UpdateTab); + f(ItemEvent::UpdateBreadcrumbs); + } + + EditorEvent::Reparsed => { + f(ItemEvent::UpdateBreadcrumbs); + } + + EditorEvent::SelectionsChanged { local } if *local => { + f(ItemEvent::UpdateBreadcrumbs); + } + + EditorEvent::DirtyChanged => { + f(ItemEvent::UpdateTab); + } + + EditorEvent::BufferEdited => { + f(ItemEvent::Edit); + f(ItemEvent::UpdateBreadcrumbs); + } + + EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => { + f(ItemEvent::Edit); + } + + _ => {} + } + } + 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 @@ -876,10 +909,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.new_view(|cx| { let mut editor = Editor::for_buffer(buffer, Some(project), cx); + editor.read_scroll_position_from_db(item_id, workspace_id, cx); editor }) @@ -894,36 +928,20 @@ 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) } } +impl EventEmitter for Editor {} + 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); } @@ -931,13 +949,13 @@ impl SearchableItem for Editor { fn update_matches(&mut self, matches: Vec>, cx: &mut ViewContext) { self.highlight_background::( matches, - |theme| theme.search.match_background, + |theme| theme.search_match_background, cx, ); } fn query_suggestion(&mut self, cx: &mut ViewContext) -> String { - let setting = settings::get::(cx).seed_search_query_from_cursor; + let setting = EditorSettings::get_global(cx).seed_search_query_from_cursor; let snapshot = &self.snapshot(cx).buffer_snapshot; let selection = self.selections.newest::(cx); @@ -1060,7 +1078,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( @@ -1153,7 +1171,7 @@ impl CursorPosition { } } - fn update_position(&mut self, editor: ViewHandle, cx: &mut ViewContext) { + fn update_position(&mut self, editor: View, cx: &mut ViewContext) { let editor = editor.read(cx); let buffer = editor.buffer().read(cx).snapshot(cx); @@ -1174,18 +1192,9 @@ impl CursorPosition { } } -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; +impl Render for CursorPosition { + fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { + div().when_some(self.position, |el, position| { let mut text = format!( "{}{FILE_ROW_COLUMN_DELIMITER}{}", position.row + 1, @@ -1194,10 +1203,9 @@ impl View for CursorPosition { 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() - } + + el.child(Label::new(text).size(LabelSize::Small)) + }) } } @@ -1220,7 +1228,7 @@ impl StatusItemView for CursorPosition { } fn path_for_buffer<'a>( - buffer: &ModelHandle, + buffer: &Model, height: usize, include_filename: bool, cx: &'a AppContext, diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 7da0b88622..42f502daed 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -2,9 +2,10 @@ 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 gpui::{px, Task, ViewContext}; use language::{Bias, ToOffset}; use lsp::LanguageServerId; use project::{ @@ -12,6 +13,7 @@ use project::{ ResolveState, }; use std::ops::Range; +use theme::ActiveTheme as _; use util::TryFutureExt; #[derive(Debug, Default)] @@ -168,7 +170,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)) @@ -396,8 +398,8 @@ pub fn show_link_definition( let result = match &trigger_point { TriggerPoint::Text(_) => { // query the LSP for definition info - cx.update(|cx| { - project.update(cx, |project, cx| match definition_kind { + project + .update(&mut cx, |project, cx| match definition_kind { LinkDefinitionKind::Symbol => { project.definition(&buffer, buffer_position, cx) } @@ -405,29 +407,30 @@ pub fn show_link_definition( 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(), + ) }) - }) - .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())), @@ -483,8 +486,14 @@ 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 style = gpui::HighlightStyle { + underline: Some(gpui::UnderlineStyle { + thickness: px(1.), + ..Default::default() + }), + color: Some(cx.theme().colors().link_text_hover), + ..Default::default() + }; let highlight_range = symbol_range.unwrap_or_else(|| match &trigger_point { TriggerPoint::Text(trigger_anchor) => { @@ -575,8 +584,8 @@ fn go_to_fetched_definition_of_kind( let is_correct_kind = cached_definitions_kind == Some(kind); if !cached_definitions.is_empty() && is_correct_kind { - if !editor.focused { - cx.focus_self(); + if !editor.focus_handle.is_focused(cx) { + cx.focus(&editor.focus_handle); } editor.navigate_to_definitions(cached_definitions, split, cx); @@ -592,8 +601,8 @@ fn go_to_fetched_definition_of_kind( 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), + LinkDefinitionKind::Symbol => editor.go_to_definition(&GoToDefinition, cx), + LinkDefinitionKind::Type => editor.go_to_type_definition(&GoToTypeDefinition, cx), } } } @@ -609,10 +618,7 @@ mod tests { test::editor_lsp_test_context::EditorLspTestContext, }; use futures::StreamExt; - use gpui::{ - platform::{self, Modifiers, ModifiersChangedEvent}, - View, - }; + use gpui::{Modifiers, ModifiersChangedEvent}; use indoc::indoc; use language::language_settings::InlayHintSettings; use lsp::request::{GotoDefinition, GotoTypeDefinition}; @@ -674,7 +680,7 @@ mod tests { ); }); requests.next().await; - cx.foreground().run_until_parked(); + cx.background_executor.run_until_parked(); cx.assert_editor_text_highlights::(indoc! {" struct A; let «variable» = A; @@ -682,10 +688,11 @@ mod tests { // Unpress shift causes highlight to go away (normal goto-definition is not valid here) cx.update_editor(|editor, cx| { - editor.modifiers_changed( - &platform::ModifiersChangedEvent { + crate::element::EditorElement::modifiers_changed( + editor, + &ModifiersChangedEvent { modifiers: Modifiers { - cmd: true, + command: true, ..Default::default() }, ..Default::default() @@ -725,7 +732,7 @@ mod tests { go_to_fetched_type_definition(editor, PointForPosition::valid(hover_point), false, cx); }); requests.next().await; - cx.foreground().run_until_parked(); + cx.background_executor.run_until_parked(); cx.assert_editor_state(indoc! {" struct «Aˇ»; @@ -747,23 +754,23 @@ mod tests { .await; cx.set_state(indoc! {" - fn ˇtest() { do_work(); } - fn do_work() { test(); } - "}); + 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(); } - "}); + fn test() { do_wˇork(); } + fn do_work() { test(); } + "}); let symbol_range = cx.lsp_range(indoc! {" - fn test() { «do_work»(); } - fn do_work() { test(); } - "}); + fn test() { «do_work»(); } + fn do_work() { test(); } + "}); let target_range = cx.lsp_range(indoc! {" - fn test() { do_work(); } - fn «do_work»() { test(); } - "}); + fn test() { do_work(); } + fn «do_work»() { test(); } + "}); let mut requests = cx.handle_request::(move |url, _, _| async move { Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ @@ -786,22 +793,22 @@ mod tests { ); }); requests.next().await; - cx.foreground().run_until_parked(); + cx.background_executor.run_until_parked(); cx.assert_editor_text_highlights::(indoc! {" - fn test() { «do_work»(); } - fn do_work() { test(); } - "}); + 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); + crate::element::EditorElement::modifiers_changed(editor, &Default::default(), cx); }); // Assert no link highlights cx.assert_editor_text_highlights::(indoc! {" - fn test() { do_work(); } - fn do_work() { test(); } - "}); + 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); @@ -826,18 +833,18 @@ mod tests { ); }); requests.next().await; - cx.foreground().run_until_parked(); + cx.background_executor.run_until_parked(); cx.assert_editor_text_highlights::(indoc! {" - fn test() { «do_work»(); } - fn do_work() { test(); } - "}); + 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(); } - "}); + fˇn test() { do_work(); } + fn do_work() { test(); } + "}); let mut requests = cx .lsp .handle_request::(move |_, _| async move { @@ -854,19 +861,19 @@ mod tests { ); }); requests.next().await; - cx.foreground().run_until_parked(); + cx.background_executor.run_until_parked(); // Assert no link highlights cx.assert_editor_text_highlights::(indoc! {" - fn test() { do_work(); } - fn do_work() { test(); } - "}); + 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(); } - "}); + fn test() { do_work(); } + fn do_work() { teˇst(); } + "}); cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, @@ -876,22 +883,22 @@ mod tests { cx, ); }); - cx.foreground().run_until_parked(); + cx.background_executor.run_until_parked(); // Assert no link highlights cx.assert_editor_text_highlights::(indoc! {" - fn test() { do_work(); } - fn do_work() { test(); } - "}); + fn test() { do_work(); } + fn do_work() { test(); } + "}); let symbol_range = cx.lsp_range(indoc! {" - fn test() { do_work(); } - fn do_work() { «test»(); } - "}); + fn test() { do_work(); } + fn do_work() { «test»(); } + "}); let target_range = cx.lsp_range(indoc! {" - fn «test»() { do_work(); } - fn do_work() { test(); } - "}); + fn «test»() { do_work(); } + fn do_work() { test(); } + "}); let mut requests = cx.handle_request::(move |url, _, _| async move { Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ @@ -904,10 +911,11 @@ mod tests { ]))) }); cx.update_editor(|editor, cx| { - editor.modifiers_changed( + crate::element::EditorElement::modifiers_changed( + editor, &ModifiersChangedEvent { modifiers: Modifiers { - cmd: true, + command: true, ..Default::default() }, }, @@ -915,21 +923,21 @@ mod tests { ); }); requests.next().await; - cx.foreground().run_until_parked(); + cx.background_executor.run_until_parked(); cx.assert_editor_text_highlights::(indoc! {" - fn test() { do_work(); } - fn do_work() { «test»(); } - "}); + 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); + workspace.on_window_activation_changed(cx); }); cx.assert_editor_text_highlights::(indoc! {" - fn test() { do_work(); } - fn do_work() { test(); } - "}); + fn test() { do_work(); } + fn do_work() { test(); } + "}); // Moving the mouse restores the highlights. cx.update_editor(|editor, cx| { @@ -941,17 +949,17 @@ mod tests { cx, ); }); - cx.foreground().run_until_parked(); + cx.background_executor.run_until_parked(); cx.assert_editor_text_highlights::(indoc! {" - fn test() { do_work(); } - fn do_work() { «test»(); } - "}); + 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(); } - "}); + fn test() { do_work(); } + fn do_work() { tesˇt(); } + "}); cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, @@ -961,11 +969,11 @@ mod tests { cx, ); }); - cx.foreground().run_until_parked(); + cx.background_executor.run_until_parked(); cx.assert_editor_text_highlights::(indoc! {" - fn test() { do_work(); } - fn do_work() { «test»(); } - "}); + 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| { @@ -978,27 +986,27 @@ mod tests { // the cached location instead Ok(Some(lsp::GotoDefinitionResponse::Link(vec![]))) }); - cx.foreground().run_until_parked(); + cx.background_executor.run_until_parked(); cx.assert_editor_state(indoc! {" - fn «testˇ»() { do_work(); } - fn do_work() { test(); } - "}); + 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(); } - "}); + 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(); } - "}); + fn test() { do_wˇork(); } + fn do_work() { test(); } + "}); let target_range = cx.lsp_range(indoc! {" - fn test() { do_work(); } - fn «do_work»() { test(); } - "}); + fn test() { do_work(); } + fn «do_work»() { test(); } + "}); let mut requests = cx.handle_request::(move |url, _, _| async move { Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ @@ -1014,22 +1022,22 @@ mod tests { go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx); }); requests.next().await; - cx.foreground().run_until_parked(); + cx.background_executor.run_until_parked(); cx.assert_editor_state(indoc! {" - fn test() { do_work(); } - fn «do_workˇ»() { test(); } - "}); + 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(); } - "}); + fn test() { do_wˇork(); } + fn do_work() { test(); } + "}); let target_range = cx.lsp_range(indoc! {" - fn test() { do_work(); } - fn «do_work»() { test(); } - "}); + 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 { @@ -1043,9 +1051,9 @@ mod tests { // create a pending selection let selection_range = cx.ranges(indoc! {" - fn «test() { do_w»ork(); } - fn do_work() { test(); } - "})[0] + fn «test() { do_w»ork(); } + fn do_work() { test(); } + "})[0] .clone(); cx.update_editor(|editor, cx| { let snapshot = editor.buffer().read(cx).snapshot(cx); @@ -1064,13 +1072,13 @@ mod tests { cx, ); }); - cx.foreground().run_until_parked(); + cx.background_executor.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(); + fn test() { do_work(); } + fn do_work() { test(); } + "}); + cx.background_executor.run_until_parked(); } #[gpui::test] @@ -1093,28 +1101,28 @@ mod tests { ) .await; cx.set_state(indoc! {" - struct TestStruct; + struct TestStruct; - fn main() { - let variableˇ = TestStruct; - } - "}); + fn main() { + let variableˇ = TestStruct; + } + "}); let hint_start_offset = cx.ranges(indoc! {" - struct TestStruct; + struct TestStruct; - fn main() { - let variableˇ = TestStruct; - } - "})[0] + 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»; + struct «TestStruct»; - fn main() { - let variable = TestStruct; - } - "}); + fn main() { + let variable = TestStruct; + } + "}); let expected_uri = cx.buffer_lsp_url.clone(); let hint_label = ": TestStruct"; @@ -1144,7 +1152,7 @@ mod tests { }) .next() .await; - cx.foreground().run_until_parked(); + cx.background_executor.run_until_parked(); cx.update_editor(|editor, cx| { let expected_layers = vec![hint_label.to_string()]; assert_eq!(expected_layers, cached_hint_labels(editor)); @@ -1153,12 +1161,12 @@ mod tests { let inlay_range = cx .ranges(indoc! {" - struct TestStruct; + struct TestStruct; - fn main() { - let variable« »= TestStruct; - } - "}) + fn main() { + let variable« »= TestStruct; + } + "}) .get(0) .cloned() .unwrap(); @@ -1190,7 +1198,7 @@ mod tests { cx, ); }); - cx.foreground().run_until_parked(); + cx.background_executor.run_until_parked(); cx.update_editor(|editor, cx| { let snapshot = editor.snapshot(cx); let actual_highlights = snapshot @@ -1210,10 +1218,11 @@ mod tests { // Unpress cmd causes highlight to go away cx.update_editor(|editor, cx| { - editor.modifiers_changed( - &platform::ModifiersChangedEvent { + crate::element::EditorElement::modifiers_changed( + editor, + &ModifiersChangedEvent { modifiers: Modifiers { - cmd: false, + command: false, ..Default::default() }, ..Default::default() @@ -1223,21 +1232,22 @@ mod tests { }); // 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(); + 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:?}"); - }); + 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 { + crate::element::EditorElement::modifiers_changed( + editor, + &ModifiersChangedEvent { modifiers: Modifiers { - cmd: true, + command: true, ..Default::default() }, ..Default::default() @@ -1253,17 +1263,17 @@ mod tests { cx, ); }); - cx.foreground().run_until_parked(); + cx.background_executor.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.background_executor.run_until_parked(); cx.assert_editor_state(indoc! {" - struct «TestStructˇ»; + struct «TestStructˇ»; - fn main() { - let variable = TestStruct; - } - "}); + fn main() { + let variable = TestStruct; + } + "}); } } diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index 8dfdcdff53..24f3b22a5c 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -2,17 +2,22 @@ use crate::{ DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition, Rename, RevealInFinder, SelectMode, ToggleCodeActions, }; -use context_menu::ContextMenuItem; -use gpui::{elements::AnchorCorner, geometry::vector::Vector2F, ViewContext}; +use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext}; + +pub struct MouseContextMenu { + pub(crate) position: Point, + pub(crate) context_menu: View, + _subscription: Subscription, +} pub fn deploy_context_menu( editor: &mut Editor, - position: Vector2F, + position: Point, point: DisplayPoint, cx: &mut ViewContext, ) { - if !editor.focused { - cx.focus_self(); + if !editor.is_focused(cx) { + editor.focus(cx); } // Don't show context menu for inline editors @@ -31,26 +36,34 @@ 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, - ); + let context_menu = ui::ContextMenu::build(cx, |menu, _cx| { + menu.action("Rename Symbol", Box::new(Rename)) + .action("Go to Definition", Box::new(GoToDefinition)) + .action("Go to Type Definition", Box::new(GoToTypeDefinition)) + .action("Find All References", Box::new(FindAllReferences)) + .action( + "Code Actions", + Box::new(ToggleCodeActions { + deployed_from_indicator: false, + }), + ) + .separator() + .action("Reveal in Finder", Box::new(RevealInFinder)) + }); + let context_menu_focus = context_menu.focus_handle(cx); + cx.focus(&context_menu_focus); + + let _subscription = cx.subscribe(&context_menu, move |this, _, _event: &DismissEvent, cx| { + this.mouse_context_menu.take(); + if context_menu_focus.contains_focused(cx) { + this.focus(cx); + } + }); + + editor.mouse_context_menu = Some(MouseContextMenu { + position, + context_menu, + _subscription, }); cx.notify(); } @@ -84,6 +97,7 @@ mod tests { do_wˇork(); } "}); + cx.editor(|editor, _app| assert!(editor.mouse_context_menu.is_none())); cx.update_editor(|editor, cx| deploy_context_menu(editor, Default::default(), point, cx)); cx.assert_editor_state(indoc! {" @@ -91,6 +105,6 @@ mod tests { do_wˇork(); } "}); - cx.editor(|editor, app| assert!(editor.mouse_context_menu.read(app).visible())); + cx.editor(|editor, _app| assert!(editor.mouse_context_menu.is_some())); } } diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 332eb3c1c5..cfccec253f 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -1,7 +1,8 @@ use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint}; -use gpui::{FontCache, TextLayoutCache}; +use gpui::{px, Pixels, TextSystem}; use language::Point; + use std::{ops::Range, sync::Arc}; #[derive(Debug, PartialEq)] @@ -13,9 +14,9 @@ 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: Arc, pub editor_style: EditorStyle, + pub rem_size: Pixels, } pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { @@ -94,10 +95,10 @@ 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, - _ => map.x_for_point(start, text_layout_details), + 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_display_point(start, text_layout_details), }; let prev_row = start.row().saturating_sub(row_count); @@ -106,19 +107,22 @@ pub fn up_by_rows( Bias::Left, ); if point.row() < start.row() { - *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details) + *point.column_mut() = map.display_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; + 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( @@ -130,28 +134,31 @@ 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, - _ => map.x_for_point(start, text_layout_details), + SelectionGoal::HorizontalPosition(x) => x.into(), + SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(), + SelectionGoal::HorizontalRange { end, .. } => end.into(), + _ => map.x_for_display_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) + *point.column_mut() = map.display_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) + goal_x = map.x_for_display_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)) + ( + clipped_point, + SelectionGoal::HorizontalPosition(goal_x.into()), + ) } pub fn line_beginning( @@ -453,6 +460,7 @@ mod tests { test::{editor_test_context::EditorTestContext, marked_display_snapshot}, Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer, }; + use gpui::{font, Context as _}; use project::Project; use settings::SettingsStore; use util::post_inc; @@ -563,19 +571,12 @@ mod tests { 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 font = font("Helvetica"); + let font_size = px(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)); + cx.new_model(|cx| DisplayMap::new(buffer, font, 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; @@ -756,22 +757,15 @@ mod tests { let mut cx = EditorTestContext::new(cx).await; let editor = cx.editor.clone(); let window = cx.window.clone(); - cx.update_window(window, |cx| { + _ = cx.update_window(window, |_, cx| { let text_layout_details = - editor.read_with(cx, |editor, cx| editor.text_layout_details(cx)); + editor.update(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 font = font("Helvetica"); let buffer = - cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn")); - let multibuffer = cx.add_model(|cx| { + cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abc\ndefg\nhijkl\nmn")); + let multibuffer = cx.new_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( buffer.clone(), @@ -790,19 +784,20 @@ mod tests { multibuffer }); let display_map = - cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx)); + cx.new_model(|cx| DisplayMap::new(multibuffer, font, px(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); + let col_2_x = + snapshot.x_for_display_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), + SelectionGoal::HorizontalPosition(col_2_x.0), false, &text_layout_details ), @@ -825,67 +820,70 @@ mod tests { ), ); - let col_4_x = snapshot.x_for_point(DisplayPoint::new(3, 4), &text_layout_details); + let col_4_x = + snapshot.x_for_display_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), + SelectionGoal::HorizontalPosition(col_4_x.0), false, &text_layout_details ), ( DisplayPoint::new(2, 3), - SelectionGoal::HorizontalPosition(col_4_x) + SelectionGoal::HorizontalPosition(col_4_x.0) ), ); assert_eq!( down( &snapshot, DisplayPoint::new(2, 3), - SelectionGoal::HorizontalPosition(col_4_x), + SelectionGoal::HorizontalPosition(col_4_x.0), false, &text_layout_details ), ( DisplayPoint::new(3, 4), - SelectionGoal::HorizontalPosition(col_4_x) + SelectionGoal::HorizontalPosition(col_4_x.0) ), ); - let col_5_x = snapshot.x_for_point(DisplayPoint::new(6, 5), &text_layout_details); + let col_5_x = + snapshot.x_for_display_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), + SelectionGoal::HorizontalPosition(col_5_x.0), false, &text_layout_details ), ( DisplayPoint::new(3, 4), - SelectionGoal::HorizontalPosition(col_5_x) + SelectionGoal::HorizontalPosition(col_5_x.0) ), ); assert_eq!( down( &snapshot, DisplayPoint::new(3, 4), - SelectionGoal::HorizontalPosition(col_5_x), + SelectionGoal::HorizontalPosition(col_5_x.0), false, &text_layout_details ), ( DisplayPoint::new(6, 5), - SelectionGoal::HorizontalPosition(col_5_x) + SelectionGoal::HorizontalPosition(col_5_x.0) ), ); - let max_point_x = snapshot.x_for_point(DisplayPoint::new(7, 2), &text_layout_details); + let max_point_x = + snapshot.x_for_display_point(DisplayPoint::new(7, 2), &text_layout_details); // Can't move down off the end assert_eq!( @@ -898,28 +896,29 @@ mod tests { ), ( DisplayPoint::new(7, 2), - SelectionGoal::HorizontalPosition(max_point_x) + SelectionGoal::HorizontalPosition(max_point_x.0) ), ); assert_eq!( down( &snapshot, DisplayPoint::new(7, 2), - SelectionGoal::HorizontalPosition(max_point_x), + SelectionGoal::HorizontalPosition(max_point_x.0), false, &text_layout_details ), ( DisplayPoint::new(7, 2), - SelectionGoal::HorizontalPosition(max_point_x) + SelectionGoal::HorizontalPosition(max_point_x.0) ), ); }); } fn init_test(cx: &mut gpui::AppContext) { - cx.set_global(SettingsStore::test(cx)); - theme::init((), cx); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + theme::init(theme::LoadThemes::JustBase, cx); language::init(cx); crate::init(cx); Project::init_settings(cx); diff --git a/crates/editor/src/rust_analyzer_ext.rs b/crates/editor/src/rust_analyzer_ext.rs index 197215ddaa..067d09d9ce 100644 --- a/crates/editor/src/rust_analyzer_ext.rs +++ b/crates/editor/src/rust_analyzer_ext.rs @@ -1,31 +1,50 @@ use std::sync::Arc; use anyhow::Context as _; -use gpui::{AppContext, Task, ViewContext}; +use gpui::{Context, View, ViewContext, VisualContext, WindowContext}; use language::Language; use multi_buffer::MultiBuffer; use project::lsp_ext_command::ExpandMacro; use text::ToPointUtf16; -use crate::{Editor, ExpandMacroRecursively}; +use crate::{element::register_action, Editor, ExpandMacroRecursively}; -pub fn apply_related_actions(cx: &mut AppContext) { - cx.add_async_action(expand_macro_recursively); +pub fn apply_related_actions(editor: &View, cx: &mut WindowContext) { + let is_rust_related = editor.update(cx, |editor, cx| { + editor + .buffer() + .read(cx) + .all_buffers() + .iter() + .any(|b| match b.read(cx).language() { + Some(l) => is_rust_language(l), + None => false, + }) + }); + + if is_rust_related { + register_action(editor, cx, expand_macro_recursively); + } } pub fn expand_macro_recursively( editor: &mut Editor, _: &ExpandMacroRecursively, - cx: &mut ViewContext<'_, '_, Editor>, -) -> Option>> { + cx: &mut ViewContext<'_, Editor>, +) { if editor.selections.count() == 0 { - return None; + return; } - let project = editor.project.as_ref()?; - let workspace = editor.workspace(cx)?; + let Some(project) = &editor.project else { + return; + }; + let Some(workspace) = editor.workspace() else { + return; + }; + let multibuffer = editor.buffer().read(cx); - let (trigger_anchor, rust_language, server_to_query, buffer) = editor + let Some((trigger_anchor, rust_language, server_to_query, buffer)) = editor .selections .disjoint_anchors() .into_iter() @@ -56,7 +75,10 @@ pub fn expand_macro_recursively( None } }) - })?; + }) + else { + return; + }; let project = project.clone(); let buffer_snapshot = buffer.read(cx).snapshot(); @@ -69,7 +91,7 @@ pub fn expand_macro_recursively( cx, ) }); - Some(cx.spawn(|_, mut cx| async move { + cx.spawn(|_editor, mut cx| async move { let macro_expansion = expand_macro_task.await.context("expand macro")?; if macro_expansion.is_empty() { log::info!("Empty macro expansion for position {position:?}"); @@ -78,19 +100,18 @@ pub fn expand_macro_recursively( let buffer = project.update(&mut cx, |project, cx| { project.create_buffer(¯o_expansion.expansion, Some(rust_language), cx) - })?; + })??; workspace.update(&mut cx, |workspace, cx| { - let buffer = cx.add_model(|cx| { + let buffer = cx.new_model(|cx| { MultiBuffer::singleton(buffer, cx).with_title(macro_expansion.name) }); workspace.add_item( - Box::new(cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))), + Box::new(cx.new_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))), cx, ); - }); - - anyhow::Ok(()) - })) + }) + }) + .detach_and_log_err(cx); } fn is_rust_language(language: &Language) -> bool { diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index 8233f92a1a..0798870f76 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -2,26 +2,21 @@ 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, + Anchor, DisplayPoint, Editor, EditorEvent, EditorMode, InlayHintRefreshReason, + MultiBufferSnapshot, ToPoint, }; +use gpui::{point, px, AppContext, Entity, Pixels, Task, ViewContext}; +use language::{Bias, Point}; +use std::{ + cmp::Ordering, + time::{Duration, Instant}, +}; +use util::ResultExt; +use workspace::{ItemId, WorkspaceId}; use self::{ autoscroll::{Autoscroll, AutoscrollStrategy}, @@ -37,25 +32,25 @@ 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: gpui::Point::default(), anchor: Anchor::min(), } } - pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F { + 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 } @@ -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,13 +80,13 @@ impl OngoingScroll { } } - pub fn filter(&self, delta: &mut Vector2F) -> Option { + 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(); - 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 @@ -114,8 +115,12 @@ 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(px(0.), delta.y); + } + Some(Axis::Horizontal) => { + *delta = point(delta.x, px(0.)); + } None => {} } @@ -128,9 +133,10 @@ 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>, + dragging_scrollbar: bool, visible_line_count: Option, } @@ -143,6 +149,7 @@ impl ScrollManager { autoscroll_request: None, show_scrollbars: true, hide_scrollbar_task: None, + dragging_scrollbar: false, last_autoscroll: None, visible_line_count: None, } @@ -166,30 +173,30 @@ impl ScrollManager { self.ongoing.axis = axis; } - pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F { + pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point { self.anchor.scroll_position(snapshot) } fn set_scroll_position( &mut self, - scroll_position: Vector2F, + 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(), - offset: scroll_position.max(vec2f(0., 0.)), + offset: scroll_position.max(&gpui::Point::default()), }, 0, ) } 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); @@ -197,9 +204,9 @@ impl ScrollManager { ( ScrollAnchor { anchor: top_anchor, - offset: vec2f( - scroll_position.x(), - scroll_position.y() - top_anchor.to_display_point(&map).row() as f32, + offset: point( + scroll_position.x, + scroll_position.y - top_anchor.to_display_point(&map).row() as f32, ), }, scroll_top_buffer_point.row, @@ -219,20 +226,20 @@ impl ScrollManager { cx: &mut ViewContext, ) { self.anchor = anchor; - cx.emit(Event::ScrollPositionChanged { local, autoscroll }); + cx.emit(EditorEvent::ScrollPositionChanged { local, autoscroll }); 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().as_u64() as ItemId; - cx.background() + cx.foreground_executor() .spawn(async move { DB.save_scroll_position( item_id, workspace_id, top_row, - anchor.offset.x(), - anchor.offset.y(), + anchor.offset.x, + anchor.offset.y, ) .await .log_err() @@ -250,7 +257,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; @@ -271,9 +280,20 @@ impl ScrollManager { self.autoscroll_request.is_some() } + pub fn is_dragging_scrollbar(&self) -> bool { + self.dragging_scrollbar + } + + pub fn set_is_dragging_scrollbar(&mut self, dragging: bool, cx: &mut ViewContext) { + if dragging != self.dragging_scrollbar { + self.dragging_scrollbar = dragging; + cx.notify(); + } + } + 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 @@ -310,13 +330,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: 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: Vector2F, + scroll_position: gpui::Point, local: bool, autoscroll: bool, cx: &mut ViewContext, @@ -337,7 +361,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) -> gpui::Point { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); self.scroll_manager.anchor.scroll_position(&display_map) } @@ -370,7 +394,7 @@ impl Editor { pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext) { if matches!(self.mode, EditorMode::SingleLine) { - cx.propagate_action(); + cx.propagate(); return; } @@ -379,7 +403,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); } @@ -415,7 +439,7 @@ impl Editor { pub fn read_scroll_position_from_db( &mut self, - item_id: usize, + item_id: u64, workspace_id: WorkspaceId, cx: &mut ViewContext, ) { @@ -427,7 +451,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: gpui::Point::new(x, y), anchor: top_anchor, }; self.set_scroll_anchor(scroll_anchor, cx); diff --git a/crates/editor/src/scroll/actions.rs b/crates/editor/src/scroll/actions.rs index 82c2e10589..21a4258f6f 100644 --- a/crates/editor/src/scroll/actions.rs +++ b/crates/editor/src/scroll/actions.rs @@ -1,72 +1,31 @@ -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) - }); -} +use super::Axis; +use crate::{ + Autoscroll, Bias, Editor, EditorMode, NextScreen, ScrollAnchor, ScrollCursorBottom, + ScrollCursorCenter, ScrollCursorTop, +}; +use gpui::{Point, ViewContext}; impl Editor { - pub fn next_screen(&mut self, _: &NextScreen, cx: &mut ViewContext) -> Option<()> { + pub fn next_screen(&mut self, _: &NextScreen, cx: &mut ViewContext) { if self.take_rename(true, cx).is_some() { - return None; + return; } - if self.mouse_context_menu.read(cx).visible() { - return None; - } + // todo!() + // if self.mouse_context_menu.read(cx).visible() { + // return None; + // } if matches!(self.mode, EditorMode::SingleLine) { - cx.propagate_action(); - return None; + cx.propagate(); + return; } self.request_autoscroll(Autoscroll::Next, cx); - Some(()) } pub fn scroll( &mut self, - scroll_position: Vector2F, + scroll_position: Point, axis: Option, cx: &mut ViewContext, ) { @@ -74,17 +33,17 @@ impl Editor { 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; + pub fn scroll_cursor_top(&mut self, _: &ScrollCursorTop, cx: &mut ViewContext) { + let snapshot = self.snapshot(cx).display_snapshot; + let scroll_margin_rows = self.vertical_scroll_margin() as u32; - let mut new_screen_top = editor.selections.newest_display(cx).head(); + let mut new_screen_top = self.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( + self.set_scroll_anchor( ScrollAnchor { anchor: new_anchor, offset: Default::default(), @@ -93,25 +52,21 @@ impl Editor { ) } - 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() { + pub fn scroll_cursor_center(&mut self, _: &ScrollCursorCenter, cx: &mut ViewContext) { + let snapshot = self.snapshot(cx).display_snapshot; + let visible_rows = if let Some(visible_rows) = self.visible_line_count() { visible_rows as u32 } else { return; }; - let mut new_screen_top = editor.selections.newest_display(cx).head(); + let mut new_screen_top = self.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( + self.set_scroll_anchor( ScrollAnchor { anchor: new_anchor, offset: Default::default(), @@ -120,20 +75,16 @@ impl Editor { ) } - 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() { + pub fn scroll_cursor_bottom(&mut self, _: &ScrollCursorBottom, cx: &mut ViewContext) { + let snapshot = self.snapshot(cx).display_snapshot; + let scroll_margin_rows = self.vertical_scroll_margin() as u32; + let visible_rows = if let Some(visible_rows) = self.visible_line_count() { visible_rows as u32 } else { return; }; - let mut new_screen_top = editor.selections.newest_display(cx).head(); + let mut new_screen_top = self.selections.newest_display(cx).head(); *new_screen_top.row_mut() = new_screen_top .row() .saturating_sub(visible_rows.saturating_sub(scroll_margin_rows)); @@ -141,7 +92,7 @@ impl Editor { 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( + self.set_scroll_anchor( ScrollAnchor { anchor: new_anchor, offset: Default::default(), diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index ffada50179..ba70739942 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/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}; @@ -48,11 +48,11 @@ impl AutoscrollStrategy { impl Editor { pub fn autoscroll_vertically( &mut self, - viewport_height: f32, - line_height: f32, + viewport_height: Pixels, + line_height: Pixels, cx: &mut ViewContext, ) -> bool { - let visible_lines = viewport_height / line_height; + let visible_lines = f32::from(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 { .. }) { @@ -60,8 +60,8 @@ impl Editor { } else { display_map.max_point().row() as f32 }; - if scroll_position.y() > max_scroll_top { - scroll_position.set_y(max_scroll_top); + if scroll_position.y > max_scroll_top { + scroll_position.y = max_scroll_top; self.set_scroll_position(scroll_position, cx); } @@ -136,31 +136,31 @@ 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; 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 { @@ -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).into(); 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).into(); true } else { false diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 4b2dc855c3..8d71916210 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -6,7 +6,7 @@ use std::{ }; use collections::HashMap; -use gpui::{AppContext, ModelHandle}; +use gpui::{AppContext, Model, Pixels}; use itertools::Itertools; use language::{Bias, Point, Selection, SelectionGoal, TextDimension, ToPoint}; use util::post_inc; @@ -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, @@ -306,19 +306,19 @@ impl SelectionsCollection { &mut self, display_map: &DisplaySnapshot, row: u32, - positions: &Range, + 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 line = display_map.layout_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_col = line.closest_index_for_x(positions.start) as u32; + if start_col < line_len || (is_empty && positions.start == 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_col = line.closest_index_for_x(positions.end) as u32; let end = DisplayPoint::new(row, end_col); Some(Selection { @@ -327,8 +327,8 @@ impl SelectionsCollection { end: end.to_point(display_map), reversed, goal: SelectionGoal::HorizontalRange { - start: positions.start, - end: positions.end, + start: positions.start.into(), + end: positions.end.into(), }, }) } else { @@ -592,7 +592,10 @@ impl<'a> MutableSelectionsCollection<'a> { self.select(selections) } - pub fn select_anchor_ranges>>(&mut self, ranges: I) { + pub fn select_anchor_ranges(&mut self, ranges: I) + where + I: IntoIterator>, + { let buffer = self.buffer.read(self.cx).snapshot(self.cx); let selections = ranges .into_iter() @@ -614,7 +617,6 @@ impl<'a> MutableSelectionsCollection<'a> { } }) .collect::>(); - self.select_anchors(selections) } diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 08cc533d62..4ce539ad79 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -6,7 +6,7 @@ use crate::{ DisplayPoint, Editor, EditorMode, MultiBuffer, }; -use gpui::{ModelHandle, ViewContext}; +use gpui::{Context, Model, Pixels, ViewContext}; use project::Project; use util::test::{marked_text_offsets, marked_text_ranges}; @@ -26,19 +26,11 @@ pub fn marked_display_snapshot( ) -> (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 font = cx.text_style().font(); + let font_size: Pixels = 14usize.into(); 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 display_map = cx.new_model(|cx| DisplayMap::new(buffer, font, font_size, None, 1, 1, cx)); let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); let markers = markers .into_iter() @@ -67,17 +59,16 @@ 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 { - Editor::new(EditorMode::Full, buffer, None, None, cx) +pub(crate) fn build_editor(buffer: Model, cx: &mut ViewContext) -> Editor { + // todo!() + 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) + // todo!() + Editor::new(EditorMode::Full, buffer, Some(project), /*None,*/ cx) } diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index 3e2f38a0b6..7ee55cddba 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -5,11 +5,12 @@ use std::{ }; use anyhow::Result; +use serde_json::json; use crate::{Editor, ToPoint}; use collections::HashSet; use futures::Future; -use gpui::{json, ViewContext, ViewHandle}; +use gpui::{View, ViewContext, VisualTestContext}; use indoc::indoc; use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries}; use lsp::{notification, request}; @@ -18,12 +19,12 @@ use project::Project; use smol::stream::StreamExt; use workspace::{AppState, Workspace, WorkspaceHandle}; -use super::editor_test_context::EditorTestContext; +use super::editor_test_context::{AssertionContextManager, EditorTestContext}; pub struct EditorLspTestContext<'a> { pub cx: EditorTestContext<'a>, pub lsp: lsp::FakeLanguageServer, - pub workspace: ViewHandle, + pub workspace: View, pub buffer_lsp_url: lsp::Url, } @@ -33,8 +34,6 @@ impl<'a> EditorLspTestContext<'a> { capabilities: lsp::ServerCapabilities, cx: &'a mut gpui::TestAppContext, ) -> EditorLspTestContext<'a> { - use json::json; - let app_state = cx.update(AppState::test); cx.update(|cx| { @@ -60,6 +59,7 @@ impl<'a> EditorLspTestContext<'a> { .await; let project = Project::test(app_state.fs.clone(), [], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); app_state @@ -69,37 +69,38 @@ impl<'a> EditorLspTestContext<'a> { .await; let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let workspace = window.root(cx); + + let workspace = window.root_view(cx).unwrap(); + + let mut cx = VisualTestContext::from_window(*window.deref(), cx); project - .update(cx, |project, cx| { + .update(&mut 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| { + .update(&mut 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()); + editor.update(&mut cx, |editor, cx| editor.focus(cx)); let lsp = fake_servers.next().await.unwrap(); - Self { cx: EditorTestContext { cx, window: window.into(), editor, + assertion_cx: AssertionContextManager::new(), }, lsp, workspace, @@ -257,7 +258,7 @@ impl<'a> EditorLspTestContext<'a> { where F: FnOnce(&mut Workspace, &mut ViewContext) -> T, { - self.workspace.update(self.cx.cx, update) + self.workspace.update(&mut self.cx.cx, update) } pub fn handle_request( diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index c083ce0e68..bd5acb9945 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -1,17 +1,23 @@ use crate::{ display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer, }; +use collections::BTreeMap; use futures::Future; use gpui::{ - executor::Foreground, keymap_matcher::Keystroke, AnyWindowHandle, AppContext, ContextHandle, - ModelContext, ViewContext, ViewHandle, + AnyWindowHandle, AppContext, Keystroke, ModelContext, View, ViewContext, VisualTestContext, }; use indoc::indoc; +use itertools::Itertools; use language::{Buffer, BufferSnapshot}; +use parking_lot::RwLock; use project::{FakeFs, Project}; use std::{ any::TypeId, ops::{Deref, DerefMut, Range}, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, }; use util::{ assert_set_eq, @@ -21,14 +27,15 @@ use util::{ use super::build_editor_with_project; pub struct EditorTestContext<'a> { - pub cx: &'a mut gpui::TestAppContext, + pub cx: gpui::VisualTestContext<'a>, pub window: AnyWindowHandle, - pub editor: ViewHandle, + pub editor: View, + pub assertion_cx: AssertionContextManager, } impl<'a> EditorTestContext<'a> { pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> { - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); // fs.insert_file("/file", "".to_owned()).await; fs.insert_tree( "/root", @@ -44,15 +51,18 @@ impl<'a> EditorTestContext<'a> { }) .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 = cx.add_window(|cx| { + let editor = + build_editor_with_project(project, MultiBuffer::build_from_buffer(buffer, cx), cx); + editor.focus(cx); + editor }); - let editor = window.root(cx); + let editor_view = editor.root_view(cx).unwrap(); Self { - cx, - window: window.into(), - editor, + cx: VisualTestContext::from_window(*editor.deref(), cx), + window: editor.into(), + editor: editor_view, + assertion_cx: AssertionContextManager::new(), } } @@ -60,24 +70,28 @@ impl<'a> EditorTestContext<'a> { &self, predicate: impl FnMut(&Editor, &AppContext) -> bool, ) -> impl Future { - self.editor.condition(self.cx, predicate) + self.editor + .condition::(&self.cx, predicate) } - pub fn editor(&self, read: F) -> T + #[track_caller] + pub fn editor(&mut self, read: F) -> T where F: FnOnce(&Editor, &ViewContext) -> T, { - self.editor.read_with(self.cx, read) + self.editor + .update(&mut self.cx, |this, cx| read(&this, &cx)) } + #[track_caller] pub fn update_editor(&mut self, update: F) -> T where F: FnOnce(&mut Editor, &mut ViewContext) -> T, { - self.editor.update(self.cx, update) + self.editor.update(&mut self.cx, update) } - pub fn multibuffer(&self, read: F) -> T + pub fn multibuffer(&mut self, read: F) -> T where F: FnOnce(&MultiBuffer, &AppContext) -> T, { @@ -91,11 +105,11 @@ impl<'a> EditorTestContext<'a> { self.update_editor(|editor, cx| editor.buffer().update(cx, update)) } - pub fn buffer_text(&self) -> String { + pub fn buffer_text(&mut self) -> String { self.multibuffer(|buffer, cx| buffer.snapshot(cx).text()) } - pub fn buffer(&self, read: F) -> T + pub fn buffer(&mut self, read: F) -> T where F: FnOnce(&Buffer, &AppContext) -> T, { @@ -115,10 +129,18 @@ impl<'a> EditorTestContext<'a> { }) } - pub fn buffer_snapshot(&self) -> BufferSnapshot { + pub fn buffer_snapshot(&mut self) -> BufferSnapshot { self.buffer(|buffer, _| buffer.snapshot()) } + pub fn add_assertion_context(&self, context: String) -> ContextHandle { + self.assertion_cx.add_context(context) + } + + pub fn assertion_context(&self) -> String { + self.assertion_cx.context() + } + pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle { let keystroke_under_test_handle = self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text)); @@ -142,16 +164,16 @@ impl<'a> EditorTestContext<'a> { // 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!(); - } + self.cx.background_executor.run_until_parked(); keystrokes_under_test_handle } - pub fn ranges(&self, marked_text: &str) -> Vec> { + pub fn run_until_parked(&mut self) { + self.cx.background_executor.run_until_parked(); + } + + pub fn ranges(&mut self, marked_text: &str) -> Vec> { let (unmarked_text, ranges) = marked_text_ranges(marked_text, false); assert_eq!(self.buffer_text(), unmarked_text); ranges @@ -161,12 +183,12 @@ impl<'a> EditorTestContext<'a> { let ranges = self.ranges(marked_text); let snapshot = self .editor - .update(self.cx, |editor, cx| editor.snapshot(cx)); + .update(&mut 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 { + pub fn text_anchor_range(&mut 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) @@ -191,7 +213,7 @@ impl<'a> EditorTestContext<'a> { marked_text.escape_debug().to_string() )); let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true); - self.editor.update(self.cx, |editor, cx| { + self.editor.update(&mut self.cx, |editor, cx| { editor.set_text(unmarked_text, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_ranges(selection_ranges) @@ -207,7 +229,7 @@ impl<'a> EditorTestContext<'a> { marked_text.escape_debug().to_string() )); let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true); - self.editor.update(self.cx, |editor, cx| { + self.editor.update(&mut self.cx, |editor, cx| { assert_eq!(editor.text(cx), unmarked_text); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_ranges(selection_ranges) @@ -274,9 +296,12 @@ impl<'a> EditorTestContext<'a> { self.assert_selections(expected_selections, expected_marked_text) } - fn editor_selections(&self) -> Vec> { + #[track_caller] + fn editor_selections(&mut self) -> Vec> { self.editor - .read_with(self.cx, |editor, cx| editor.selections.all::(cx)) + .update(&mut self.cx, |editor, cx| { + editor.selections.all::(cx) + }) .into_iter() .map(|s| { if s.reversed { @@ -301,14 +326,14 @@ impl<'a> EditorTestContext<'a> { panic!( indoc! {" - {}Editor has unexpected selections. + {}Editor has unexpected selections. - Expected selections: - {} + Expected selections: + {} - Actual selections: - {} - "}, + Actual selections: + {} + "}, self.assertion_context(), expected_marked_text, actual_marked_text, @@ -321,7 +346,7 @@ impl<'a> Deref for EditorTestContext<'a> { type Target = gpui::TestAppContext; fn deref(&self) -> &Self::Target { - self.cx + &self.cx } } @@ -330,3 +355,50 @@ impl<'a> DerefMut for EditorTestContext<'a> { &mut self.cx } } + +/// Tracks string context to be printed when assertions fail. +/// Often this is done by storing a context string in the manager and returning the handle. +#[derive(Clone)] +pub struct AssertionContextManager { + id: Arc, + contexts: Arc>>, +} + +impl AssertionContextManager { + pub fn new() -> Self { + Self { + id: Arc::new(AtomicUsize::new(0)), + contexts: Arc::new(RwLock::new(BTreeMap::new())), + } + } + + pub fn add_context(&self, context: String) -> ContextHandle { + let id = self.id.fetch_add(1, Ordering::Relaxed); + let mut contexts = self.contexts.write(); + contexts.insert(id, context); + ContextHandle { + id, + manager: self.clone(), + } + } + + pub fn context(&self) -> String { + let contexts = self.contexts.read(); + format!("\n{}\n", contexts.values().join("\n")) + } +} + +/// Used to track the lifetime of a piece of context so that it can be provided when an assertion fails. +/// For example, in the EditorTestContext, `set_state` returns a context handle so that if an assertion fails, +/// the state that was set initially for the failure can be printed in the error message +pub struct ContextHandle { + id: usize, + manager: AssertionContextManager, +} + +impl Drop for ContextHandle { + fn drop(&mut self) { + let mut contexts = self.manager.contexts.write(); + contexts.remove(&self.id); + } +} diff --git a/crates/editor2/Cargo.toml b/crates/editor2/Cargo.toml deleted file mode 100644 index 98bd06bc4d..0000000000 --- a/crates/editor2/Cargo.toml +++ /dev/null @@ -1,93 +0,0 @@ -[package] -name = "editor2" -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 = { package = "client2", path = "../client2" } -clock = { path = "../clock" } -copilot = { package="copilot2", path = "../copilot2" } -db = { package="db2", path = "../db2" } -collections = { path = "../collections" } -# context_menu = { path = "../context_menu" } -fuzzy = { package = "fuzzy2", path = "../fuzzy2" } -git = { package = "git3", path = "../git3" } -gpui = { package = "gpui2", path = "../gpui2" } -language = { package = "language2", path = "../language2" } -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 = { package = "rich_text2", path = "../rich_text2" } -settings = { package="settings2", path = "../settings2" } -snippet = { path = "../snippet" } -sum_tree = { path = "../sum_tree" } -text = { package="text2", path = "../text2" } -theme = { package="theme2", path = "../theme2" } -ui = { package = "ui2", path = "../ui2" } -util = { path = "../util" } -sqlez = { path = "../sqlez" } -workspace = { package = "workspace2", path = "../workspace2" } - -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_json.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 = { 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 = { 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 = { package = "multi_buffer2", path = "../multi_buffer2", 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 deleted file mode 100644 index e3a8ce6293..0000000000 --- a/crates/editor2/src/blink_manager.rs +++ /dev/null @@ -1,107 +0,0 @@ -use crate::EditorSettings; -use gpui::ModelContext; -use settings::Settings; -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| async move { - Timer::after(interval).await; - 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 EditorSettings::get_global(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| async move { - Timer::after(interval).await; - if let Some(this) = this.upgrade() { - this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx)) - .ok(); - } - }) - .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) { - if self.enabled { - return; - } - - 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 - } -} diff --git a/crates/editor2/src/display_map.rs b/crates/editor2/src/display_map.rs deleted file mode 100644 index 8703f1ba40..0000000000 --- a/crates/editor2/src/display_map.rs +++ /dev/null @@ -1,1854 +0,0 @@ -mod block_map; -mod fold_map; -mod inlay_map; -mod tab_map; -mod wrap_map; - -use crate::EditorStyle; -use crate::{ - link_go_to_definition::InlayHighlight, movement::TextLayoutDetails, Anchor, AnchorRangeExt, - InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, -}; -pub use block_map::{BlockMap, BlockPoint}; -use collections::{BTreeMap, HashMap, HashSet}; -use fold_map::FoldMap; -use gpui::{Font, HighlightStyle, Hsla, LineLayout, Model, ModelContext, Pixels, UnderlineStyle}; -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::{Fold, FoldPoint}; -pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint}; - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum FoldStatus { - Folded, - Foldable, -} - -const UNNECESSARY_CODE_FADE: f32 = 0.3; - -pub trait ToDisplayPoint { - fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint; -} - -type TextHighlights = TreeMap, Arc<(HighlightStyle, Vec>)>>; -type InlayHighlights = BTreeMap>; - -pub struct DisplayMap { - buffer: Model, - buffer_subscription: BufferSubscription, - fold_map: FoldMap, - inlay_map: InlayMap, - tab_map: TabMap, - wrap_map: Model, - block_map: BlockMap, - text_highlights: TextHighlights, - inlay_highlights: InlayHighlights, - pub clip_at_line_ends: bool, -} - -impl DisplayMap { - pub fn new( - buffer: Model, - font: Font, - font_size: Pixels, - 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, 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.range.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: Font, font_size: Pixels, cx: &mut ModelContext) -> bool { - self.wrap_map - .update(cx, |map, cx| map.set_font_with_size(font, font_size, cx)) - } - - pub fn set_fold_ellipses_color(&mut self, color: Hsla) -> 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: &Model, 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, - editor_style: &'a EditorStyle, - ) -> impl Iterator> { - self.chunks( - display_rows, - language_aware, - Some(editor_style.inlays_style), - Some(editor_style.suggestions_style), - ) - .map(|chunk| { - let mut highlight_style = chunk - .syntax_highlight_id - .and_then(|id| id.style(&editor_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(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_color = - super::diagnostic_style(severity, true, &editor_style.status); - diagnostic_highlight.underline = Some(UnderlineStyle { - color: Some(diagnostic_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 layout_row( - &self, - display_row: u32, - TextLayoutDetails { - text_system, - editor_style, - rem_size, - }: &TextLayoutDetails, - ) -> Arc { - let mut runs = Vec::new(); - let mut line = String::new(); - - 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 { - Cow::Owned(editor_style.text.clone().highlight(style)) - } else { - Cow::Borrowed(&editor_style.text) - }; - - runs.push(text_style.to_run(chunk.chunk.len())) - } - - if line.ends_with('\n') { - line.pop(); - if let Some(last_run) = runs.last_mut() { - last_run.len -= 1; - if last_run.len == 0 { - runs.pop(); - } - } - } - - let font_size = editor_style.text.font_size.to_pixels(*rem_size); - text_system - .layout_line(&line, font_size, &runs) - .expect("we expect the font to be loaded because it's rendered by the editor") - } - - pub fn x_for_display_point( - &self, - display_point: DisplayPoint, - text_layout_details: &TextLayoutDetails, - ) -> Pixels { - let line = self.layout_row(display_point.row(), text_layout_details); - line.x_for_index(display_point.column() as usize) - } - - pub fn display_column_for_x( - &self, - display_row: u32, - x: Pixels, - details: &TextLayoutDetails, - ) -> u32 { - let layout_line = self.layout_row(display_row, details); - layout_line.closest_index_for_x(x) 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::{div, font, observe, px, AppContext, Context, Element, 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::{LoadThemes, 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.background_executor.set_block_on_ticks(0..=50); - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(10); - - let _test_platform = &cx.test_platform; - 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 font_size = px(14.0); - let max_wrap_width = 300.0; - let mut wrap_width = if rng.gen_bool(0.1) { - None - } else { - Some(px(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.new_model(|cx| { - DisplayMap::new( - buffer.clone(), - font("Helvetica"), - 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(px(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(|_| div().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.background_executor - .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.update(cx, |editor, cx| editor.text_layout_details(cx)); - - let font_size = px(12.0); - let wrap_width = Some(px(64.)); - - let text = "one two three four five\nsix seven eight"; - let buffer = MultiBuffer::build_simple(text, cx); - let map = cx.new_model(|cx| { - DisplayMap::new( - buffer.clone(), - font("Helvetica"), - 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_display_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.0) - ) - ); - assert_eq!( - movement::down( - &snapshot, - DisplayPoint::new(0, 7), - SelectionGoal::HorizontalPosition(x.0), - false, - &text_layout_details - ), - ( - DisplayPoint::new(1, 10), - SelectionGoal::HorizontalPosition(x.0) - ) - ); - assert_eq!( - movement::down( - &snapshot, - DisplayPoint::new(1, 10), - SelectionGoal::HorizontalPosition(x.0), - false, - &text_layout_details - ), - ( - DisplayPoint::new(2, 4), - SelectionGoal::HorizontalPosition(x.0) - ) - ); - - 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("Helvetica"), px(font_size.0 + 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 font_size = px(14.0); - let map = cx.new_model(|cx| { - DisplayMap::new(buffer.clone(), font("Helvetica"), 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_test(vec![ - ("mod.body", Hsla::red().into()), - ("fn.name", 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.new_model(|cx| { - Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) - }); - cx.condition(&buffer, |buf, _| !buf.is_parsing()).await; - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - - let font_size = px(14.0); - - let map = cx - .new_model(|cx| DisplayMap::new(buffer, font("Helvetica"), 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.background_executor - .set_block_on_ticks(usize::MAX..=usize::MAX); - - let text = r#" - fn outer() {} - - mod module { - fn inner() {} - }"# - .unindent(); - - let theme = SyntaxTheme::new_test(vec![ - ("mod.body", Hsla::red().into()), - ("fn.name", 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.new_model(|cx| { - Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) - }); - cx.condition(&buffer, |buf, _| !buf.is_parsing()).await; - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - - let font_size = px(16.0); - - let map = cx.new_model(|cx| { - DisplayMap::new(buffer, font("Courier"), font_size, Some(px(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_test(vec![ - ("operator", Hsla::red().into()), - ("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.new_model(|cx| { - Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) - }); - cx.condition(&buffer, |buf, _| !buf.is_parsing()).await; - - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); - - let font_size = px(16.0); - let map = - cx.new_model(|cx| DisplayMap::new(buffer, font("Courier"), 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_size = px(14.0); - - let map = cx.new_model(|cx| { - DisplayMap::new(buffer.clone(), font("Helvetica"), 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_size = px(14.0); - let map = cx.new_model(|cx| { - DisplayMap::new(buffer.clone(), font("Helvetica"), 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)) { - let settings = SettingsStore::test(cx); - cx.set_global(settings); - language::init(cx); - crate::init(cx); - Project::init_settings(cx); - theme::init(LoadThemes::JustBase, 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 deleted file mode 100644 index 6eb0d05bfe..0000000000 --- a/crates/editor2/src/display_map/block_map.rs +++ /dev/null @@ -1,1647 +0,0 @@ -use super::{ - wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot}, - Highlights, -}; -use crate::{Anchor, Editor, EditorStyle, ExcerptId, ExcerptRange, ToPoint as _}; -use collections::{Bound, HashMap, HashSet}; -use gpui::{AnyElement, Pixels, 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> { - pub view_context: &'b mut ViewContext<'a, Editor>, - pub anchor_x: Pixels, - pub gutter_width: Pixels, - pub gutter_padding: Pixels, - pub em_width: Pixels, - pub line_height: Pixels, - pub block_id: usize, - pub editor_style: &'b EditorStyle, -} - -#[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> Deref for BlockContext<'a, '_> { - type Target = ViewContext<'a, 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::{div, font, px, 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::TestAppContext) { - cx.update(|cx| init_test(cx)); - - let text = "aaa\nbbb\nccc\nddd"; - - let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx)); - let buffer_snapshot = cx.update(|cx| 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) = - cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(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(|_| div().into_any()), - }, - BlockProperties { - style: BlockStyle::Fixed, - position: buffer_snapshot.anchor_after(Point::new(1, 2)), - height: 2, - disposition: BlockDisposition::Above, - render: Arc::new(|_| div().into_any()), - }, - BlockProperties { - style: BlockStyle::Fixed, - position: buffer_snapshot.anchor_after(Point::new(3, 3)), - height: 3, - disposition: BlockDisposition::Below, - render: Arc::new(|_| div().into_any()), - }, - ]); - - 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::TestAppContext) { - cx.update(|cx| init_test(cx)); - - let _font_id = cx.text_system().font_id(&font("Helvetica")).unwrap(); - - let text = "one two three\nfour five six\nseven eight"; - - let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx)); - let buffer_snapshot = cx.update(|cx| 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) = cx.update(|cx| { - WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), Some(px(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(|_| div().into_any()), - height: 1, - }, - BlockProperties { - style: BlockStyle::Fixed, - position: buffer_snapshot.anchor_after(Point::new(1, 1)), - disposition: BlockDisposition::Below, - render: Arc::new(|_| div().into_any()), - 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::TestAppContext, mut rng: StdRng) { - cx.update(|cx| 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(px(rng.gen_range(0.0..=100.0))) - }; - let tab_size = 1.try_into().unwrap(); - let font_size = px(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); - cx.update(|cx| MultiBuffer::build_simple(&text, cx)) - } else { - cx.update(|cx| MultiBuffer::build_random(&mut rng, cx)) - }; - - let mut buffer_snapshot = cx.update(|cx| 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) = cx - .update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), 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(px(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 = cx.update(|cx| buffer.read(cx).read(cx).clone()); - 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(|_| div().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) { - let settings = SettingsStore::test(cx); - cx.set_global(settings); - theme::init(theme::LoadThemes::JustBase, 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 deleted file mode 100644 index 4dad2d52ae..0000000000 --- a/crates/editor2/src/display_map/fold_map.rs +++ /dev/null @@ -1,1746 +0,0 @@ -use super::{ - inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot}, - Highlights, -}; -use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset}; -use gpui::{ElementId, HighlightStyle, Hsla}; -use language::{Chunk, Edit, Point, TextSummary}; -use std::{ - any::TypeId, - cmp::{self, Ordering}, - iter, - ops::{Add, AddAssign, Deref, DerefMut, Range, Sub}, -}; -use sum_tree::{Bias, Cursor, FilterCursor, SumTree}; -use util::post_inc; - -#[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_range = - FoldRange(buffer.anchor_after(range.start)..buffer.anchor_before(range.end)); - if fold_range.0.start.excerpt_id != fold_range.0.end.excerpt_id { - continue; - } - - folds.push(Fold { - id: FoldId(post_inc(&mut self.0.next_fold_id.0)), - range: fold_range, - }); - - 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.range, &b.range, 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.range, 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.range.start.to_offset(buffer)..fold.range.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, - next_fold_id: FoldId, -} - -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, - next_fold_id: FoldId::default(), - }; - 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: Hsla) -> 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 - .range - .cmp(&next_fold.range, &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( - &FoldRange(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.range.start.to_offset(&inlay_snapshot.buffer); - let buffer_end = f.range.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(); - 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(Copy, Clone, Eq, PartialEq, Debug, Default)] -pub struct FoldId(usize); - -impl Into for FoldId { - fn into(self) -> ElementId { - ElementId::Integer(self.0) - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Fold { - pub id: FoldId, - pub range: FoldRange, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct FoldRange(Range); - -impl Deref for FoldRange { - type Target = Range; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for FoldRange { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl Default for FoldRange { - 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.range.start.clone(), - end: self.range.end.clone(), - min_start: self.range.start.clone(), - max_end: self.range.end.clone(), - count: 1, - } - } -} - -#[derive(Clone, Debug)] -pub 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 FoldRange { - 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, FoldRange> for FoldRange { - 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.range.start.to_point(&buffer_snapshot) - ..fold.range.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.range.end, &buffer_snapshot) == Ordering::Less - && end.cmp(&fold.range.start, &buffer_snapshot) == Ordering::Greater - }) - .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) { - let store = SettingsStore::test(cx); - cx.set_global(store); - } - - 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.range.cmp(&b.range, buffer)); - let mut fold_ranges = folds - .iter() - .map(|fold| fold.range.start.to_offset(buffer)..fold.range.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 deleted file mode 100644 index 84fad96a48..0000000000 --- a/crates/editor2/src/display_map/inlay_map.rs +++ /dev/null @@ -1,1896 +0,0 @@ -use crate::{Anchor, InlayId, MultiBufferSnapshot, ToOffset}; -use collections::{BTreeMap, BTreeSet}; -use gpui::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) { - let store = SettingsStore::test(cx); - cx.set_global(store); - theme::init(theme::LoadThemes::JustBase, cx); - } -} diff --git a/crates/editor2/src/display_map/tab_map.rs b/crates/editor2/src/display_map/tab_map.rs deleted file mode 100644 index 6b38ea2d24..0000000000 --- a/crates/editor2/src/display_map/tab_map.rs +++ /dev/null @@ -1,765 +0,0 @@ -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 deleted file mode 100644 index 05aa381627..0000000000 --- a/crates/editor2/src/display_map/wrap_map.rs +++ /dev/null @@ -1,1359 +0,0 @@ -use super::{ - fold_map::FoldBufferRows, - tab_map::{self, TabEdit, TabPoint, TabSnapshot}, - Highlights, -}; -use crate::MultiBufferSnapshot; -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; - -pub struct WrapMap { - snapshot: WrapSnapshot, - pending_edits: VecDeque<(TabSnapshot, Vec)>, - interpolated_edits: Patch, - edits_since_sync: Patch, - wrap_width: Option, - background_task: Option>, - font_with_size: (Font, Pixels), -} - -#[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: Font, - font_size: Pixels, - wrap_width: Option, - cx: &mut AppContext, - ) -> (Model, WrapSnapshot) { - let handle = cx.new_model(|cx| { - let mut this = Self { - font_with_size: (font, 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_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 { - 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 mut edits = Patch::default(); - let text_system = cx.text_system().clone(); - 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, font_size).log_err() - { - let tab_snapshot = new_snapshot.tab_snapshot.clone(); - let range = TabPoint::zero()..tab_snapshot.max_point(); - 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_executor() - .block_with_timeout(Duration::from_millis(5), task) - { - Ok((snapshot, edits)) => { - self.snapshot = snapshot; - self.edits_since_sync = self.edits_since_sync.compose(&edits); - } - 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(); - }) - .ok(); - })); - } - } - } 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 text_system = cx.text_system().clone(); - 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, 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_executor() - .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(); - }) - .ok(); - })); - } - } - } - } - - 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: Pixels, - 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::{font, px, test::observe}; - use rand::prelude::*; - use settings::SettingsStore; - use smol::stream::StreamExt; - use std::{cmp, env, num::NonZeroU32}; - use text::Rope; - use theme::LoadThemes; - - #[gpui::test(iterations = 100)] - async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) { - // todo!() this test is flaky - init_test(cx); - - cx.background_executor.set_block_on_ticks(0..=50); - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(10); - - let text_system = cx.read(|cx| cx.text_system().clone()); - let mut wrap_width = if rng.gen_bool(0.1) { - None - } else { - Some(px(rng.gen_range(0.0..=1000.0))) - }; - let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap(); - let font = font("Helvetica"); - let _font_id = text_system.font_id(&font).unwrap(); - let font_size = px(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 = text_system.line_wrapper(font.clone(), font_size).unwrap(); - 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, 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(px(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.update(|cx| { - let settings = SettingsStore::test(cx); - cx.set_global(settings); - theme::init(LoadThemes::JustBase, 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 deleted file mode 100644 index 85a156a8eb..0000000000 --- a/crates/editor2/src/editor.rs +++ /dev/null @@ -1,9884 +0,0 @@ -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; -mod rust_analyzer_ext; -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 as _, Result}; -use blink_manager::BlinkManager; -use client::{Client, Collaborator, ParticipantIndex, TelemetrySettings}; -use clock::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 git::diff_hunk_to_display; -use gpui::{ - actions, div, impl_actions, point, prelude::*, px, relative, rems, size, uniform_list, Action, - AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context, - DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, - HighlightStyle, Hsla, InputHandler, InteractiveText, KeyContext, Model, MouseButton, - ParentElement, Pixels, Render, SharedString, Styled, StyledText, Subscription, Task, TextStyle, - UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, 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, Documentation, IndentKind, IndentSize, Language, - LanguageRegistry, LanguageServerName, OffsetRangeExt, Point, Selection, SelectionGoal, - TransactionId, -}; - -use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState}; -use lsp::{DiagnosticSeverity, LanguageServerId}; -use mouse_context_menu::MouseContextMenu; -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::prelude::*; -use rpc::proto::{self, *}; -use scroll::{ - autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide, -}; -use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection}; -use serde::{Deserialize, Serialize}; -use settings::{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, - sync::Weak, - time::{Duration, Instant}, -}; -pub use sum_tree::Bias; -use sum_tree::TreeMap; -use text::{OffsetUtf16, Rope}; -use theme::{ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, ThemeColors, ThemeSettings}; -use ui::{ - h_stack, ButtonSize, ButtonStyle, Icon, IconButton, ListItem, ListItemSpacing, Popover, Tooltip, -}; -use ui::{prelude::*, IconSize}; -use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; -use workspace::{searchable::SearchEvent, ItemNavHistory, Pane, SplitDirection, ViewId, Workspace}; - -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( - element_id: impl Into, - parsed: &language::ParsedMarkdown, - editor_style: &EditorStyle, - workspace: Option>, - cx: &mut ViewContext, -) -> InteractiveText { - let code_span_background_color = cx - .theme() - .colors() - .editor_document_highlight_read_background; - - let highlights = gpui::combine_highlights( - parsed.highlights.iter().filter_map(|(range, highlight)| { - let highlight = highlight.to_highlight_style(&editor_style.syntax)?; - Some((range.clone(), highlight)) - }), - parsed - .regions - .iter() - .zip(&parsed.region_ranges) - .filter_map(|(region, range)| { - if region.code { - Some(( - range.clone(), - HighlightStyle { - background_color: Some(code_span_background_color), - ..Default::default() - }, - )) - } else { - None - } - }), - ); - - let mut links = Vec::new(); - let mut link_ranges = Vec::new(); - for (range, region) in parsed.region_ranges.iter().zip(&parsed.regions) { - if let Some(link) = region.link.clone() { - links.push(link); - link_ranges.push(range.clone()); - } - } - - InteractiveText::new( - element_id, - StyledText::new(parsed.text.clone()).with_highlights(&editor_style.text, highlights), - ) - .on_click(link_ranges, move |clicked_range_ix, cx| { - match &links[clicked_range_ix] { - markdown::Link::Web { url } => cx.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(); - }); - } - } - } - }) -} - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct SelectNext { - #[serde(default)] - pub replace_newest: bool, -} - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct SelectPrevious { - #[serde(default)] - pub replace_newest: bool, -} - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct SelectAllMatches { - #[serde(default)] - pub replace_newest: bool, -} - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct SelectToBeginningOfLine { - #[serde(default)] - stop_at_soft_wraps: bool, -} - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct MovePageUp { - #[serde(default)] - center_cursor: bool, -} - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct MovePageDown { - #[serde(default)] - center_cursor: bool, -} - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct SelectToEndOfLine { - #[serde(default)] - stop_at_soft_wraps: bool, -} - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct ToggleCodeActions { - #[serde(default)] - pub deployed_from_indicator: bool, -} - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct ConfirmCompletion { - #[serde(default)] - pub item_ix: Option, -} - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct ConfirmCodeAction { - #[serde(default)] - pub item_ix: Option, -} - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct ToggleComments { - #[serde(default)] - pub advance_downwards: bool, -} - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct FoldAt { - pub buffer_row: u32, -} - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct UnfoldAt { - pub buffer_row: u32, -} - -impl_actions!( - editor, - [ - SelectNext, - SelectPrevious, - SelectAllMatches, - SelectToBeginningOfLine, - MovePageUp, - MovePageDown, - SelectToEndOfLine, - ToggleCodeActions, - ConfirmCompletion, - ConfirmCodeAction, - ToggleComments, - FoldAt, - UnfoldAt - ] -); - -#[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, - [ - AddSelectionAbove, - AddSelectionBelow, - Backspace, - Cancel, - ConfirmRename, - ContextMenuFirst, - ContextMenuLast, - ContextMenuNext, - ContextMenuPrev, - ConvertToKebabCase, - ConvertToLowerCamelCase, - ConvertToLowerCase, - ConvertToSnakeCase, - ConvertToTitleCase, - ConvertToUpperCamelCase, - ConvertToUpperCase, - Copy, - CopyHighlightJson, - CopyPath, - CopyRelativePath, - Cut, - CutToEndOfLine, - Delete, - DeleteLine, - DeleteToBeginningOfLine, - DeleteToEndOfLine, - DeleteToNextSubwordEnd, - DeleteToNextWordEnd, - DeleteToPreviousSubwordStart, - DeleteToPreviousWordStart, - DuplicateLine, - ExpandMacroRecursively, - FindAllReferences, - Fold, - FoldSelectedRanges, - Format, - GoToDefinition, - GoToDefinitionSplit, - GoToDiagnostic, - GoToHunk, - GoToPrevDiagnostic, - GoToPrevHunk, - GoToTypeDefinition, - GoToTypeDefinitionSplit, - HalfPageDown, - HalfPageUp, - Hover, - Indent, - JoinLines, - LineDown, - LineUp, - MoveDown, - MoveLeft, - MoveLineDown, - MoveLineUp, - MoveRight, - MoveToBeginning, - MoveToBeginningOfLine, - MoveToEnclosingBracket, - MoveToEnd, - MoveToEndOfLine, - MoveToEndOfParagraph, - MoveToNextSubwordEnd, - MoveToNextWordEnd, - MoveToPreviousSubwordStart, - MoveToPreviousWordStart, - MoveToStartOfParagraph, - MoveUp, - Newline, - NewlineAbove, - NewlineBelow, - NextScreen, - OpenExcerpts, - Outdent, - PageDown, - PageUp, - Paste, - Redo, - RedoSelection, - Rename, - RestartLanguageServer, - RevealInFinder, - ReverseLines, - ScrollCursorBottom, - ScrollCursorCenter, - ScrollCursorTop, - SelectAll, - SelectDown, - SelectLargerSyntaxNode, - SelectLeft, - SelectLine, - SelectRight, - SelectSmallerSyntaxNode, - SelectToBeginning, - SelectToEnd, - SelectToEndOfParagraph, - SelectToNextSubwordEnd, - SelectToNextWordEnd, - SelectToPreviousSubwordStart, - SelectToPreviousWordStart, - SelectToStartOfParagraph, - SelectUp, - ShowCharacterPalette, - ShowCompletions, - ShuffleLines, - SortLinesCaseInsensitive, - SortLinesCaseSensitive, - SplitSelectionIntoLines, - Tab, - TabPrev, - ToggleInlayHints, - ToggleSoftWrap, - Transpose, - Undo, - UndoSelection, - UnfoldLines, - ] -); - -enum DocumentHighlightRead {} -enum DocumentHighlightWrite {} -enum InputComposition {} - -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum Direction { - Prev, - Next, -} - -pub fn init_settings(cx: &mut AppContext) { - EditorSettings::register(cx); -} - -pub fn init(cx: &mut AppContext) { - init_settings(cx); - - workspace::register_project_item::(cx); - workspace::register_followable_item::(cx); - workspace::register_deserializable_item::(cx); - cx.observe_new_views( - |workspace: &mut Workspace, _cx: &mut ViewContext| { - workspace.register_action(Editor::new_file); - workspace.register_action(Editor::new_file_in_direction); - }, - ) - .detach(); - - cx.on_action(move |_: &workspace::NewFile, cx| { - let app_state = cx.global::>(); - if let Some(app_state) = app_state.upgrade() { - workspace::open_new(&app_state, cx, |workspace, cx| { - Editor::new_file(workspace, &Default::default(), cx) - }) - .detach(); - } - }); - cx.on_action(move |_: &workspace::NewWindow, cx| { - let app_state = cx.global::>(); - if let Some(app_state) = app_state.upgrade() { - workspace::open_new(&app_state, cx, |workspace, cx| { - Editor::new_file(workspace, &Default::default(), cx) - }) - .detach(); - } - }); -} - -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: gpui::Point, - }, - 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, Default)] -pub struct EditorStyle { - pub background: Hsla, - pub local_player: PlayerColor, - pub text: TextStyle, - pub scrollbar_width: Pixels, - pub syntax: Arc, - pub status: StatusColors, - pub inlays_style: HighlightStyle, - pub suggestions_style: HighlightStyle, -} - -type CompletionId = usize; - -// type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor; -// type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option; - -type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Vec>); -type InlayBackgroundHighlight = (fn(&ThemeColors) -> Hsla, Vec); - -pub struct Editor { - handle: WeakView, - focus_handle: FocusHandle, - buffer: Model, - display_map: Model, - 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, - project: Option>, - collaboration_hub: Option>, - blink_manager: Model, - 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: Option, - completion_tasks: Vec<(CompletionId, Task>)>, - next_completion_id: CompletionId, - available_code_actions: Option<(Model, 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<(WeakView, 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>, - gutter_width: Pixels, - style: Option, - editor_actions: Vec)>>, -} - -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: View, - block_id: BlockId, -} - -struct InvalidationStack(Vec); - -enum ContextMenu { - Completions(CompletionsMenu), - CodeActions(CodeActionsMenu), -} - -impl ContextMenu { - fn select_first( - &mut self, - project: Option<&Model>, - 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<&Model>, - 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<&Model>, - 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<&Model>, - 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, - max_height: Pixels, - workspace: Option>, - cx: &mut ViewContext, - ) -> (DisplayPoint, AnyElement) { - match self { - ContextMenu::Completions(menu) => ( - cursor_position, - menu.render(style, max_height, workspace, cx), - ), - ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, max_height, cx), - } - } -} - -#[derive(Clone)] -struct CompletionsMenu { - id: CompletionId, - initial_position: Anchor, - buffer: Model, - completions: Arc>>, - match_candidates: Arc<[StringMatchCandidate]>, - matches: Arc<[StringMatch]>, - selected_item: usize, - scroll_handle: UniformListScrollHandle, -} - -impl CompletionsMenu { - fn select_first(&mut self, project: Option<&Model>, cx: &mut ViewContext) { - self.selected_item = 0; - self.scroll_handle.scroll_to_item(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.scroll_handle.scroll_to_item(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.scroll_handle.scroll_to_item(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.scroll_handle.scroll_to_item(self.selected_item); - self.attempt_resolve_selected_completion_documentation(project, cx); - cx.notify(); - } - - fn pre_resolve_completion_documentation( - &self, - editor: &Editor, - cx: &mut ViewContext, - ) -> Option> { - let settings = EditorSettings::get_global(cx); - if !settings.show_completion_documentation { - return None; - } - - let Some(project) = editor.project.clone() else { - return None; - }; - - 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(); - - Some(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) - }) - .ok() - .flatten(); - 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()); - } - } - })) - } - - 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 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, - max_height: Pixels, - workspace: Option>, - cx: &mut ViewContext, - ) -> AnyElement { - let settings = EditorSettings::get_global(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 style = style.clone(); - - let multiline_docs = { - let mat = &self.matches[selected_item]; - let multiline_docs = match &self.completions.read()[mat.candidate_id].documentation { - Some(Documentation::MultiLinePlainText(text)) => { - Some(div().child(SharedString::from(text.clone()))) - } - Some(Documentation::MultiLineMarkdown(parsed)) => Some(div().child( - render_parsed_markdown("completions_markdown", parsed, &style, workspace, cx), - )), - _ => None, - }; - multiline_docs.map(|div| { - div.id("multiline_docs") - .max_h(max_height) - .flex_1() - .px_1p5() - .py_1() - .min_w(px(260.)) - .max_w(px(640.)) - .w(px(500.)) - .overflow_y_scroll() - // Prevent a mouse down on documentation from being propagated to the editor, - // because that would move the cursor. - .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) - }) - }; - - let list = uniform_list( - cx.view().clone(), - "completions", - matches.len(), - move |_editor, range, cx| { - let start_ix = range.start; - let completions_guard = completions.read(); - - matches[range] - .iter() - .enumerate() - .map(|(ix, mat)| { - 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 - }; - - let highlights = gpui::combine_highlights( - mat.ranges().map(|range| (range, FontWeight::BOLD.into())), - styled_runs_for_code_label(&completion.label, &style.syntax).map( - |(range, mut highlight)| { - // Ignore font weight for syntax highlighting, as we'll use it - // for fuzzy matches. - highlight.font_weight = None; - (range, highlight) - }, - ), - ); - let completion_label = StyledText::new(completion.label.text.clone()) - .with_highlights(&style.text, highlights); - let documentation_label = - if let Some(Documentation::SingleLine(text)) = documentation { - if text.trim().is_empty() { - None - } else { - Some( - h_stack().ml_4().child( - Label::new(text.clone()) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ) - } - } else { - None - }; - - div().min_w(px(220.)).max_w(px(540.)).child( - ListItem::new(mat.candidate_id) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .selected(item_ix == selected_item) - .on_click(cx.listener(move |editor, _event, cx| { - cx.stop_propagation(); - editor - .confirm_completion( - &ConfirmCompletion { - item_ix: Some(item_ix), - }, - cx, - ) - .map(|task| task.detach_and_log_err(cx)); - })) - .child(h_stack().overflow_hidden().child(completion_label)) - .end_slot::

(documentation_label), - ) - }) - .collect() - }, - ) - .max_h(max_height) - .track_scroll(self.scroll_handle.clone()) - .with_width_from_item(widest_completion_ix); - - Popover::new() - .child(list) - .when_some(multiline_docs, |popover, multiline_docs| { - popover.aside(multiline_docs) - }) - .into_any_element() - } - - 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(), - ) - }); - - for mat in &mut matches { - let completion = &completions[mat.candidate_id]; - mat.string = completion.label.text.clone(); - for position in &mut mat.positions { - *position += completion.label.filter_range.start; - } - } - drop(completions); - - self.matches = matches.into(); - self.selected_item = 0; - } -} - -#[derive(Clone)] -struct CodeActionsMenu { - actions: Arc<[CodeAction]>, - buffer: Model, - selected_item: usize, - scroll_handle: UniformListScrollHandle, - deployed_from_indicator: bool, -} - -impl CodeActionsMenu { - fn select_first(&mut self, cx: &mut ViewContext) { - self.selected_item = 0; - self.scroll_handle.scroll_to_item(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.scroll_handle.scroll_to_item(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.scroll_handle.scroll_to_item(self.selected_item); - cx.notify(); - } - - fn select_last(&mut self, cx: &mut ViewContext) { - self.selected_item = self.actions.len() - 1; - self.scroll_handle.scroll_to_item(self.selected_item); - cx.notify() - } - - fn visible(&self) -> bool { - !self.actions.is_empty() - } - - fn render( - &self, - mut cursor_position: DisplayPoint, - _style: &EditorStyle, - max_height: Pixels, - cx: &mut ViewContext, - ) -> (DisplayPoint, AnyElement) { - let actions = self.actions.clone(); - let selected_item = self.selected_item; - - let element = uniform_list( - cx.view().clone(), - "code_actions_menu", - self.actions.len(), - move |_this, range, cx| { - actions[range.clone()] - .iter() - .enumerate() - .map(|(ix, action)| { - let item_ix = range.start + ix; - let selected = selected_item == item_ix; - let colors = cx.theme().colors(); - div() - .px_2() - .text_color(colors.text) - .when(selected, |style| { - style - .bg(colors.element_active) - .text_color(colors.text_accent) - }) - .hover(|style| { - style - .bg(colors.element_hover) - .text_color(colors.text_accent) - }) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |editor, _, cx| { - cx.stop_propagation(); - editor - .confirm_code_action( - &ConfirmCodeAction { - item_ix: Some(item_ix), - }, - cx, - ) - .map(|task| task.detach_and_log_err(cx)); - }), - ) - // TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here. - .child(SharedString::from(action.lsp_action.title.clone())) - }) - .collect() - }, - ) - .elevation_1(cx) - .px_2() - .py_1() - .max_h(max_height) - .track_scroll(self.scroll_handle.clone()) - .with_width_from_item( - self.actions - .iter() - .enumerate() - .max_by_key(|(_, action)| action.lsp_action.title.chars().count()) - .map(|(ix, _)| ix), - ) - .into_any_element(); - - 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 View); - -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(cx: &mut ViewContext) -> Self { - let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), String::new())); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new(EditorMode::SingleLine, buffer, None, cx) - } - - pub fn multi_line(cx: &mut ViewContext) -> Self { - let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), String::new())); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new(EditorMode::Full, buffer, None, cx) - } - - pub fn auto_height(max_lines: usize, cx: &mut ViewContext) -> Self { - let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), String::new())); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new(EditorMode::AutoHeight { max_lines }, buffer, None, cx) - } - - pub fn for_buffer( - buffer: Model, - project: Option>, - cx: &mut ViewContext, - ) -> Self { - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - 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, cx) - } - - pub fn clone(&self, cx: &mut ViewContext) -> Self { - let mut clone = Self::new(self.mode, self.buffer.clone(), self.project.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>, - cx: &mut ViewContext, - ) -> Self { - let style = cx.text_style(); - let font_size = style.font_size.to_pixels(cx.rem_size()); - let display_map = cx.new_model(|cx| { - DisplayMap::new(buffer.clone(), style.font(), font_size, None, 2, 1, cx) - }); - - let selections = SelectionsCollection::new(display_map.clone(), buffer.clone()); - - let blink_manager = cx.new_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(EditorEvent::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 focus_handle = cx.focus_handle(); - cx.on_focus(&focus_handle, Self::handle_focus).detach(); - cx.on_blur(&focus_handle, Self::handle_blur).detach(); - - let mut this = Self { - handle: cx.view().downgrade(), - focus_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, - collaboration_hub: project.clone().map(|project| Box::new(project) as _), - project, - 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: None, - 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, - 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, - gutter_width: Default::default(), - style: None, - editor_actions: Default::default(), - _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._subscriptions.extend(project_subscriptions); - - this.end_selection(cx); - this.scroll_manager.show_scrollbar(cx); - - // todo!("use a different mechanism") - // let editor_created_event = EditorCreated(cx.handle()); - // cx.emit_global(editor_created_event); - - 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 - } - - fn key_context(&self, cx: &AppContext) -> KeyContext { - let mut key_context = KeyContext::default(); - key_context.add("Editor"); - let mode = match self.mode { - EditorMode::SingleLine => "single_line", - EditorMode::AutoHeight { .. } => "auto_height", - EditorMode::Full => "full", - }; - key_context.set("mode", mode); - if self.pending_rename.is_some() { - key_context.add("renaming"); - } - if self.context_menu_visible() { - match self.context_menu.read().as_ref() { - Some(ContextMenu::Completions(_)) => { - key_context.add("menu"); - key_context.add("showing_completions") - } - Some(ContextMenu::CodeActions(_)) => { - key_context.add("menu"); - key_context.add("showing_code_actions") - } - None => {} - } - } - - for layer in self.keymap_context_layers.values() { - key_context.extend(layer); - } - - if let Some(extension) = self - .buffer - .read(cx) - .as_singleton() - .and_then(|buffer| buffer.read(cx).file()?.path().extension()?.to_str()) - { - key_context.set("extension", extension.to_string()); - } - - key_context - } - - 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(); - } else if let Some(buffer) = project - .update(cx, |project, cx| project.create_buffer("", None, cx)) - .log_err() - { - workspace.add_item( - Box::new(cx.new_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(); - } 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.new_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 workspace(&self) -> Option> { - self.workspace.as_ref()?.0.upgrade() - } - - pub fn pane(&self, cx: &AppContext) -> Option> { - self.workspace()?.read(cx).pane_for(&self.handle.upgrade()?) - } - - 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.focus_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 placeholder_text(&self) -> Option<&str> { - self.placeholder_text.as_deref() - } - - pub fn set_placeholder_text( - &mut self, - placeholder_text: impl Into>, - cx: &mut ViewContext, - ) { - let placeholder_text = Some(placeholder_text.into()); - if self.placeholder_text != placeholder_text { - self.placeholder_text = placeholder_text; - 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: KeyContext, - 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; - } - - fn selections_did_change( - &mut self, - local: bool, - old_cursor_position: &Anchor, - cx: &mut ViewContext, - ) { - if self.focus_handle.is_focused(cx) && 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_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); - } - - 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(EditorEvent::SelectionsChanged { local }); - - if self.selections.disjoint_anchors().len() == 1 { - cx.emit(SearchEvent::ActiveMatchChanged) - } - - 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.focus_handle.is_focused(cx) { - 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; - } - } - - 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.focus_handle.is_focused(cx) { - 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)); - - 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: 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 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 { - log::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(); - } - - 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 && EditorSettings::get_global(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 !EditorSettings::get_global(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.excerpts_for_inlay_hints_query(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 excerpts_for_inlay_hints_query( - &self, - restrict_to_languages: Option<&HashSet>>, - cx: &mut ViewContext, - ) -> HashMap, clock::Global, Range)> { - let Some(project) = self.project.as_ref() else { - return HashMap::default(); - }; - let project = project.read(cx); - 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 buffer_file = project::worktree::File::from_dyn(buffer.file())?; - let buffer_worktree = project.worktree_for_id(buffer_file.worktree_id(cx), cx)?; - let worktree_entry = buffer_worktree - .read(cx) - .entry_for_id(buffer_file.project_entry_id(cx)?)?; - if worktree_entry.is_ignored { - return None; - } - - 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 { - text_system: cx.text_system().clone(), - editor_style: self.style.clone().unwrap(), - rem_size: cx.rem_size(), - } - } - - 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()); - }) - .ok(); - } - 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 completions = completions.await.log_err(); - let (menu, pre_resolve_task) = if let Some(completions) = completions { - 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, - scroll_handle: UniformListScrollHandle::new(), - }; - menu.filter(query.as_deref(), cx.background_executor().clone()) - .await; - - if menu.matches.is_empty() { - (None, None) - } else { - let pre_resolve_task = this - .update(&mut cx, |editor, cx| { - menu.pre_resolve_completion_documentation(editor, cx) - }) - .ok() - .flatten(); - (Some(menu), pre_resolve_task) - } - } else { - (None, 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.focus_handle.is_focused(cx) && 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.len() <= 1 { - // 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 let Some(pre_resolve_task) = pre_resolve_task { - pre_resolve_task.await; - } - - 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(EditorEvent::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_executor().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.focus_handle.is_focused(cx) { - 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(), - scroll_handle: UniformListScrollHandle::default(), - deployed_from_indicator, - })); - cx.notify(); - } - } - })?; - - Ok::<_, anyhow::Error>(()) - }) - .detach_and_log_err(cx); - } - - pub fn confirm_code_action( - &mut self, - action: &ConfirmCodeAction, - cx: &mut ViewContext, - ) -> Option>> { - let actions_menu = if let ContextMenu::CodeActions(menu) = self.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 workspace = self.workspace()?; - - let apply_code_actions = workspace - .read(cx) - .project() - .clone() - .update(cx, |project, cx| { - project.apply_code_action(buffer, action, true, cx) - }); - let workspace = workspace.downgrade(); - Some(cx.spawn(|editor, 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: &WeakView, - workspace: WeakView, - transaction: ProjectTransaction, - title: String, - mut cx: AsyncWindowContext, - ) -> Result<()> { - let replica_id = this.update(&mut cx, |this, cx| this.replica_id(cx))?; - - let mut entries = transaction.0.into_iter().collect::>(); - cx.update(|_, cx| { - entries.sort_unstable_by_key(|(buffer, _)| { - buffer.read(cx).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.update(&mut 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.new_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.new_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_executor() - .timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT) - .await; - - 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 - }; - - 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 - } - - 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_executor() - .timer(DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT) - .await; - - 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 - }; - - 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_executor() - .timer(COPILOT_DEBOUNCE_TIMEOUT) - .await; - } - - 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(()) - } - - 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) - }) - .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()?; - - 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(); - } - } - } - - 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(); - } - } - } - - 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(EditorEvent::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() { - Some( - IconButton::new("code_actions_indicator", ui::Icon::Bolt) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .selected(is_active) - .on_click(cx.listener(|editor, _e, cx| { - editor.toggle_code_actions( - &ToggleCodeActions { - deployed_from_indicator: true, - }, - cx, - ); - })), - ) - } else { - None - } - } - - pub fn render_fold_indicators( - &self, - fold_data: Vec>, - _style: &EditorStyle, - gutter_hovered: bool, - _line_height: Pixels, - _gutter_margin: Pixels, - cx: &mut ViewContext, - ) -> Vec> { - fold_data - .iter() - .enumerate() - .map(|(ix, fold_data)| { - fold_data - .map(|(fold_status, buffer_row, active)| { - (active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| { - IconButton::new(ix as usize, ui::Icon::ChevronDown) - .on_click(cx.listener(move |editor, _e, cx| match fold_status { - FoldStatus::Folded => { - editor.unfold_at(&UnfoldAt { buffer_row }, cx); - } - FoldStatus::Foldable => { - editor.fold_at(&FoldAt { buffer_row }, cx); - } - })) - .icon_color(ui::Color::Muted) - .icon_size(ui::IconSize::Small) - .selected(fold_status == FoldStatus::Folded) - .selected_icon(ui::Icon::ChevronRight) - .size(ui::ButtonSize::None) - }) - }) - .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, - max_height: Pixels, - cx: &mut ViewContext, - ) -> Option<(DisplayPoint, AnyElement)> { - self.context_menu.read().as_ref().map(|menu| { - menu.render( - cursor_position, - style, - max_height, - 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.range.start.to_point(&buffer); - let mut end = fold.range.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.range.start.to_point(&buffer); - let mut end = fold.range.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_display_point(head, &text_layout_details) - .into(), - ); - 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(EditorEvent::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(EditorEvent::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(); - 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(); - 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(); - 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(); - 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(); - 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(); - 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(); - 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(); - 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(); - 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(); - 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_display_point(range.start, &text_layout_details); - let end_x = display_map.x_for_display_point(range.end, &text_layout_details); - let positions = start_x.min(end_x)..start_x.max(end_x); - - 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 { - px(start)..px(end) - } else { - let start_x = - display_map.x_for_display_point(range.start, &text_layout_details); - let end_x = - display_map.x_for_display_point(range.end, &text_layout_details); - start_x.min(end_x)..start_x.max(end_x) - }; - - 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_display_point(display_point, &text_layout_details) - .into(), - ); - (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() 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(|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() 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, - ) - }) - .ok(); - - 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( - &mut self, - _: &FindAllReferences, - cx: &mut ViewContext, - ) -> Option>> { - let buffer = self.buffer.read(cx); - let head = self.selections.newest::(cx).head(); - let (buffer, head) = buffer.text_anchor_for_position(head, cx)?; - let replica_id = self.replica_id(cx); - - let workspace = self.workspace()?; - let project = workspace.read(cx).project().clone(); - let references = project.update(cx, |project, cx| project.references(&buffer, head, cx)); - Some(cx.spawn(|_, 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.new_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.new_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 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.new_view(|cx| { - let mut editor = Editor::single_line(cx); - 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(0.6), - ..Default::default() - }, - cx, - ); - let rename_focus_handle = rename_editor.focus_handle(cx); - cx.focus(&rename_focus_handle); - let block_id = this.insert_blocks( - [BlockProperties { - style: BlockStyle::Flex, - position: range.start.clone(), - height: 1, - render: Arc::new({ - let rename_editor = rename_editor.clone(); - move |cx: &mut BlockContext| { - let mut text_style = cx.editor_style.text.clone(); - if let Some(highlight_style) = old_highlight_id - .and_then(|h| h.style(&cx.editor_style.syntax)) - { - text_style = text_style.highlight(highlight_style); - } - div() - .pl(cx.anchor_x) - .child(EditorElement::new( - &rename_editor, - EditorStyle { - background: cx.theme().system().transparent, - local_player: cx.editor_style.local_player, - text: text_style, - scrollbar_width: cx.editor_style.scrollbar_width, - syntax: cx.editor_style.syntax.clone(), - status: cx.editor_style.status.clone(), - // todo!("what about the rest of the highlight style parts for inlays and suggestions?") - inlays_style: HighlightStyle { - color: Some(cx.theme().status().hint), - font_weight: Some(FontWeight::BOLD), - ..HighlightStyle::default() - }, - suggestions_style: HighlightStyle { - color: Some(cx.theme().status().predictive), - ..HighlightStyle::default() - }, - }, - )) - .into_any_element() - } - }), - 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( - &mut self, - _: &ConfirmRename, - cx: &mut ViewContext, - ) -> Option>> { - let rename = self.take_rename(false, cx)?; - let workspace = self.workspace()?; - let (start_buffer, start) = self - .buffer - .read(cx) - .text_anchor_for_position(rename.range.start.clone(), cx)?; - let (end_buffer, end) = self - .buffer - .read(cx) - .text_anchor_for_position(rename.range.end.clone(), cx)?; - if start_buffer != end_buffer { - return None; - } - - let buffer = start_buffer; - let range = start..end; - let old_name = rename.old_name; - let new_name = rename.editor.read(cx).text(cx); - - let rename = workspace - .read(cx) - .project() - .clone() - .update(cx, |project, cx| { - project.perform_rename(buffer.clone(), range.start, new_name.clone(), true, cx) - }); - let workspace = workspace.downgrade(); - - Some(cx.spawn(|editor, 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()?; - if rename.editor.focus_handle(cx).is_focused(cx) { - cx.focus(&self.focus_handle); - } - - 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_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(); - }) - .ok(); - - 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 { - log::error!("unexpectedly ended a transaction that wasn't started by this editor"); - } - - cx.emit(EditorEvent::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 set_gutter_hovered(&mut self, hovered: bool, cx: &mut ViewContext) { - if hovered != self.gutter_hovered { - 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 text_option(&self, cx: &AppContext) -> Option { - let text = self.text(cx); - let text = text.trim(); - - if text.is_empty() { - return None; - } - - Some(text.to_string()) - } - - 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_style(&mut self, style: EditorStyle, cx: &mut ViewContext) { - let rem_size = cx.rem_size(); - self.display_map.update(cx, |map, cx| { - map.set_font( - style.text.font(), - style.text.font_size.to_pixels(rem_size), - cx, - ) - }); - self.style = Some(style); - } - - #[cfg(any(test, feature = "test-support"))] - pub fn style(&self) -> Option<&EditorStyle> { - self.style.as_ref() - } - - // Called by the element. This method is not designed to be called outside of the editor - // element's layout code because it does not notify when rewrapping is computed synchronously. - pub(crate) 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(&ThemeColors) -> Hsla, - 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(&ThemeColors) -> Hsla, - 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, Hsla)> { - 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 = cx.theme().colors(); - self.background_highlights_in_range(start..end, &snapshot, theme) - } - - 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: &ThemeColors, - ) -> Vec<(Range, Hsla)> { - 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: &WindowContext) -> bool { - self.blink_manager.read(cx).visible() && self.focus_handle.is_focused(cx) - } - - 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(EditorEvent::BufferEdited); - cx.emit(SearchEvent::MatchesInvalidated); - - 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(EditorEvent::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(EditorEvent::ExcerptsRemoved { ids: ids.clone() }) - } - multi_buffer::Event::Reparsed => cx.emit(EditorEvent::Reparsed), - multi_buffer::Event::DirtyChanged => cx.emit(EditorEvent::DirtyChanged), - multi_buffer::Event::Saved => cx.emit(EditorEvent::Saved), - multi_buffer::Event::FileHandleChanged | multi_buffer::Event::Reloaded => { - cx.emit(EditorEvent::TitleChanged) - } - multi_buffer::Event::DiffBaseChanged => cx.emit(EditorEvent::DiffBaseChanged), - multi_buffer::Event::Closed => cx.emit(EditorEvent::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(&mut self, _: &OpenExcerpts, cx: &mut ViewContext) { - let buffer = self.buffer.read(cx); - if buffer.is_singleton() { - cx.propagate(); - return; - } - - let Some(workspace) = self.workspace() else { - cx.propagate(); - return; - }; - - let mut new_selections_by_buffer = HashMap::default(); - for selection in self.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) - } - } - - self.push_to_nav_history(self.selections.newest_anchor().head(), None, cx); - - // 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.window_context().defer(move |cx| { - workspace.update(cx, |workspace, cx| { - let pane = workspace.active_pane().clone(); - pane.update(cx, |pane, _| pane.disable_history()); - - 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( - &mut self, - path: ProjectPath, - position: Point, - anchor: language::Anchor, - cx: &mut ViewContext, - ) { - let workspace = self.workspace(); - cx.spawn(|_, mut cx| async move { - let workspace = workspace.ok_or_else(|| anyhow!("cannot jump without workspace"))?; - let editor = workspace.update(&mut cx, |workspace, cx| { - workspace.open_path(path, None, true, cx) - })?; - 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 = *TelemetrySettings::get_global(cx); - - telemetry.report_copilot_event( - telemetry_settings, - suggestion_id, - suggestion_accepted, - file_extension, - ) - } - - #[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 = *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(); - telemetry.report_editor_event( - telemetry_settings, - file_extension, - vim_mode, - operation, - copilot_enabled, - copilot_enabled_for_language, - ) - } - - /// 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 Some(style) = self.style.as_ref() else { - return; - }; - - for chunk in chunks { - let highlight = chunk - .syntax_highlight_id - .and_then(|id| id.name(&style.syntax)); - 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(EditorEvent::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 focus(&self, cx: &mut WindowContext) { - cx.focus(&self.focus_handle) - } - - pub fn is_focused(&self, cx: &WindowContext) -> bool { - self.focus_handle.is_focused(cx) - } - - fn handle_focus(&mut self, cx: &mut ViewContext) { - cx.emit(EditorEvent::Focused); - - if let Some(rename) = self.pending_rename.as_ref() { - let rename_editor_focus_handle = rename.editor.read(cx).focus_handle.clone(); - cx.focus(&rename_editor_focus_handle); - } else { - self.blink_manager.update(cx, BlinkManager::enable); - 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, - ); - } - }); - } - } - - pub fn handle_blur(&mut self, cx: &mut ViewContext) { - 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(EditorEvent::Blurred); - cx.notify(); - } - - pub fn register_action( - &mut self, - listener: impl Fn(&A, &mut WindowContext) + 'static, - ) -> &mut Self { - let listener = Arc::new(listener); - - self.editor_actions.push(Box::new(move |cx| { - let _view = cx.view().clone(); - let cx = cx.window_context(); - let listener = listener.clone(); - cx.on_action(TypeId::of::(), move |action, phase, cx| { - let action = action.downcast_ref().unwrap(); - if phase == DispatchPhase::Bubble { - listener(action, cx) - } - }) - })); - self - } -} - -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 EditorEvent { - 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, -} - -impl EventEmitter for Editor {} - -impl FocusableView for Editor { - fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for Editor { - fn render<'a>(&mut self, cx: &mut ViewContext<'a, Self>) -> impl IntoElement { - let settings = ThemeSettings::get_global(cx); - let text_style = match self.mode { - EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle { - color: cx.theme().colors().editor_foreground, - font_family: settings.ui_font.family.clone(), - font_features: settings.ui_font.features, - font_size: rems(0.875).into(), - font_weight: FontWeight::NORMAL, - font_style: FontStyle::Normal, - line_height: relative(settings.buffer_line_height.value()), - background_color: None, - underline: None, - white_space: WhiteSpace::Normal, - }, - - EditorMode::Full => TextStyle { - color: cx.theme().colors().editor_foreground, - font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features, - font_size: settings.buffer_font_size(cx).into(), - font_weight: FontWeight::NORMAL, - font_style: FontStyle::Normal, - line_height: relative(settings.buffer_line_height.value()), - background_color: None, - underline: None, - white_space: WhiteSpace::Normal, - }, - }; - - let background = match self.mode { - EditorMode::SingleLine => cx.theme().system().transparent, - EditorMode::AutoHeight { max_lines: _ } => cx.theme().system().transparent, - EditorMode::Full => cx.theme().colors().editor_background, - }; - - EditorElement::new( - cx.view(), - EditorStyle { - background, - local_player: cx.theme().players().local(), - text: text_style, - scrollbar_width: px(12.), - syntax: cx.theme().syntax().clone(), - status: cx.theme().status().clone(), - // todo!("what about the rest of the highlight style parts?") - inlays_style: HighlightStyle { - color: Some(cx.theme().status().hint), - font_weight: Some(FontWeight::BOLD), - ..HighlightStyle::default() - }, - suggestions_style: HighlightStyle { - color: Some(cx.theme().status().predictive), - ..HighlightStyle::default() - }, - }, - ) - } -} - -impl InputHandler for Editor { - fn text_for_range( - &mut self, - range_utf16: Range, - cx: &mut ViewContext, - ) -> Option { - Some( - self.buffer - .read(cx) - .read(cx) - .text_for_range(OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end)) - .collect(), - ) - } - - fn selected_text_range(&mut self, cx: &mut ViewContext) -> 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: &mut ViewContext) -> 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(EditorEvent::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(EditorEvent::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(EditorEvent::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(EditorEvent::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(), - HighlightStyle::default(), // todo!() 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 bounds_for_range( - &mut self, - range_utf16: Range, - element_bounds: gpui::Bounds, - cx: &mut ViewContext, - ) -> Option> { - let text_layout_details = self.text_layout_details(cx); - let style = &text_layout_details.editor_style; - let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); - let font_size = style.text.font_size.to_pixels(cx.rem_size()); - let line_height = style.text.line_height_in_pixels(cx.rem_size()); - let em_width = cx - .text_system() - .typographic_bounds(font_id, font_size, 'm') - .unwrap() - .size - .width; - - let snapshot = self.snapshot(cx); - let scroll_position = snapshot.scroll_position(); - let scroll_left = scroll_position.x * em_width; - - let start = OffsetUtf16(range_utf16.start).to_display_point(&snapshot); - let x = snapshot.x_for_display_point(start, &text_layout_details) - scroll_left - + self.gutter_width; - let y = line_height * (start.row() as f32 - scroll_position.y); - - Some(Bounds { - origin: element_bounds.origin + point(x, y), - size: size(em_width, line_height), - }) - } -} - -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] - } -} - -pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> RenderBlock { - let (text_without_backticks, code_ranges) = highlight_diagnostic_message(&diagnostic); - - Arc::new(move |cx: &mut BlockContext| { - let color = Some(cx.theme().colors().text_accent); - let group_id: SharedString = cx.block_id.to_string().into(); - // TODO: Nate: We should tint the background of the block with the severity color - // We need to extend the theme before we can do this - h_stack() - .id(cx.block_id) - .group(group_id.clone()) - .relative() - .pl(cx.anchor_x) - .size_full() - .gap_2() - .child( - StyledText::new(text_without_backticks.clone()).with_highlights( - &cx.text_style(), - code_ranges.iter().map(|range| { - ( - range.clone(), - HighlightStyle { - color, - ..Default::default() - }, - ) - }), - ), - ) - .child( - IconButton::new(("copy-block", cx.block_id), Icon::Copy) - .icon_color(Color::Muted) - .size(ButtonSize::Compact) - .style(ButtonStyle::Transparent) - .visible_on_hover(group_id) - .on_click(cx.listener({ - let message = diagnostic.message.clone(); - move |_, _, cx| cx.write_to_clipboard(ClipboardItem::new(message.clone())) - })) - .tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)), - ) - .into_any_element() - }) -} - -pub fn highlight_diagnostic_message(diagnostic: &Diagnostic) -> (SharedString, Vec>) { - let mut text_without_backticks = String::new(); - let mut code_ranges = Vec::new(); - - if let Some(source) = &diagnostic.source { - text_without_backticks.push_str(&source); - code_ranges.push(0..source.len()); - text_without_backticks.push_str(": "); - } - - let mut prev_offset = 0; - let mut in_code_block = false; - for (ix, _) in diagnostic - .message - .match_indices('`') - .chain([(diagnostic.message.len(), "")]) - { - let prev_len = text_without_backticks.len(); - text_without_backticks.push_str(&diagnostic.message[prev_offset..ix]); - prev_offset = ix + 1; - if in_code_block { - code_ranges.push(prev_len..text_without_backticks.len()); - in_code_block = false; - } else { - in_code_block = true; - } - } - - (text_without_backticks.into(), code_ranges) -} - -pub fn diagnostic_style(severity: DiagnosticSeverity, valid: bool, colors: &StatusColors) -> Hsla { - match (severity, valid) { - (DiagnosticSeverity::ERROR, true) => colors.error, - (DiagnosticSeverity::ERROR, false) => colors.error, - (DiagnosticSeverity::WARNING, true) => colors.warning, - (DiagnosticSeverity::WARNING, false) => colors.warning, - (DiagnosticSeverity::INFORMATION, true) => colors.info, - (DiagnosticSeverity::INFORMATION, false) => colors.info, - (DiagnosticSeverity::HINT, true) => colors.info, - (DiagnosticSeverity::HINT, false) => colors.info, - _ => colors.ignored, - } -} - -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 deleted file mode 100644 index fd7e2feea3..0000000000 --- a/crates/editor2/src/editor_settings.rs +++ /dev/null @@ -1,72 +0,0 @@ -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::Settings; - -#[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, - pub seed_search_query_from_cursor: SeedQuerySetting, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum SeedQuerySetting { - Always, - Selection, - Never, -} - -#[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, - pub seed_search_query_from_cursor: Option, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -pub struct ScrollbarContent { - pub show: Option, - pub git_diff: Option, - pub selections: Option, -} - -impl Settings for EditorSettings { - const KEY: Option<&'static str> = None; - - type FileContent = EditorSettingsContent; - - fn load( - default_value: &Self::FileContent, - user_values: &[&Self::FileContent], - _: &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 deleted file mode 100644 index 2b5c97bac5..0000000000 --- a/crates/editor2/src/element.rs +++ /dev/null @@ -1,3815 +0,0 @@ -use crate::{ - display_map::{ - BlockContext, BlockStyle, DisplaySnapshot, FoldStatus, HighlightedChunk, ToDisplayPoint, - TransformBlock, - }, - editor_settings::ShowScrollbar, - git::{diff_hunk_to_display, DisplayDiffHunk}, - hover_popover::{ - self, 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, show_link_definition, - update_go_to_definition_link, update_inlay_link_and_hover_points, GoToDefinitionTrigger, - LinkGoToDefinitionState, - }, - mouse_context_menu, - scroll::scroll_amount::ScrollAmount, - CursorShape, DisplayPoint, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, - HalfPageDown, HalfPageUp, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, SelectPhase, - Selection, SoftWrap, ToPoint, MAX_LINE_LEN, -}; -use anyhow::Result; -use collections::{BTreeMap, HashMap}; -use git::diff::DiffHunkStatus; -use gpui::{ - div, fill, outline, overlay, point, px, quad, relative, size, transparent_black, Action, - AnchorCorner, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Corners, - CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Hsla, InteractiveBounds, - InteractiveElement, IntoElement, ModifiersChangedEvent, MouseButton, MouseDownEvent, - MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, ScrollWheelEvent, ShapedLine, - SharedString, Size, StackingOrder, StatefulInteractiveElement, Style, Styled, TextRun, - TextStyle, View, ViewContext, WindowContext, -}; -use itertools::Itertools; -use language::language_settings::ShowWhitespaceSetting; -use multi_buffer::Anchor; -use project::{ - project_settings::{GitGutterSetting, ProjectSettings}, - ProjectPath, -}; -use settings::Settings; -use smallvec::SmallVec; -use std::{ - any::TypeId, - borrow::Cow, - cmp::{self, Ordering}, - fmt::Write, - iter, - ops::Range, - sync::Arc, -}; -use sum_tree::Bias; -use theme::{ActiveTheme, PlayerColor}; -use ui::prelude::*; -use ui::{h_stack, ButtonLike, ButtonStyle, IconButton, Tooltip}; -use util::ResultExt; -use workspace::item::Item; - -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 { - editor: View, - style: EditorStyle, -} - -impl EditorElement { - pub fn new(editor: &View, style: EditorStyle) -> Self { - Self { - editor: editor.clone(), - style, - } - } - - fn register_actions(&self, cx: &mut WindowContext) { - let view = &self.editor; - view.update(cx, |editor, cx| { - for action in editor.editor_actions.iter() { - (action)(cx) - } - }); - - crate::rust_analyzer_ext::apply_related_actions(view, cx); - register_action(view, cx, Editor::move_left); - register_action(view, cx, Editor::move_right); - register_action(view, cx, Editor::move_down); - register_action(view, cx, Editor::move_up); - register_action(view, cx, Editor::cancel); - register_action(view, cx, Editor::newline); - register_action(view, cx, Editor::newline_above); - register_action(view, cx, Editor::newline_below); - register_action(view, cx, Editor::backspace); - register_action(view, cx, Editor::delete); - register_action(view, cx, Editor::tab); - register_action(view, cx, Editor::tab_prev); - register_action(view, cx, Editor::indent); - register_action(view, cx, Editor::outdent); - register_action(view, cx, Editor::delete_line); - register_action(view, cx, Editor::join_lines); - register_action(view, cx, Editor::sort_lines_case_sensitive); - register_action(view, cx, Editor::sort_lines_case_insensitive); - register_action(view, cx, Editor::reverse_lines); - register_action(view, cx, Editor::shuffle_lines); - register_action(view, cx, Editor::convert_to_upper_case); - register_action(view, cx, Editor::convert_to_lower_case); - register_action(view, cx, Editor::convert_to_title_case); - register_action(view, cx, Editor::convert_to_snake_case); - register_action(view, cx, Editor::convert_to_kebab_case); - register_action(view, cx, Editor::convert_to_upper_camel_case); - register_action(view, cx, Editor::convert_to_lower_camel_case); - register_action(view, cx, Editor::delete_to_previous_word_start); - register_action(view, cx, Editor::delete_to_previous_subword_start); - register_action(view, cx, Editor::delete_to_next_word_end); - register_action(view, cx, Editor::delete_to_next_subword_end); - register_action(view, cx, Editor::delete_to_beginning_of_line); - register_action(view, cx, Editor::delete_to_end_of_line); - register_action(view, cx, Editor::cut_to_end_of_line); - register_action(view, cx, Editor::duplicate_line); - register_action(view, cx, Editor::move_line_up); - register_action(view, cx, Editor::move_line_down); - register_action(view, cx, Editor::transpose); - register_action(view, cx, Editor::cut); - register_action(view, cx, Editor::copy); - register_action(view, cx, Editor::paste); - register_action(view, cx, Editor::undo); - register_action(view, cx, Editor::redo); - register_action(view, cx, Editor::move_page_up); - register_action(view, cx, Editor::move_page_down); - register_action(view, cx, Editor::next_screen); - register_action(view, cx, Editor::scroll_cursor_top); - register_action(view, cx, Editor::scroll_cursor_center); - register_action(view, cx, Editor::scroll_cursor_bottom); - register_action(view, cx, |editor, _: &LineDown, cx| { - editor.scroll_screen(&ScrollAmount::Line(1.), cx) - }); - register_action(view, cx, |editor, _: &LineUp, cx| { - editor.scroll_screen(&ScrollAmount::Line(-1.), cx) - }); - register_action(view, cx, |editor, _: &HalfPageDown, cx| { - editor.scroll_screen(&ScrollAmount::Page(0.5), cx) - }); - register_action(view, cx, |editor, _: &HalfPageUp, cx| { - editor.scroll_screen(&ScrollAmount::Page(-0.5), cx) - }); - register_action(view, cx, |editor, _: &PageDown, cx| { - editor.scroll_screen(&ScrollAmount::Page(1.), cx) - }); - register_action(view, cx, |editor, _: &PageUp, cx| { - editor.scroll_screen(&ScrollAmount::Page(-1.), cx) - }); - register_action(view, cx, Editor::move_to_previous_word_start); - register_action(view, cx, Editor::move_to_previous_subword_start); - register_action(view, cx, Editor::move_to_next_word_end); - register_action(view, cx, Editor::move_to_next_subword_end); - register_action(view, cx, Editor::move_to_beginning_of_line); - register_action(view, cx, Editor::move_to_end_of_line); - register_action(view, cx, Editor::move_to_start_of_paragraph); - register_action(view, cx, Editor::move_to_end_of_paragraph); - register_action(view, cx, Editor::move_to_beginning); - register_action(view, cx, Editor::move_to_end); - register_action(view, cx, Editor::select_up); - register_action(view, cx, Editor::select_down); - register_action(view, cx, Editor::select_left); - register_action(view, cx, Editor::select_right); - register_action(view, cx, Editor::select_to_previous_word_start); - register_action(view, cx, Editor::select_to_previous_subword_start); - register_action(view, cx, Editor::select_to_next_word_end); - register_action(view, cx, Editor::select_to_next_subword_end); - register_action(view, cx, Editor::select_to_beginning_of_line); - register_action(view, cx, Editor::select_to_end_of_line); - register_action(view, cx, Editor::select_to_start_of_paragraph); - register_action(view, cx, Editor::select_to_end_of_paragraph); - register_action(view, cx, Editor::select_to_beginning); - register_action(view, cx, Editor::select_to_end); - register_action(view, cx, Editor::select_all); - register_action(view, cx, |editor, action, cx| { - editor.select_all_matches(action, cx).log_err(); - }); - register_action(view, cx, Editor::select_line); - register_action(view, cx, Editor::split_selection_into_lines); - register_action(view, cx, Editor::add_selection_above); - register_action(view, cx, Editor::add_selection_below); - register_action(view, cx, |editor, action, cx| { - editor.select_next(action, cx).log_err(); - }); - register_action(view, cx, |editor, action, cx| { - editor.select_previous(action, cx).log_err(); - }); - register_action(view, cx, Editor::toggle_comments); - register_action(view, cx, Editor::select_larger_syntax_node); - register_action(view, cx, Editor::select_smaller_syntax_node); - register_action(view, cx, Editor::move_to_enclosing_bracket); - register_action(view, cx, Editor::undo_selection); - register_action(view, cx, Editor::redo_selection); - register_action(view, cx, Editor::go_to_diagnostic); - register_action(view, cx, Editor::go_to_prev_diagnostic); - register_action(view, cx, Editor::go_to_hunk); - register_action(view, cx, Editor::go_to_prev_hunk); - register_action(view, cx, Editor::go_to_definition); - register_action(view, cx, Editor::go_to_definition_split); - register_action(view, cx, Editor::go_to_type_definition); - register_action(view, cx, Editor::go_to_type_definition_split); - register_action(view, cx, Editor::fold); - register_action(view, cx, Editor::fold_at); - register_action(view, cx, Editor::unfold_lines); - register_action(view, cx, Editor::unfold_at); - register_action(view, cx, Editor::fold_selected_ranges); - register_action(view, cx, Editor::show_completions); - register_action(view, cx, Editor::toggle_code_actions); - register_action(view, cx, Editor::open_excerpts); - register_action(view, cx, Editor::toggle_soft_wrap); - register_action(view, cx, Editor::toggle_inlay_hints); - register_action(view, cx, hover_popover::hover); - register_action(view, cx, Editor::reveal_in_finder); - register_action(view, cx, Editor::copy_path); - register_action(view, cx, Editor::copy_relative_path); - register_action(view, cx, Editor::copy_highlight_json); - register_action(view, cx, |editor, action, cx| { - if let Some(task) = editor.format(action, cx) { - task.detach_and_log_err(cx); - } else { - cx.propagate(); - } - }); - register_action(view, cx, Editor::restart_language_server); - register_action(view, cx, Editor::show_character_palette); - register_action(view, cx, |editor, action, cx| { - if let Some(task) = editor.confirm_completion(action, cx) { - task.detach_and_log_err(cx); - } else { - cx.propagate(); - } - }); - register_action(view, cx, |editor, action, cx| { - if let Some(task) = editor.confirm_code_action(action, cx) { - task.detach_and_log_err(cx); - } else { - cx.propagate(); - } - }); - register_action(view, cx, |editor, action, cx| { - if let Some(task) = editor.rename(action, cx) { - task.detach_and_log_err(cx); - } else { - cx.propagate(); - } - }); - register_action(view, cx, |editor, action, cx| { - if let Some(task) = editor.confirm_rename(action, cx) { - task.detach_and_log_err(cx); - } else { - cx.propagate(); - } - }); - register_action(view, cx, |editor, action, cx| { - if let Some(task) = editor.find_all_references(action, cx) { - task.detach_and_log_err(cx); - } else { - cx.propagate(); - } - }); - register_action(view, cx, Editor::next_copilot_suggestion); - register_action(view, cx, Editor::previous_copilot_suggestion); - register_action(view, cx, Editor::copilot_suggest); - register_action(view, cx, Editor::context_menu_first); - register_action(view, cx, Editor::context_menu_prev); - register_action(view, cx, Editor::context_menu_next); - register_action(view, cx, Editor::context_menu_last); - } - - fn register_key_listeners(&self, cx: &mut WindowContext) { - cx.on_key_event({ - let editor = self.editor.clone(); - move |event: &ModifiersChangedEvent, phase, cx| { - if phase != DispatchPhase::Bubble { - return; - } - - if editor.update(cx, |editor, cx| Self::modifiers_changed(editor, event, cx)) { - cx.stop_propagation(); - } - } - }); - } - - pub(crate) fn modifiers_changed( - editor: &mut Editor, - event: &ModifiersChangedEvent, - cx: &mut ViewContext, - ) -> bool { - let pending_selection = editor.has_pending_selection(); - - if let Some(point) = &editor.link_go_to_definition_state.last_trigger_point { - if event.command && !pending_selection { - let point = point.clone(); - let snapshot = editor.snapshot(cx); - let kind = point.definition_kind(event.shift); - - show_link_definition(kind, editor, point, snapshot, cx); - return false; - } - } - - { - 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); - } - - false - } - - fn mouse_left_down( - editor: &mut Editor, - event: &MouseDownEvent, - position_map: &PositionMap, - text_bounds: Bounds, - gutter_bounds: Bounds, - stacking_order: &StackingOrder, - cx: &mut ViewContext, - ) { - let mut click_count = event.click_count; - let modifiers = event.modifiers; - - if gutter_bounds.contains(&event.position) { - click_count = 3; // Simulate triple-click when clicking the gutter to select lines - } else if !text_bounds.contains(&event.position) { - return; - } - if !cx.was_top_layer(&event.position, stacking_order) { - return; - } - - let point_for_position = position_map.point_for_position(text_bounds, event.position); - let position = point_for_position.previous_valid; - if modifiers.shift && modifiers.alt { - editor.select( - SelectPhase::BeginColumnar { - position, - goal_column: point_for_position.exact_unclipped.column(), - }, - cx, - ); - } else if modifiers.shift && !modifiers.control && !modifiers.alt && !modifiers.command { - editor.select( - SelectPhase::Extend { - position, - click_count, - }, - cx, - ); - } else { - editor.select( - SelectPhase::Begin { - position, - add: modifiers.alt, - click_count, - }, - cx, - ); - } - - cx.stop_propagation(); - } - - fn mouse_right_down( - editor: &mut Editor, - event: &MouseDownEvent, - position_map: &PositionMap, - text_bounds: Bounds, - cx: &mut ViewContext, - ) { - if !text_bounds.contains(&event.position) { - return; - } - let point_for_position = position_map.point_for_position(text_bounds, event.position); - mouse_context_menu::deploy_context_menu( - editor, - event.position, - point_for_position.previous_valid, - cx, - ); - cx.stop_propagation(); - } - - fn mouse_up( - editor: &mut Editor, - event: &MouseUpEvent, - position_map: &PositionMap, - text_bounds: Bounds, - stacking_order: &StackingOrder, - cx: &mut ViewContext, - ) { - 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 - && event.modifiers.command - && text_bounds.contains(&event.position) - && cx.was_top_layer(&event.position, stacking_order) - { - let point = position_map.point_for_position(text_bounds, event.position); - let could_be_inlay = point.as_valid().is_none(); - let split = event.modifiers.alt; - if event.modifiers.shift || could_be_inlay { - go_to_fetched_type_definition(editor, point, split, cx); - } else { - go_to_fetched_definition(editor, point, split, cx); - } - - cx.stop_propagation(); - } else if end_selection { - cx.stop_propagation(); - } - } - - fn mouse_dragged( - editor: &mut Editor, - event: &MouseMoveEvent, - position_map: &PositionMap, - text_bounds: Bounds, - _gutter_bounds: Bounds, - _stacking_order: &StackingOrder, - cx: &mut ViewContext, - ) { - if !editor.has_pending_selection() { - return; - } - - let point_for_position = position_map.point_for_position(text_bounds, event.position); - let mut scroll_delta = gpui::Point::::default(); - let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0); - let top = text_bounds.origin.y + vertical_margin; - let bottom = text_bounds.lower_left().y - vertical_margin; - if event.position.y < top { - scroll_delta.y = -scale_vertical_mouse_autoscroll_delta(top - event.position.y); - } - if event.position.y > bottom { - scroll_delta.y = scale_vertical_mouse_autoscroll_delta(event.position.y - bottom); - } - - let horizontal_margin = position_map.line_height.min(text_bounds.size.width / 3.0); - let left = text_bounds.origin.x + horizontal_margin; - let right = text_bounds.upper_right().x - horizontal_margin; - if event.position.x < left { - scroll_delta.x = -scale_horizontal_mouse_autoscroll_delta(left - event.position.x); - } - if event.position.x > right { - scroll_delta.x = scale_horizontal_mouse_autoscroll_delta(event.position.x - right); - } - - 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::default(), &position_map.scroll_max), - }, - cx, - ); - } - - fn mouse_moved( - editor: &mut Editor, - event: &MouseMoveEvent, - position_map: &PositionMap, - text_bounds: Bounds, - gutter_bounds: Bounds, - stacking_order: &StackingOrder, - cx: &mut ViewContext, - ) { - let modifiers = event.modifiers; - let text_hovered = text_bounds.contains(&event.position); - let gutter_hovered = gutter_bounds.contains(&event.position); - let was_top = cx.was_top_layer(&event.position, stacking_order); - - editor.set_gutter_hovered(gutter_hovered, cx); - - // Don't trigger hover popover if mouse is hovering over context menu - if text_hovered && was_top { - let point_for_position = position_map.point_for_position(text_bounds, event.position); - - match point_for_position.as_valid() { - Some(point) => { - update_go_to_definition_link( - editor, - Some(GoToDefinitionTrigger::Text(point)), - modifiers.command, - modifiers.shift, - cx, - ); - hover_at(editor, Some(point), cx); - } - None => { - update_inlay_link_and_hover_points( - &position_map.snapshot, - point_for_position, - editor, - modifiers.command, - modifiers.shift, - cx, - ); - } - } - } else { - update_go_to_definition_link(editor, None, modifiers.command, modifiers.shift, cx); - hover_at(editor, None, cx); - if gutter_hovered && was_top { - cx.stop_propagation(); - } - } - } - - fn scroll( - editor: &mut Editor, - event: &ScrollWheelEvent, - position_map: &PositionMap, - bounds: &InteractiveBounds, - cx: &mut ViewContext, - ) { - if !bounds.visibly_contains(&event.position, cx) { - return; - } - - let line_height = position_map.line_height; - let max_glyph_width = position_map.em_width; - let (delta, axis) = match event.delta { - gpui::ScrollDelta::Pixels(mut pixels) => { - //Trackpad - let axis = position_map.snapshot.ongoing_scroll.filter(&mut pixels); - (pixels, axis) - } - - gpui::ScrollDelta::Lines(lines) => { - //Not trackpad - let pixels = point(lines.x * max_glyph_width, lines.y * line_height); - (pixels, None) - } - }; - - let scroll_position = position_map.snapshot.scroll_position(); - let x = f32::from((scroll_position.x * max_glyph_width - delta.x) / max_glyph_width); - let y = f32::from((scroll_position.y * line_height - delta.y) / line_height); - let scroll_position = point(x, y).clamp(&point(0., 0.), &position_map.scroll_max); - editor.scroll(scroll_position, axis, cx); - cx.stop_propagation(); - } - - fn paint_background( - &self, - gutter_bounds: Bounds, - text_bounds: Bounds, - layout: &LayoutState, - cx: &mut WindowContext, - ) { - let bounds = gutter_bounds.union(&text_bounds); - let scroll_top = - layout.position_map.snapshot.scroll_position().y * layout.position_map.line_height; - let gutter_bg = cx.theme().colors().editor_gutter_background; - cx.paint_quad(fill(gutter_bounds, gutter_bg)); - cx.paint_quad(fill(text_bounds, self.style.background)); - - 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 = point( - bounds.origin.x, - bounds.origin.y + (layout.position_map.line_height * *start_row as f32) - - scroll_top, - ); - let size = size( - bounds.size.width, - layout.position_map.line_height * (end_row - start_row + 1) as f32, - ); - let active_line_bg = cx.theme().colors().editor_active_line_background; - cx.paint_quad(fill(Bounds { origin, size }, active_line_bg)); - } - } - - if let Some(highlighted_rows) = &layout.highlighted_rows { - let origin = point( - bounds.origin.x, - bounds.origin.y - + (layout.position_map.line_height * highlighted_rows.start as f32) - - scroll_top, - ); - let size = size( - bounds.size.width, - layout.position_map.line_height * highlighted_rows.len() as f32, - ); - let highlighted_line_bg = cx.theme().colors().editor_highlighted_line_background; - cx.paint_quad(fill(Bounds { origin, size }, highlighted_line_bg)); - } - - 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 { - cx.theme().colors().editor_active_wrap_guide - } else { - cx.theme().colors().editor_wrap_guide - }; - cx.paint_quad(fill( - Bounds { - origin: point(x, text_bounds.origin.y), - size: size(px(1.), text_bounds.size.height), - }, - color, - )); - } - } - } - - fn paint_gutter( - &mut self, - bounds: Bounds, - layout: &mut LayoutState, - cx: &mut WindowContext, - ) { - 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!( - ProjectSettings::get_global(cx).git.git_gutter, - Some(GitGutterSetting::TrackedFiles) - ); - - if show_gutter { - Self::paint_diff_hunks(bounds, layout, cx); - } - - for (ix, line) in layout.line_numbers.iter().enumerate() { - if let Some(line) = line { - let line_origin = bounds.origin - + point( - bounds.size.width - line.width - layout.gutter_padding, - ix as f32 * line_height - (scroll_top % line_height), - ); - - line.paint(line_origin, line_height, cx).log_err(); - } - } - - cx.with_z_index(1, |cx| { - for (ix, fold_indicator) in layout.fold_indicators.drain(..).enumerate() { - if let Some(fold_indicator) = fold_indicator { - let mut fold_indicator = fold_indicator.into_any_element(); - let available_space = size( - AvailableSpace::MinContent, - AvailableSpace::Definite(line_height * 0.55), - ); - let fold_indicator_size = fold_indicator.measure(available_space, cx); - - let position = point( - bounds.size.width - layout.gutter_padding, - ix as f32 * line_height - (scroll_top % line_height), - ); - let centering_offset = point( - (layout.gutter_padding + layout.gutter_margin - fold_indicator_size.width) - / 2., - (line_height - fold_indicator_size.height) / 2., - ); - let origin = bounds.origin + position + centering_offset; - fold_indicator.draw(origin, available_space, cx); - } - } - - if let Some(indicator) = layout.code_actions_indicator.take() { - let mut button = indicator.button.into_any_element(); - let available_space = size( - AvailableSpace::MinContent, - AvailableSpace::Definite(line_height), - ); - let indicator_size = button.measure(available_space, cx); - - let mut x = Pixels::ZERO; - let mut y = indicator.row as f32 * line_height - scroll_top; - // Center indicator. - x += ((layout.gutter_padding + layout.gutter_margin) - indicator_size.width) / 2.; - y += (line_height - indicator_size.height) / 2.; - - button.draw(bounds.origin + point(x, y), available_space, cx); - } - }); - } - - fn paint_diff_hunks(bounds: Bounds, layout: &LayoutState, cx: &mut WindowContext) { - 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 = 0.275 * line_height; - let highlight_origin = bounds.origin + point(-width, start_y); - let highlight_size = size(width * 2., end_y - start_y); - let highlight_bounds = Bounds::new(highlight_origin, highlight_size); - cx.paint_quad(quad( - highlight_bounds, - Corners::all(1. * line_height), - gpui::yellow(), // todo!("use the right color") - Edges::default(), - transparent_black(), - )); - - continue; - } - - DisplayDiffHunk::Unfolded { - display_row_range, - status, - } => (display_row_range, status), - }; - - let color = match status { - DiffHunkStatus::Added => cx.theme().status().created, - DiffHunkStatus::Modified => cx.theme().status().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 = 0.275 * line_height; - let highlight_origin = bounds.origin + point(-width, start_y); - let highlight_size = size(width * 2., end_y - start_y); - let highlight_bounds = Bounds::new(highlight_origin, highlight_size); - cx.paint_quad(quad( - highlight_bounds, - Corners::all(1. * line_height), - cx.theme().status().deleted, - Edges::default(), - transparent_black(), - )); - - 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 = 0.275 * line_height; - let highlight_origin = bounds.origin + point(-width, start_y); - let highlight_size = size(width * 2., end_y - start_y); - let highlight_bounds = Bounds::new(highlight_origin, highlight_size); - cx.paint_quad(quad( - highlight_bounds, - Corners::all(0.05 * line_height), - color, // todo!("use the right color") - Edges::default(), - transparent_black(), - )); - } - } - - fn paint_text( - &mut self, - text_bounds: Bounds, - layout: &mut LayoutState, - cx: &mut WindowContext, - ) { - let start_row = layout.visible_display_row_range.start; - let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO); - let line_end_overshoot = 0.15 * layout.position_map.line_height; - let whitespace_setting = self - .editor - .read(cx) - .buffer - .read(cx) - .settings_at(0, cx) - .show_whitespaces; - - cx.with_content_mask( - Some(ContentMask { - bounds: text_bounds, - }), - |cx| { - let interactive_text_bounds = InteractiveBounds { - bounds: text_bounds, - stacking_order: cx.stacking_order().clone(), - }; - if interactive_text_bounds.visibly_contains(&cx.mouse_position(), cx) { - if self - .editor - .read(cx) - .link_go_to_definition_state - .definitions - .is_empty() - { - cx.set_cursor_style(CursorStyle::IBeam); - } else { - cx.set_cursor_style(CursorStyle::PointingHand); - } - } - - let fold_corner_radius = 0.15 * layout.position_map.line_height; - cx.with_element_id(Some("folds"), |cx| { - let snapshot = &layout.position_map.snapshot; - for fold in snapshot.folds_in_range(layout.visible_anchor_range.clone()) { - let fold_range = fold.range.clone(); - let display_range = fold.range.start.to_display_point(&snapshot) - ..fold.range.end.to_display_point(&snapshot); - debug_assert_eq!(display_range.start.row(), display_range.end.row()); - let row = display_range.start.row(); - - let line_layout = &layout.position_map.line_layouts - [(row - layout.visible_display_row_range.start) as usize] - .line; - let start_x = content_origin.x - + line_layout.x_for_index(display_range.start.column() as usize) - - layout.position_map.scroll_position.x; - let start_y = content_origin.y - + row as f32 * layout.position_map.line_height - - layout.position_map.scroll_position.y; - let end_x = content_origin.x - + line_layout.x_for_index(display_range.end.column() as usize) - - layout.position_map.scroll_position.x; - - let fold_bounds = Bounds { - origin: point(start_x, start_y), - size: size(end_x - start_x, layout.position_map.line_height), - }; - - let fold_background = cx.with_z_index(1, |cx| { - div() - .id(fold.id) - .size_full() - .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) - .on_click(cx.listener_for( - &self.editor, - move |editor: &mut Editor, _, cx| { - editor.unfold_ranges( - [fold_range.start..fold_range.end], - true, - false, - cx, - ); - cx.stop_propagation(); - }, - )) - .draw_and_update_state( - fold_bounds.origin, - fold_bounds.size, - cx, - |fold_element_state, cx| { - if fold_element_state.is_active() { - cx.theme().colors().ghost_element_active - } else if fold_bounds.contains(&cx.mouse_position()) { - cx.theme().colors().ghost_element_hover - } else { - cx.theme().colors().ghost_element_background - } - }, - ) - }); - - self.paint_highlighted_range( - display_range.clone(), - fold_background, - fold_corner_radius, - fold_corner_radius * 2., - layout, - content_origin, - text_bounds, - cx, - ); - } - }); - - for (range, color) in &layout.highlighted_ranges { - self.paint_highlighted_range( - range.clone(), - *color, - Pixels::ZERO, - line_end_overshoot, - layout, - content_origin, - text_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, - text_bounds, - cx, - ); - - if selection.is_local && !selection.range.is_empty() { - invisible_display_ranges.push(selection.range.clone()); - } - - if !selection.is_local || self.editor.read(cx).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 == Pixels::ZERO { - 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, _)| { - // todo!() currently shape_line panics if text conatins newlines - let text = if character == '\n' { - SharedString::from(" ") - } else { - SharedString::from(character.to_string()) - }; - let len = text.len(); - cx.text_system() - .shape_line( - text, - cursor_row_layout.font_size, - &[TextRun { - len, - font: self.style.text.font(), - color: self.style.background, - background_color: None, - underline: None, - }], - ) - .log_err() - }) - } else { - None - }; - - let x = cursor_character_x - layout.position_map.scroll_position.x; - let y = cursor_position.row() as f32 - * layout.position_map.line_height - - layout.position_map.scroll_position.y; - if selection.is_newest { - self.editor.update(cx, |editor, _| { - editor.pixel_position_of_newest_cursor = Some(point( - text_bounds.origin.x + x + block_width / 2., - text_bounds.origin.y - + y - + layout.position_map.line_height / 2., - )) - }); - } - cursors.push(Cursor { - color: selection_style.cursor, - block_width, - origin: point(x, y), - line_height: layout.position_map.line_height, - shape: selection.cursor_shape, - block_text, - }); - } - } - } - } - - 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, - content_origin, - whitespace_setting, - &invisible_display_ranges, - cx, - ) - } - - cx.with_z_index(0, |cx| { - for cursor in cursors { - cursor.paint(content_origin, cx); - } - }); - }, - ) - } - - fn paint_overlays( - &mut self, - text_bounds: Bounds, - layout: &mut LayoutState, - cx: &mut WindowContext, - ) { - let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO); - let start_row = layout.visible_display_row_range.start; - if let Some((position, mut context_menu)) = layout.context_menu.take() { - let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); - let context_menu_size = context_menu.measure(available_space, cx); - - 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) - - layout.position_map.scroll_position.x; - let y = (position.row() + 1) as f32 * layout.position_map.line_height - - layout.position_map.scroll_position.y; - let mut list_origin = content_origin + point(x, y); - let list_width = context_menu_size.width; - let list_height = context_menu_size.height; - - // 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.viewport_size().width { - list_origin.x = (cx.viewport_size().width - list_width).max(Pixels::ZERO); - } - - if list_origin.y + list_height > text_bounds.lower_right().y { - list_origin.y -= layout.position_map.line_height + list_height; - } - - cx.break_content_mask(|cx| context_menu.draw(list_origin, available_space, cx)); - } - - if let Some((position, mut hover_popovers)) = layout.hover_popovers.take() { - let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); - - // 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].measure(available_space, cx); - let height_to_reserve = - first_size.height + 1.5 * MIN_POPOVER_LINE_HEIGHT * layout.position_map.line_height; - - // Compute Hovered Point - let x = hovered_row_layout.x_for_index(position.column() as usize) - - layout.position_map.scroll_position.x; - let y = position.row() as f32 * layout.position_map.line_height - - layout.position_map.scroll_position.y; - let hovered_point = content_origin + point(x, y); - - if hovered_point.y - height_to_reserve > Pixels::ZERO { - // There is enough space above. Render popovers above the hovered point - let mut current_y = hovered_point.y; - for mut hover_popover in hover_popovers { - let size = hover_popover.measure(available_space, cx); - let mut popover_origin = point(hovered_point.x, current_y - size.height); - - let x_out_of_bounds = - text_bounds.upper_right().x - (popover_origin.x + size.width); - if x_out_of_bounds < Pixels::ZERO { - popover_origin.x = popover_origin.x + x_out_of_bounds; - } - - cx.break_content_mask(|cx| { - hover_popover.draw(popover_origin, available_space, 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 mut hover_popover in hover_popovers { - let size = hover_popover.measure(available_space, cx); - let mut popover_origin = point(hovered_point.x, current_y); - - let x_out_of_bounds = - text_bounds.upper_right().x - (popover_origin.x + size.width); - if x_out_of_bounds < Pixels::ZERO { - popover_origin.x = popover_origin.x + x_out_of_bounds; - } - - hover_popover.draw(popover_origin, available_space, cx); - - current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP; - } - } - } - - if let Some(mouse_context_menu) = self.editor.read(cx).mouse_context_menu.as_ref() { - let element = overlay() - .position(mouse_context_menu.position) - .child(mouse_context_menu.context_menu.clone()) - .anchor(AnchorCorner::TopLeft) - .snap_to_window(); - element.into_any().draw( - gpui::Point::default(), - size(AvailableSpace::MinContent, AvailableSpace::MinContent), - cx, - ); - } - } - - fn scrollbar_left(&self, bounds: &Bounds) -> Pixels { - bounds.upper_right().x - self.style.scrollbar_width - } - - fn paint_scrollbar( - &mut self, - bounds: Bounds, - layout: &mut LayoutState, - cx: &mut WindowContext, - ) { - if layout.mode != EditorMode::Full { - return; - } - - let top = bounds.origin.y; - let bottom = bounds.lower_left().y; - let right = bounds.lower_right().x; - let left = self.scrollbar_left(&bounds); - let row_range = layout.scrollbar_row_range.clone(); - let max_row = layout.max_row as f32 + (row_range.end - row_range.start); - - let mut height = bounds.size.height; - let mut first_row_y_offset = px(0.0); - - // Impose a minimum height on the scrollbar thumb - let row_height = height / max_row; - let min_thumb_height = layout.position_map.line_height; - 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| -> Pixels { 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_corners(point(left, top), point(right, bottom)); - let thumb_bounds = Bounds::from_corners(point(left, thumb_top), point(right, thumb_bottom)); - - if layout.show_scrollbars { - cx.paint_quad(quad( - track_bounds, - Corners::default(), - cx.theme().colors().scrollbar_track_background, - Edges { - top: Pixels::ZERO, - right: Pixels::ZERO, - bottom: Pixels::ZERO, - left: px(1.), - }, - cx.theme().colors().scrollbar_track_border, - )); - let scrollbar_settings = EditorSettings::get_global(cx).scrollbar; - if layout.is_singleton && scrollbar_settings.selections { - let start_anchor = Anchor::min(); - let end_anchor = Anchor::max(); - let background_ranges = self - .editor - .read(cx) - .background_highlight_row_ranges::( - start_anchor..end_anchor, - &layout.position_map.snapshot, - 50000, - ); - for range in background_ranges { - let start_y = y_for_row(range.start().row() as f32); - let mut end_y = y_for_row(range.end().row() as f32); - if end_y - start_y < px(1.) { - end_y = start_y + px(1.); - } - let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y)); - cx.paint_quad(quad( - bounds, - Corners::default(), - cx.theme().status().info, - Edges { - top: Pixels::ZERO, - right: px(1.), - bottom: Pixels::ZERO, - left: px(1.), - }, - cx.theme().colors().scrollbar_thumb_border, - )); - } - } - - if layout.is_singleton && scrollbar_settings.git_diff { - 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 < px(1.) { - end_y = start_y + px(1.); - } - let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y)); - - let color = match hunk.status() { - DiffHunkStatus::Added => cx.theme().status().created, - DiffHunkStatus::Modified => cx.theme().status().modified, - DiffHunkStatus::Removed => cx.theme().status().deleted, - }; - cx.paint_quad(quad( - bounds, - Corners::default(), - color, - Edges { - top: Pixels::ZERO, - right: px(1.), - bottom: Pixels::ZERO, - left: px(1.), - }, - cx.theme().colors().scrollbar_thumb_border, - )); - } - } - - cx.paint_quad(quad( - thumb_bounds, - Corners::default(), - cx.theme().colors().scrollbar_thumb_background, - Edges { - top: Pixels::ZERO, - right: px(1.), - bottom: Pixels::ZERO, - left: px(1.), - }, - cx.theme().colors().scrollbar_thumb_border, - )); - } - - let interactive_track_bounds = InteractiveBounds { - bounds: track_bounds, - stacking_order: cx.stacking_order().clone(), - }; - let mut mouse_position = cx.mouse_position(); - if interactive_track_bounds.visibly_contains(&mouse_position, cx) { - cx.set_cursor_style(CursorStyle::Arrow); - } - - cx.on_mouse_event({ - let editor = self.editor.clone(); - move |event: &MouseMoveEvent, phase, cx| { - if phase == DispatchPhase::Capture { - return; - } - - editor.update(cx, |editor, cx| { - if event.pressed_button == Some(MouseButton::Left) - && editor.scroll_manager.is_dragging_scrollbar() - { - let y = mouse_position.y; - let new_y = event.position.y; - if (track_bounds.top()..track_bounds.bottom()).contains(&y) { - let mut position = editor.scroll_position(cx); - position.y += (new_y - y) * (max_row as f32) / height; - if position.y < 0.0 { - position.y = 0.0; - } - editor.set_scroll_position(position, cx); - } - - mouse_position = event.position; - cx.stop_propagation(); - } else { - editor.scroll_manager.set_is_dragging_scrollbar(false, cx); - if interactive_track_bounds.visibly_contains(&event.position, cx) { - editor.scroll_manager.show_scrollbar(cx); - } - } - }) - } - }); - - if self.editor.read(cx).scroll_manager.is_dragging_scrollbar() { - cx.on_mouse_event({ - let editor = self.editor.clone(); - move |_: &MouseUpEvent, phase, cx| { - if phase == DispatchPhase::Capture { - return; - } - - editor.update(cx, |editor, cx| { - editor.scroll_manager.set_is_dragging_scrollbar(false, cx); - cx.stop_propagation(); - }); - } - }); - } else { - cx.on_mouse_event({ - let editor = self.editor.clone(); - move |event: &MouseDownEvent, phase, cx| { - if phase == DispatchPhase::Capture { - return; - } - - editor.update(cx, |editor, cx| { - if track_bounds.contains(&event.position) { - editor.scroll_manager.set_is_dragging_scrollbar(true, 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.y = top_row as f32; - editor.set_scroll_position(position, cx); - } else { - editor.scroll_manager.show_scrollbar(cx); - } - - cx.stop_propagation(); - } - }); - } - }); - } - } - - #[allow(clippy::too_many_arguments)] - fn paint_highlighted_range( - &self, - range: Range, - color: Hsla, - corner_radius: Pixels, - line_end_overshoot: Pixels, - layout: &LayoutState, - content_origin: gpui::Point, - bounds: Bounds, - cx: &mut WindowContext, - ) { - 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 - - layout.position_map.scroll_position.y, - 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) - - layout.position_map.scroll_position.x - } else { - content_origin.x - layout.position_map.scroll_position.x - }, - end_x: if row == range.end.row() { - content_origin.x - + line_layout.x_for_index(range.end.column() as usize) - - layout.position_map.scroll_position.x - } else { - content_origin.x + line_layout.width + line_end_overshoot - - layout.position_map.scroll_position.x - }, - } - }) - .collect(), - }; - - highlighted_range.paint(bounds, cx); - } - } - - fn paint_blocks( - &mut self, - bounds: Bounds, - layout: &mut LayoutState, - cx: &mut WindowContext, - ) { - 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 mut block in layout.blocks.drain(..) { - let mut origin = bounds.origin - + point( - Pixels::ZERO, - block.row as f32 * layout.position_map.line_height - scroll_top, - ); - if !matches!(block.style, BlockStyle::Sticky) { - origin += point(-scroll_left, Pixels::ZERO); - } - block.element.draw(origin, block.available_space, cx); - } - } - - fn column_pixels(&self, column: usize, cx: &WindowContext) -> Pixels { - let style = &self.style; - let font_size = style.text.font_size.to_pixels(cx.rem_size()); - let layout = cx - .text_system() - .shape_line( - SharedString::from(" ".repeat(column)), - font_size, - &[TextRun { - len: column, - font: style.text.font(), - color: Hsla::default(), - background_color: None, - underline: None, - }], - ) - .unwrap(); - - layout.width - } - - fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &WindowContext) -> Pixels { - 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 shape_line_numbers( - &self, - rows: Range, - active_rows: &BTreeMap, - newest_selection_head: DisplayPoint, - is_singleton: bool, - snapshot: &EditorSnapshot, - cx: &ViewContext, - ) -> ( - Vec>, - Vec>, - ) { - let font_size = self.style.text.font_size.to_pixels(cx.rem_size()); - let include_line_numbers = snapshot.mode == EditorMode::Full; - let mut shaped_line_numbers = Vec::with_capacity(rows.len()); - let mut fold_statuses = Vec::with_capacity(rows.len()); - let mut line_number = String::new(); - let is_relative = EditorSettings::get_global(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, cx.theme().colors().editor_active_line_number) - } else { - (false, cx.theme().colors().editor_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(); - let run = TextRun { - len: line_number.len(), - font: self.style.text.font(), - color, - background_color: None, - underline: None, - }; - let shaped_line = cx - .text_system() - .shape_line(line_number.clone().into(), font_size, &[run]) - .unwrap(); - shaped_line_numbers.push(Some(shaped_line)); - 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); - shaped_line_numbers.push(None); - } - } - - (shaped_line_numbers, fold_statuses) - } - - fn layout_lines( - &self, - rows: Range, - line_number_layouts: &[Option], - snapshot: &EditorSnapshot, - cx: &ViewContext, - ) -> Vec { - if rows.start >= rows.end { - return Vec::new(); - } - - // Show the placeholder when the editor is empty - if snapshot.is_empty() { - let font_size = self.style.text.font_size.to_pixels(cx.rem_size()); - let placeholder_color = cx.theme().colors().text_placeholder; - 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 - .filter_map(move |line| { - let run = TextRun { - len: line.len(), - font: self.style.text.font(), - color: placeholder_color, - background_color: None, - underline: Default::default(), - }; - cx.text_system() - .shape_line(line.to_string().into(), font_size, &[run]) - .log_err() - }) - .map(|line| LineWithInvisibles { - line, - invisibles: Vec::new(), - }) - .collect() - } else { - let chunks = snapshot.highlighted_chunks(rows.clone(), true, &self.style); - LineWithInvisibles::from_chunks( - chunks, - &self.style.text, - MAX_LINE_LEN, - rows.len() as usize, - line_number_layouts, - snapshot.mode, - cx, - ) - } - } - - fn compute_layout(&mut self, bounds: Bounds, cx: &mut WindowContext) -> LayoutState { - self.editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - let style = self.style.clone(); - - let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); - let font_size = style.text.font_size.to_pixels(cx.rem_size()); - let line_height = style.text.line_height_in_pixels(cx.rem_size()); - let em_width = cx - .text_system() - .typographic_bounds(font_id, font_size, 'm') - .unwrap() - .size - .width; - let em_advance = cx - .text_system() - .advance(font_id, font_size, 'm') - .unwrap() - .width; - - let gutter_padding; - let gutter_width; - let gutter_margin; - if snapshot.show_gutter { - let descent = cx.text_system().descent(font_id, font_size); - - let gutter_padding_factor = 3.5; - gutter_padding = (em_width * gutter_padding_factor).round(); - gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0; - gutter_margin = -descent; - } else { - gutter_padding = Pixels::ZERO; - gutter_width = Pixels::ZERO; - gutter_margin = Pixels::ZERO; - }; - - editor.gutter_width = gutter_width; - - let text_width = bounds.size.width - gutter_width; - let overscroll = size(em_width, px(0.)); - let _snapshot = { - editor.set_visible_line_count((bounds.size.height / line_height).into(), cx); - - let editor_width = text_width - gutter_margin - overscroll.width - 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 gutter_size = size(gutter_width, bounds.size.height); - let text_size = size(text_width, bounds.size.height); - - let autoscroll_horizontally = - editor.autoscroll_vertically(bounds.size.height, 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 = f32::from(bounds.size.height / 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<(PlayerColor, Vec)> = Vec::new(); - let mut active_rows = BTreeMap::new(); - let is_singleton = editor.is_singleton(cx); - - let highlighted_rows = editor.highlighted_rows(); - let highlighted_ranges = editor.background_highlights_in_range( - start_anchor..end_anchor, - &snapshot.display_snapshot, - cx.theme().colors(), - ); - - 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.local_player, 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 = cx - .theme() - .players() - .color_for_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 { - cx.theme() - .players() - .color_for_participant(participant_index.0) - } else { - cx.theme().players().absent() - }; - - // 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 = EditorSettings::get_global(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 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_numbers, fold_statuses) = self.shape_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 = Pixels::ZERO; - let line_layouts = self.layout_lines(start_row..end_row, &line_numbers, &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 longest_line_width = layout_line(snapshot.longest_row(), &snapshot, &style, cx) - .unwrap() - .width; - let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.width; - - let (scroll_width, blocks) = cx.with_element_id(Some("editor_blocks"), |cx| { - self.layout_blocks( - start_row..end_row, - &snapshot, - bounds.size.width, - scroll_width, - gutter_padding, - gutter_width, - em_width, - gutter_width + gutter_margin, - line_height, - &style, - &line_layouts, - editor, - cx, - ) - }); - - let scroll_max = point( - f32::from((scroll_width - text_size.width) / 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.width, - scroll_width, - em_width, - &line_layouts, - cx, - ) - } else { - false - }; - - if clamped || autoscrolled { - snapshot = editor.snapshot(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() { - let max_height = (12. * line_height).min((bounds.size.height - line_height) / 2.); - context_menu = - editor.render_context_menu(newest_selection_head, &self.style, max_height, 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(|element| CodeActionsIndicator { - row: newest_selection_head.row(), - button: element, - }); - } - } - - let visible_rows = start_row..start_row + line_layouts.len() as u32; - let max_size = size( - (120. * em_width) // Default size - .min(bounds.size.width / 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(bounds.size.height / 2.) // Shrink to half of the editor height - .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines - ); - - let hover = editor.hover_state.render( - &snapshot, - &style, - visible_rows, - max_size, - editor.workspace.as_ref().map(|(w, _)| w.clone()), - cx, - ); - - let fold_indicators = cx.with_element_id(Some("gutter_fold_indicators"), |cx| { - editor.render_fold_indicators( - fold_statuses, - &style, - editor.gutter_hovered, - line_height, - gutter_margin, - cx, - ) - }); - - let invisible_symbol_font_size = font_size / 2.; - let tab_invisible = cx - .text_system() - .shape_line( - "→".into(), - invisible_symbol_font_size, - &[TextRun { - len: "→".len(), - font: self.style.text.font(), - color: cx.theme().colors().editor_invisible, - background_color: None, - underline: None, - }], - ) - .unwrap(); - let space_invisible = cx - .text_system() - .shape_line( - "•".into(), - invisible_symbol_font_size, - &[TextRun { - len: "•".len(), - font: self.style.text.font(), - color: cx.theme().colors().editor_invisible, - background_color: None, - underline: None, - }], - ) - .unwrap(); - - LayoutState { - mode: snapshot.mode, - position_map: Arc::new(PositionMap { - size: bounds.size, - scroll_position: point( - scroll_position.x * em_width, - scroll_position.y * line_height, - ), - scroll_max, - line_layouts, - line_height, - em_width, - em_advance, - snapshot, - }), - visible_anchor_range: start_anchor..end_anchor, - 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, - line_numbers, - display_hunks, - blocks, - selections, - context_menu, - code_actions_indicator, - fold_indicators, - tab_invisible, - space_invisible, - hover_popovers: hover, - } - }) - } - - #[allow(clippy::too_many_arguments)] - fn layout_blocks( - &self, - rows: Range, - snapshot: &EditorSnapshot, - editor_width: Pixels, - scroll_width: Pixels, - gutter_padding: Pixels, - gutter_width: Pixels, - em_width: Pixels, - text_x: Pixels, - line_height: Pixels, - style: &EditorStyle, - line_layouts: &[LineWithInvisibles], - editor: &mut Editor, - cx: &mut ViewContext, - ) -> (Pixels, Vec) { - let mut block_id = 0; - 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 render_block = |block: &TransformBlock, - available_space: Size, - block_id: usize, - editor: &mut Editor, - cx: &mut ViewContext| { - 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) - .unwrap() - .x_for_index(align_to.column() as usize) - }; - - block.render(&mut BlockContext { - view_context: cx, - anchor_x, - gutter_padding, - line_height, - gutter_width, - em_width, - block_id, - editor_style: &self.style, - }) - } - - TransformBlock::ExcerptHeader { - buffer, - range, - starts_new_buffer, - .. - } => { - let include_root = editor - .project - .as_ref() - .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) - .unwrap_or_default(); - - let jump_handler = 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); - - let jump_handler = cx.listener_for(&self.editor, move |editor, _, cx| { - editor.jump(jump_path.clone(), jump_position, jump_anchor, cx); - }); - - jump_handler - }); - - let element = if *starts_new_buffer { - 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| SharedString::from(p.to_string_lossy().to_string() + "/")); - } - - div() - .id(("path header container", block_id)) - .size_full() - .p_1p5() - .child( - h_stack() - .id("path header block") - .py_1p5() - .pl_3() - .pr_2() - .rounded_lg() - .shadow_md() - .border() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().editor_subheader_background) - .justify_between() - .hover(|style| style.bg(cx.theme().colors().element_hover)) - .child( - h_stack().gap_3().child( - h_stack() - .gap_2() - .child( - filename - .map(SharedString::from) - .unwrap_or_else(|| "untitled".into()), - ) - .when_some(parent_path, |then, path| { - then.child( - div().child(path).text_color( - cx.theme().colors().text_muted, - ), - ) - }), - ), - ) - .when_some(jump_handler, |this, jump_handler| { - this.cursor_pointer() - .tooltip(|cx| { - Tooltip::for_action( - "Jump to Buffer", - &OpenExcerpts, - cx, - ) - }) - .on_mouse_down(MouseButton::Left, |_, cx| { - cx.stop_propagation() - }) - .on_click(jump_handler) - }), - ) - } else { - h_stack() - .id(("collapsed context", block_id)) - .size_full() - .gap(gutter_padding) - .child( - h_stack() - .justify_end() - .flex_none() - .w(gutter_width - gutter_padding) - .h_full() - .text_buffer(cx) - .text_color(cx.theme().colors().editor_line_number) - .child("..."), - ) - .map(|this| { - if let Some(jump_handler) = jump_handler { - this.child( - ButtonLike::new("jump to collapsed context") - .style(ButtonStyle::Transparent) - .full_width() - .on_click(jump_handler) - .tooltip(|cx| { - Tooltip::for_action( - "Jump to Buffer", - &OpenExcerpts, - cx, - ) - }) - .child( - div() - .h_px() - .w_full() - .bg(cx.theme().colors().border_variant) - .group_hover("", |style| { - style.bg(cx.theme().colors().border) - }), - ), - ) - } else { - this.child(div().size_full().bg(gpui::green())) - } - }) - }; - element.into_any() - } - }; - - let size = element.measure(available_space, cx); - (element, size) - }; - - let mut fixed_block_max_width = Pixels::ZERO; - let mut blocks = Vec::new(); - for (row, block) in fixed_blocks { - let available_space = size( - AvailableSpace::MinContent, - AvailableSpace::Definite(block.height() as f32 * line_height), - ); - let (element, element_size) = - render_block(block, available_space, block_id, editor, cx); - block_id += 1; - fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width); - blocks.push(BlockLayout { - row, - element, - available_space, - 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 available_space = size( - AvailableSpace::Definite(width), - AvailableSpace::Definite(block.height() as f32 * line_height), - ); - let (element, _) = render_block(block, available_space, block_id, editor, cx); - block_id += 1; - blocks.push(BlockLayout { - row, - element, - available_space, - style, - }); - } - ( - scroll_width.max(fixed_block_max_width - gutter_width), - blocks, - ) - } - - fn paint_mouse_listeners( - &mut self, - bounds: Bounds, - gutter_bounds: Bounds, - text_bounds: Bounds, - layout: &LayoutState, - cx: &mut WindowContext, - ) { - let interactive_bounds = InteractiveBounds { - bounds: bounds.intersect(&cx.content_mask().bounds), - stacking_order: cx.stacking_order().clone(), - }; - - cx.on_mouse_event({ - let position_map = layout.position_map.clone(); - let editor = self.editor.clone(); - let interactive_bounds = interactive_bounds.clone(); - - move |event: &ScrollWheelEvent, phase, cx| { - if phase == DispatchPhase::Bubble - && interactive_bounds.visibly_contains(&event.position, cx) - { - editor.update(cx, |editor, cx| { - Self::scroll(editor, event, &position_map, &interactive_bounds, cx) - }); - } - } - }); - - cx.on_mouse_event({ - let position_map = layout.position_map.clone(); - let editor = self.editor.clone(); - let stacking_order = cx.stacking_order().clone(); - let interactive_bounds = interactive_bounds.clone(); - - move |event: &MouseDownEvent, phase, cx| { - if phase == DispatchPhase::Bubble - && interactive_bounds.visibly_contains(&event.position, cx) - { - match event.button { - MouseButton::Left => editor.update(cx, |editor, cx| { - Self::mouse_left_down( - editor, - event, - &position_map, - text_bounds, - gutter_bounds, - &stacking_order, - cx, - ); - }), - MouseButton::Right => editor.update(cx, |editor, cx| { - Self::mouse_right_down(editor, event, &position_map, text_bounds, cx); - }), - _ => {} - }; - } - } - }); - - cx.on_mouse_event({ - let position_map = layout.position_map.clone(); - let editor = self.editor.clone(); - let stacking_order = cx.stacking_order().clone(); - let interactive_bounds = interactive_bounds.clone(); - - move |event: &MouseUpEvent, phase, cx| { - if phase == DispatchPhase::Bubble - && interactive_bounds.visibly_contains(&event.position, cx) - { - editor.update(cx, |editor, cx| { - Self::mouse_up( - editor, - event, - &position_map, - text_bounds, - &stacking_order, - cx, - ) - }); - } - } - }); - cx.on_mouse_event({ - let position_map = layout.position_map.clone(); - let editor = self.editor.clone(); - let stacking_order = cx.stacking_order().clone(); - - move |event: &MouseMoveEvent, phase, cx| { - // if editor.has_pending_selection() && event.pressed_button == Some(MouseButton::Left) { - - if phase == DispatchPhase::Bubble { - editor.update(cx, |editor, cx| { - if event.pressed_button == Some(MouseButton::Left) { - Self::mouse_dragged( - editor, - event, - &position_map, - text_bounds, - gutter_bounds, - &stacking_order, - cx, - ) - } - - if interactive_bounds.visibly_contains(&event.position, cx) { - Self::mouse_moved( - editor, - event, - &position_map, - text_bounds, - gutter_bounds, - &stacking_order, - cx, - ) - } - }); - } - } - }); - } -} - -#[derive(Debug)] -pub struct LineWithInvisibles { - pub line: ShapedLine, - invisibles: Vec, -} - -impl LineWithInvisibles { - fn from_chunks<'a>( - chunks: impl Iterator>, - text_style: &TextStyle, - max_line_len: usize, - max_line_count: usize, - line_number_layouts: &[Option], - editor_mode: EditorMode, - cx: &WindowContext, - ) -> 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; - let font_size = text_style.font_size.to_pixels(cx.rem_size()); - - 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 { - let shaped_line = cx - .text_system() - .shape_line(line.clone().into(), font_size, &styles) - .unwrap(); - layouts.push(Self { - line: shaped_line, - 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 { - Cow::Owned(text_style.clone().highlight(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(TextRun { - len: line_chunk.len(), - font: text_style.font(), - color: text_style.color, - background_color: text_style.background_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, - content_origin: gpui::Point, - whitespace_setting: ShowWhitespaceSetting, - selection_ranges: &[Range], - cx: &mut WindowContext, - ) { - let line_height = layout.position_map.line_height; - let line_y = line_height * row as f32 - layout.position_map.scroll_position.y; - - self.line - .paint( - content_origin + gpui::point(-layout.position_map.scroll_position.x, line_y), - line_height, - cx, - ) - .log_err(); - - self.draw_invisibles( - &selection_ranges, - layout, - content_origin, - line_y, - row, - line_height, - whitespace_setting, - cx, - ); - } - - fn draw_invisibles( - &self, - selection_ranges: &[Range], - layout: &LayoutState, - content_origin: gpui::Point, - line_y: Pixels, - row: u32, - line_height: Pixels, - whitespace_setting: ShowWhitespaceSetting, - cx: &mut WindowContext, - ) { - 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(Pixels::ZERO) / 2.0; - let origin = content_origin - + gpui::point( - x_offset + invisible_offset - layout.position_map.scroll_position.x, - 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, line_height, cx).log_err(); - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum Invisible { - Tab { line_start_offset: usize }, - Whitespace { line_offset: usize }, -} - -impl Element for EditorElement { - type State = (); - - fn request_layout( - &mut self, - _element_state: Option, - cx: &mut gpui::WindowContext, - ) -> (gpui::LayoutId, Self::State) { - self.editor.update(cx, |editor, cx| { - editor.set_style(self.style.clone(), cx); - - let layout_id = match editor.mode { - EditorMode::SingleLine => { - let rem_size = cx.rem_size(); - let mut style = Style::default(); - style.size.width = relative(1.).into(); - style.size.height = self.style.text.line_height_in_pixels(rem_size).into(); - cx.request_layout(&style, None) - } - EditorMode::AutoHeight { max_lines } => { - let editor_handle = cx.view().clone(); - let max_line_number_width = - self.max_line_number_width(&editor.snapshot(cx), cx); - cx.request_measured_layout(Style::default(), move |known_dimensions, _, cx| { - editor_handle - .update(cx, |editor, cx| { - compute_auto_height_layout( - editor, - max_lines, - max_line_number_width, - known_dimensions, - cx, - ) - }) - .unwrap_or_default() - }) - } - EditorMode::Full => { - let mut style = Style::default(); - style.size.width = relative(1.).into(); - style.size.height = relative(1.).into(); - cx.request_layout(&style, None) - } - }; - - (layout_id, ()) - }) - } - - fn paint( - &mut self, - bounds: Bounds, - _element_state: &mut Self::State, - cx: &mut gpui::WindowContext, - ) { - let editor = self.editor.clone(); - - cx.with_text_style( - Some(gpui::TextStyleRefinement { - font_size: Some(self.style.text.font_size), - ..Default::default() - }), - |cx| { - let mut layout = self.compute_layout(bounds, cx); - let gutter_bounds = Bounds { - origin: bounds.origin, - size: layout.gutter_size, - }; - let text_bounds = Bounds { - origin: gutter_bounds.upper_right(), - size: layout.text_size, - }; - - let focus_handle = editor.focus_handle(cx); - let key_context = self.editor.read(cx).key_context(cx); - cx.with_key_dispatch(Some(key_context), Some(focus_handle.clone()), |_, cx| { - self.register_actions(cx); - self.register_key_listeners(cx); - - cx.with_content_mask(Some(ContentMask { bounds }), |cx| { - let input_handler = - ElementInputHandler::new(bounds, self.editor.clone(), cx); - cx.handle_input(&focus_handle, input_handler); - - self.paint_background(gutter_bounds, text_bounds, &layout, cx); - if layout.gutter_size.width > Pixels::ZERO { - self.paint_gutter(gutter_bounds, &mut layout, cx); - } - self.paint_text(text_bounds, &mut layout, cx); - - cx.with_z_index(0, |cx| { - self.paint_mouse_listeners( - bounds, - gutter_bounds, - text_bounds, - &layout, - cx, - ); - }); - if !layout.blocks.is_empty() { - cx.with_z_index(0, |cx| { - cx.with_element_id(Some("editor_blocks"), |cx| { - self.paint_blocks(bounds, &mut layout, cx); - }); - }) - } - - cx.with_z_index(1, |cx| { - self.paint_overlays(text_bounds, &mut layout, cx); - }); - - cx.with_z_index(2, |cx| self.paint_scrollbar(bounds, &mut layout, cx)); - }); - }) - }, - ); - } -} - -impl IntoElement for EditorElement { - type Element = Self; - - fn element_id(&self) -> Option { - self.editor.element_id() - } - - fn into_element(self) -> Self::Element { - self - } -} - -type BufferRow = u32; - -pub struct LayoutState { - position_map: Arc, - gutter_size: Size, - gutter_padding: Pixels, - gutter_margin: Pixels, - text_size: gpui::Size, - mode: EditorMode, - wrap_guides: SmallVec<[(Pixels, bool); 2]>, - visible_anchor_range: Range, - visible_display_row_range: Range, - active_rows: BTreeMap, - highlighted_rows: Option>, - line_numbers: Vec>, - display_hunks: Vec, - blocks: Vec, - highlighted_ranges: Vec<(Range, Hsla)>, - selections: Vec<(PlayerColor, Vec)>, - scrollbar_row_range: Range, - show_scrollbars: bool, - is_singleton: bool, - max_row: u32, - context_menu: Option<(DisplayPoint, AnyElement)>, - code_actions_indicator: Option, - hover_popovers: Option<(DisplayPoint, Vec)>, - fold_indicators: Vec>, - tab_invisible: ShapedLine, - space_invisible: ShapedLine, -} - -struct CodeActionsIndicator { - row: u32, - button: IconButton, -} - -struct PositionMap { - size: Size, - line_height: Pixels, - scroll_position: gpui::Point, - scroll_max: gpui::Point, - em_width: Pixels, - em_advance: Pixels, - 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: Bounds, - position: gpui::Point, - ) -> PointForPosition { - let scroll_position = self.snapshot.scroll_position(); - let position = position - text_bounds.origin; - let y = position.y.max(px(0.)).min(self.size.height); - 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 as usize) - .map(|&LineWithInvisibles { ref line, .. }| line) - { - if let Some(ix) = line.index_for_x(x) { - (ix as u32, px(0.)) - } else { - (line.len as u32, px(0.).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, - available_space: Size, - style: BlockStyle, -} - -fn layout_line( - row: u32, - snapshot: &EditorSnapshot, - style: &EditorStyle, - cx: &WindowContext, -) -> Result { - 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); - } - - cx.text_system().shape_line( - line.into(), - style.text.font_size.to_pixels(cx.rem_size()), - &[TextRun { - len: snapshot.line_len(row) as usize, - font: style.text.font(), - color: Hsla::default(), - background_color: None, - underline: None, - }], - ) -} - -#[derive(Debug)] -pub struct Cursor { - origin: gpui::Point, - block_width: Pixels, - line_height: Pixels, - color: Hsla, - shape: CursorShape, - block_text: Option, -} - -impl Cursor { - pub fn new( - origin: gpui::Point, - block_width: Pixels, - line_height: Pixels, - color: Hsla, - 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 { - origin: self.origin + origin, - size: size(self.block_width, self.line_height), - } - } - - pub fn paint(&self, origin: gpui::Point, cx: &mut WindowContext) { - let bounds = match self.shape { - CursorShape::Bar => Bounds { - origin: self.origin + origin, - size: size(px(2.0), self.line_height), - }, - CursorShape::Block | CursorShape::Hollow => Bounds { - origin: self.origin + origin, - size: size(self.block_width, self.line_height), - }, - CursorShape::Underscore => Bounds { - origin: self.origin - + origin - + gpui::Point::new(Pixels::ZERO, self.line_height - px(2.0)), - size: size(self.block_width, px(2.0)), - }, - }; - - //Draw background or border quad - let cursor = if matches!(self.shape, CursorShape::Hollow) { - outline(bounds, self.color) - } else { - fill(bounds, self.color) - }; - - cx.paint_quad(cursor); - - if let Some(block_text) = &self.block_text { - block_text - .paint(self.origin + origin, self.line_height, cx) - .log_err(); - } - } - - pub fn shape(&self) -> CursorShape { - self.shape - } -} - -#[derive(Debug)] -pub struct HighlightedRange { - pub start_y: Pixels, - pub line_height: Pixels, - pub lines: Vec, - pub color: Hsla, - pub corner_radius: Pixels, -} - -#[derive(Debug)] -pub struct HighlightedRangeLine { - pub start_x: Pixels, - pub end_x: Pixels, -} - -impl HighlightedRange { - 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: Pixels, - lines: &[HighlightedRangeLine], - _bounds: Bounds, - cx: &mut WindowContext, - ) { - if lines.is_empty() { - return; - } - - let first_line = lines.first().unwrap(); - let last_line = lines.last().unwrap(); - - let first_top_left = point(first_line.start_x, start_y); - let first_top_right = point(first_line.end_x, start_y); - - let curve_height = point(Pixels::ZERO, self.corner_radius); - let curve_width = |start_x: Pixels, end_x: Pixels| { - let max = (end_x - start_x) / 2.; - let width = if max < self.corner_radius { - max - } else { - self.corner_radius - }; - - point(width, Pixels::ZERO) - }; - - let top_curve_width = curve_width(first_line.start_x, first_line.end_x); - let mut path = gpui::Path::new(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 = point(line.end_x, start_y + (ix + 1) as f32 * self.line_height); - - if let Some((_, next_line)) = iter.peek() { - let next_top_right = point(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 > Pixels::ZERO { - path.curve_to(bottom_right - curve_width, bottom_right); - } - path.line_to(next_top_right + curve_width); - if self.corner_radius > Pixels::ZERO { - 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 > Pixels::ZERO { - path.curve_to(bottom_right + curve_width, bottom_right); - } - path.line_to(next_top_right - curve_width); - if self.corner_radius > Pixels::ZERO { - 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 > Pixels::ZERO { - path.curve_to(bottom_right - curve_width, bottom_right); - } - - let bottom_left = point(line.start_x, bottom_right.y); - path.line_to(bottom_left + curve_width); - if self.corner_radius > Pixels::ZERO { - 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 = point(last_line.start_x, start_y + self.line_height); - path.line_to(second_top_left + curve_height); - if self.corner_radius > Pixels::ZERO { - path.curve_to(second_top_left + curve_width, second_top_left); - } - let first_bottom_left = point(first_line.start_x, second_top_left.y); - path.line_to(first_bottom_left - curve_width); - if self.corner_radius > Pixels::ZERO { - path.curve_to(first_bottom_left - curve_height, first_bottom_left); - } - } - - path.line_to(first_top_left + curve_height); - if self.corner_radius > Pixels::ZERO { - path.curve_to(first_top_left + top_curve_width, first_top_left); - } - path.line_to(first_top_right - top_curve_width); - - cx.paint_path(path, self.color); - } -} - -pub fn scale_vertical_mouse_autoscroll_delta(delta: Pixels) -> f32 { - (delta.pow(1.5) / 100.0).into() -} - -fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 { - (delta.pow(1.2) / 300.0).into() -} - -#[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_shape_line_numbers(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - let window = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); - Editor::new(EditorMode::Full, buffer, None, cx) - }); - - let editor = window.root(cx).unwrap(); - let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); - let element = EditorElement::new(&editor, style); - - let layouts = window - .update(cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - element - .shape_line_numbers( - 0..6, - &Default::default(), - DisplayPoint::new(0, 0), - false, - &snapshot, - cx, - ) - .0 - }) - .unwrap(); - assert_eq!(layouts.len(), 6); - - let relative_rows = window - .update(cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - element.calculate_relative_line_numbers(&snapshot, &(0..6), Some(3)) - }) - .unwrap(); - 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 = window - .update(cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - - element.calculate_relative_line_numbers(&snapshot, &(3..6), Some(1)) - }) - .unwrap(); - 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 = window - .update(cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - - element.calculate_relative_line_numbers(&snapshot, &(0..3), Some(6)) - }) - .unwrap(); - 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 window = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx); - Editor::new(EditorMode::Full, buffer, None, cx) - }); - let editor = window.root(cx).unwrap(); - let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); - let mut element = EditorElement::new(&editor, style); - - window - .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), - ]); - }); - }) - .unwrap(); - let state = cx - .update_window(window.into(), |_, cx| { - element.compute_layout( - Bounds { - origin: point(px(500.), px(500.)), - size: size(px(500.), px(500.)), - }, - cx, - ) - }) - .unwrap(); - - 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 window = 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, cx) - }); - let editor = window.root(cx).unwrap(); - let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); - let mut element = EditorElement::new(&editor, style); - let _state = window.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), - ]); - }); - }); - - let state = cx - .update_window(window.into(), |_, cx| { - element.compute_layout( - Bounds { - origin: point(px(500.), px(500.)), - size: size(px(500.), px(500.)), - }, - cx, - ) - }) - .unwrap(); - 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 window = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("", cx); - Editor::new(EditorMode::Full, buffer, None, cx) - }); - let editor = window.root(cx).unwrap(); - let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); - window - .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(|_| div().into_any()), - }], - None, - cx, - ); - - // Blur the editor so that it displays placeholder text. - cx.blur(); - }) - .unwrap(); - - let mut element = EditorElement::new(&editor, style); - let state = cx - .update_window(window.into(), |_, cx| { - element.compute_layout( - Bounds { - origin: point(px(500.), px(500.)), - size: size(px(500.), px(500.)), - }, - cx, - ) - }) - .unwrap(); - let size = state.position_map.size; - - assert_eq!(state.position_map.line_layouts.len(), 4); - assert_eq!( - state - .line_numbers - .iter() - .map(Option::is_some) - .collect::>(), - &[false, false, false, true] - ); - - // Don't panic. - let bounds = Bounds::::new(Default::default(), size); - cx.update_window(window.into(), |_, cx| { - element.paint(bounds, &mut (), cx); - }) - .unwrap() - } - - #[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, px(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", - px(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, - px(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: Pixels, - ) -> Vec { - info!( - "Creating editor with mode {editor_mode:?}, width {}px and text '{input_text}'", - editor_width.0 - ); - let window = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple(&input_text, cx); - Editor::new(editor_mode, buffer, None, cx) - }); - let editor = window.root(cx).unwrap(); - let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); - let mut element = EditorElement::new(&editor, style); - window - .update(cx, |editor, cx| { - editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx); - editor.set_wrap_width(Some(editor_width), cx); - }) - .unwrap(); - let layout_state = cx - .update_window(window.into(), |_, cx| { - element.compute_layout( - Bounds { - origin: point(px(500.), px(500.)), - size: size(px(500.), px(500.)), - }, - cx, - ) - }) - .unwrap(); - - layout_state - .position_map - .line_layouts - .iter() - .map(|line_with_invisibles| &line_with_invisibles.invisibles) - .flatten() - .cloned() - .collect() - } -} - -pub fn register_action( - view: &View, - cx: &mut WindowContext, - listener: impl Fn(&mut Editor, &T, &mut ViewContext) + 'static, -) { - let view = view.clone(); - cx.on_action(TypeId::of::(), move |action, phase, cx| { - let action = action.downcast_ref().unwrap(); - if phase == DispatchPhase::Bubble { - view.update(cx, |editor, cx| { - listener(editor, action, cx); - }) - } - }) -} - -fn compute_auto_height_layout( - editor: &mut Editor, - max_lines: usize, - max_line_number_width: Pixels, - known_dimensions: Size>, - cx: &mut ViewContext, -) -> Option> { - let width = known_dimensions.width?; - if let Some(height) = known_dimensions.height { - return Some(size(width, height)); - } - - let style = editor.style.as_ref().unwrap(); - let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); - let font_size = style.text.font_size.to_pixels(cx.rem_size()); - let line_height = style.text.line_height_in_pixels(cx.rem_size()); - let em_width = cx - .text_system() - .typographic_bounds(font_id, font_size, 'm') - .unwrap() - .size - .width; - - let mut snapshot = editor.snapshot(cx); - let gutter_width; - let gutter_margin; - if snapshot.show_gutter { - let descent = cx.text_system().descent(font_id, font_size); - let gutter_padding_factor = 3.5; - let gutter_padding = (em_width * gutter_padding_factor).round(); - gutter_width = max_line_number_width + gutter_padding * 2.0; - gutter_margin = -descent; - } else { - gutter_width = Pixels::ZERO; - gutter_margin = Pixels::ZERO; - }; - - editor.gutter_width = gutter_width; - let text_width = width - gutter_width; - let overscroll = size(em_width, px(0.)); - - let editor_width = text_width - gutter_margin - overscroll.width - em_width; - if editor.set_wrap_width(Some(editor_width), cx) { - snapshot = editor.snapshot(cx); - } - - let scroll_height = Pixels::from(snapshot.max_point().row() + 1) * line_height; - let height = scroll_height - .max(line_height) - .min(line_height * max_lines as f32); - - Some(size(width, height)) -} diff --git a/crates/editor2/src/git.rs b/crates/editor2/src/git.rs deleted file mode 100644 index e1715aa3b2..0000000000 --- a/crates/editor2/src/git.rs +++ /dev/null @@ -1,282 +0,0 @@ -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| { - 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.range.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(test)] -mod tests { - use crate::editor_tests::init_test; - use crate::Point; - use gpui::{Context, 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_executor.clone()); - 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.background_executor.run_until_parked(); - - let multibuffer = cx.new_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 deleted file mode 100644 index 1ed7700f37..0000000000 --- a/crates/editor2/src/highlight_matching_bracket.rs +++ /dev/null @@ -1,138 +0,0 @@ -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 deleted file mode 100644 index 6fdd53f43a..0000000000 --- a/crates/editor2/src/hover_popover.rs +++ /dev/null @@ -1,1345 +0,0 @@ -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, div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, Model, - MouseButton, ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled, - Task, ViewContext, WeakView, -}; -use language::{markdown, Bias, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown}; - -use lsp::DiagnosticSeverity; -use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; -use settings::Settings; -use std::{ops::Range, sync::Arc, time::Duration}; -use ui::{StyledExt, Tooltip}; -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: Pixels = px(4.); -pub const HOVER_POPOVER_GAP: Pixels = px(10.); - -actions!(editor, [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 EditorSettings::get_global(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 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.element_hover, // todo!("use a proper background here") - 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_executor() - .timer(Duration::from_millis(HOVER_DELAY_MILLIS)), - ); - - cx.background_executor() - .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.element_hover, // todo! update theme - 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, - max_size: Size, - 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, max_size, cx)); - } - if let Some(info_popover) = self.info_popover.as_mut() { - elements.push(info_popover.render(style, max_size, workspace, cx)); - } - - Some((point, elements)) - } -} - -#[derive(Debug, Clone)] -pub struct InfoPopover { - pub project: Model, - symbol_range: RangeInEditor, - pub blocks: Vec, - parsed_content: ParsedMarkdown, -} - -impl InfoPopover { - pub fn render( - &mut self, - style: &EditorStyle, - max_size: Size, - workspace: Option>, - cx: &mut ViewContext, - ) -> AnyElement { - div() - .id("info_popover") - .elevation_2(cx) - .p_2() - .overflow_y_scroll() - .max_w(max_size.width) - .max_h(max_size.height) - // Prevent a mouse move on the popover from being propagated to the editor, - // because that would dismiss the popover. - .on_mouse_move(|_, cx| cx.stop_propagation()) - .child(crate::render_parsed_markdown( - "content", - &self.parsed_content, - style, - workspace, - cx, - )) - .into_any_element() - } -} - -#[derive(Debug, Clone)] -pub struct DiagnosticPopover { - local_diagnostic: DiagnosticEntry, - primary_diagnostic: Option>, -} - -impl DiagnosticPopover { - pub fn render( - &self, - style: &EditorStyle, - max_size: Size, - cx: &mut ViewContext, - ) -> AnyElement { - let text = match &self.local_diagnostic.diagnostic.source { - Some(source) => format!("{source}: {}", self.local_diagnostic.diagnostic.message), - None => self.local_diagnostic.diagnostic.message.clone(), - }; - - struct DiagnosticColors { - pub text: Hsla, - pub background: Hsla, - pub border: Hsla, - } - - let diagnostic_colors = match self.local_diagnostic.diagnostic.severity { - DiagnosticSeverity::ERROR => DiagnosticColors { - text: style.status.error, - background: style.status.error_background, - border: style.status.error_border, - }, - DiagnosticSeverity::WARNING => DiagnosticColors { - text: style.status.warning, - background: style.status.warning_background, - border: style.status.warning_border, - }, - DiagnosticSeverity::INFORMATION => DiagnosticColors { - text: style.status.info, - background: style.status.info_background, - border: style.status.info_border, - }, - DiagnosticSeverity::HINT => DiagnosticColors { - text: style.status.hint, - background: style.status.hint_background, - border: style.status.hint_border, - }, - _ => DiagnosticColors { - text: style.status.ignored, - background: style.status.ignored_background, - border: style.status.ignored_border, - }, - }; - - div() - .id("diagnostic") - .overflow_y_scroll() - .px_2() - .py_1() - .bg(diagnostic_colors.background) - .text_color(diagnostic_colors.text) - .border_1() - .border_color(diagnostic_colors.border) - .rounded_md() - .max_w(max_size.width) - .max_h(max_size.height) - .cursor(CursorStyle::PointingHand) - .tooltip(move |cx| Tooltip::for_action("Go To Diagnostic", &crate::GoToDiagnostic, cx)) - // Prevent a mouse move on the popover from being propagated to the editor, - // because that would dismiss the popover. - .on_mouse_move(|_, cx| cx.stop_propagation()) - // Prevent a mouse down on the popover from being propagated to the editor, - // because that would move the cursor. - .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) - .on_click(cx.listener(|editor, _, cx| editor.go_to_diagnostic(&Default::default(), cx))) - .child(SharedString::from(text)) - .into_any_element() - } - - 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::{FontWeight, HighlightStyle, UnderlineStyle}; - 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.background_executor - .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.background_executor - .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.background_executor.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.background_executor - .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); - - cx.background_executor.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, |_| {}); - - let editor = cx.add_window(|cx| Editor::single_line(cx)); - editor - .update(cx, |editor, _cx| { - let style = editor.style.clone().unwrap(); - - 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 { - font_weight: Some(FontWeight::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(UnderlineStyle { - 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(UnderlineStyle { - 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(UnderlineStyle { - 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:?}" - ); - } - }) - .unwrap(); - } - - #[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.background_executor.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.background_executor.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.background_executor - .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); - cx.background_executor.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.background_executor - .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); - cx.background_executor.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 deleted file mode 100644 index d7dfa01b21..0000000000 --- a/crates/editor2/src/inlay_hint_cache.rs +++ /dev/null @@ -1,3268 +0,0 @@ -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::{Model, ModelContext, 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: &Model, - 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: &Model, - 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: &Model, - 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_executor().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::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::AsyncWindowContext, -) -> 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.excerpts_for_inlay_hints_query(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_executor() - .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}, - ExcerptRange, - }; - use futures::StreamExt; - use gpui::{Context, TestAppContext, WindowHandle}; - use itertools::Itertools; - use language::{ - language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig, - }; - use lsp::FakeLanguageServer; - use parking_lot::Mutex; - use project::{FakeFs, Project}; - use serde_json::json; - use settings::SettingsStore; - use text::{Point, ToPoint}; - - 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.executor().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.executor().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.executor().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.executor().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.executor().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.executor().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.executor().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_executor.clone()); - 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 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.executor().run_until_parked(); - cx.executor().start_waiting(); - let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap(); - let rs_editor = - cx.add_window(|cx| Editor::for_buffer(rs_buffer, Some(project.clone()), cx)); - 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.executor().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.executor().run_until_parked(); - let md_buffer = project - .update(cx, |project, cx| { - project.open_local_buffer("/a/other.md", cx) - }) - .await - .unwrap(); - cx.executor().run_until_parked(); - cx.executor().start_waiting(); - let md_fake_server = md_fake_servers.unwrap().next().await.unwrap(); - let md_editor = cx.add_window(|cx| Editor::for_buffer(md_buffer, Some(project), cx)); - 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.executor().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.executor().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.executor().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.executor().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.executor().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.executor().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.executor().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.executor().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.executor().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.executor().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.executor().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(); - edits.push(cx.spawn(|mut cx| async move { - _ = task_editor.update(&mut 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.executor().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_executor.clone()); - 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 buffer = project - .update(cx, |project, cx| { - project.open_local_buffer("/a/main.rs", cx) - }) - .await - .unwrap(); - cx.executor().run_until_parked(); - cx.executor().start_waiting(); - let fake_server = fake_servers.next().await.unwrap(); - let editor = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx)); - 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: &WindowHandle, - cx: &mut gpui::TestAppContext, - ) -> Range { - let ranges = editor - .update(cx, |editor, cx| { - editor.excerpts_for_inlay_hints_query(None, cx) - }) - .unwrap(); - 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.executor().advance_clock(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - )); - cx.executor().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.executor().advance_clock(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - )); - cx.executor().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()) - .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 - }) - .unwrap(); - - _ = 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.executor().advance_clock(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - )); - cx.executor().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.executor().advance_clock(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - )); - cx.executor().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(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_executor.clone()); - 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 worktree_id = project.update(cx, |project, cx| { - project.worktrees().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.new_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 - }); - - cx.executor().run_until_parked(); - let editor = - cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), 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.executor().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(), - ]; - 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.executor().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.executor().advance_clock(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - )); - cx.executor().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() - }).unwrap(); - - _ = 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.executor().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| { - // TODO if this gets set to hint boundary (e.g. 56) we sometimes get an extra cache version bump, why? - s.select_ranges([Point::new(57, 0)..Point::new(57, 0)]) - }); - editor.handle_input("++++more text++++", cx); - }); - cx.executor().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; - assert_eq!( - current_cache_version, - last_scroll_update_version + expected_hints.len(), - "We should have updated cache N times == N of new hints arrived (separately from each excerpt)" - ); - }); - } - - #[gpui::test] - async fn test_excerpts_removed(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_executor.clone()); - 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 worktree_id = project.update(cx, |project, cx| { - project.worktrees().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.new_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()); - - cx.executor().run_until_parked(); - let editor = - cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), 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.executor().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.executor().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.executor().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_executor.clone()); - 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 buffer = project - .update(cx, |project, cx| { - project.open_local_buffer("/a/main.rs", cx) - }) - .await - .unwrap(); - cx.executor().run_until_parked(); - cx.executor().start_waiting(); - let fake_server = fake_servers.next().await.unwrap(); - let editor = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx)); - 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.executor().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.executor().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.executor().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.executor().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.executor().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.executor().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.executor().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.executor().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.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - theme::init(theme::LoadThemes::JustBase, 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, WindowHandle, 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_executor.clone()); - 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 buffer = project - .update(cx, |project, cx| { - project.open_local_buffer("/a/main.rs", cx) - }) - .await - .unwrap(); - cx.executor().run_until_parked(); - cx.executor().start_waiting(); - let fake_server = fake_servers.next().await.unwrap(); - let editor = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx)); - - _ = 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 deleted file mode 100644 index 31c4e24659..0000000000 --- a/crates/editor2/src/items.rs +++ /dev/null @@ -1,1339 +0,0 @@ -use crate::{ - editor_settings::SeedQuerySetting, link_go_to_definition::hide_link_definition, - persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, EditorEvent, EditorSettings, - ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, -}; -use anyhow::{anyhow, Context as _, Result}; -use collections::HashSet; -use futures::future::try_join_all; -use gpui::{ - div, point, AnyElement, AppContext, AsyncWindowContext, Context, Entity, EntityId, - EventEmitter, IntoElement, Model, ParentElement, Pixels, Render, SharedString, Styled, - Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext, -}; -use language::{ - proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt, - Point, SelectionGoal, -}; -use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath}; -use rpc::proto::{self, update_view, PeerId}; -use settings::Settings; - -use std::fmt::Write; -use std::{ - borrow::Cow, - cmp::{self, Ordering}, - iter, - ops::Range, - path::{Path, PathBuf}, - sync::Arc, -}; -use text::Selection; -use theme::{ActiveTheme, Theme}; -use ui::{h_stack, prelude::*, Label}; -use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt}; -use workspace::{ - item::{BreadcrumbText, FollowEvent, FollowableItemHandle}, - StatusItemView, -}; -use workspace::{ - item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem}, - searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, - ItemId, ItemNavHistory, Pane, 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: View, - workspace: View, - remote_id: ViewId, - state: &mut Option, - cx: &mut WindowContext, - ) -> 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.update(&mut 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.new_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.new_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 if self.focus_handle.is_focused(cx) { - self.buffer.update(cx, |buffer, cx| { - buffer.set_active_selections( - &self.selections.disjoint_anchors(), - self.selections.line_mode, - self.cursor_shape, - cx, - ); - }); - } - cx.notify(); - } - - fn to_state_proto(&self, cx: &WindowContext) -> 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 to_follow_event(event: &EditorEvent) -> Option { - match event { - EditorEvent::Edited => Some(FollowEvent::Unfollow), - EditorEvent::SelectionsChanged { local } - | EditorEvent::ScrollPositionChanged { local, .. } => { - if *local { - Some(FollowEvent::Unfollow) - } else { - None - } - } - _ => None, - } - } - - fn add_event_to_update_proto( - &self, - event: &EditorEvent, - update: &mut Option, - cx: &WindowContext, - ) -> 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 { - EditorEvent::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 - } - EditorEvent::ExcerptsRemoved { ids } => { - update - .deleted_excerpts - .extend(ids.iter().map(ExcerptId::to_proto)); - true - } - EditorEvent::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 - } - EditorEvent::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: &Model, - 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 is_project_item(&self, _cx: &WindowContext) -> bool { - true - } -} - -async fn update_editor_from_message( - this: WeakView, - project: Model, - message: proto::update_view::Editor, - cx: &mut AsyncWindowContext, -) -> 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(()) -} - -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 { - type Event = EditorEvent; - - 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>(&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, selected: bool, cx: &WindowContext) -> AnyElement { - let _theme = cx.theme(); - - let description = detail.and_then(|detail| { - let path = path_for_buffer(&self.buffer, detail, false, cx)?; - let description = path.to_string_lossy(); - let description = description.trim(); - - if description.is_empty() { - return None; - } - - Some(util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN)) - }); - - h_stack() - .gap_2() - .child(Label::new(self.title(cx).to_string()).color(if selected { - Color::Default - } else { - Color::Muted - })) - .when_some(description, |this, description| { - this.child( - Label::new(description) - .size(LabelSize::XSmall) - .color(Color::Muted), - ) - }) - .into_any_element() - } - - 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.entity_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(cx.new_view(|cx| 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: 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(); - 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 - .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| { - 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(); - } - } - - Ok(()) - }) - } - - fn save_as( - &mut self, - project: Model, - 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: Model, 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(); - Ok(()) - }) - } - - fn as_searchable(&self, handle: &View) -> 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 - } - - fn breadcrumbs(&self, variant: &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(&variant.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().item_id().as_u64() as ItemId; - self.workspace = Some((workspace.weak_handle(), workspace.database_id())); - - fn serialize( - buffer: Model, - 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_executor() - .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().item_id().as_u64() as ItemId, - cx, - ); - } - } - }) - .detach(); - } - } - - fn serialized_item_kind() -> Option<&'static str> { - Some("Editor") - } - - fn to_item_events(event: &EditorEvent, mut f: impl FnMut(ItemEvent)) { - match event { - EditorEvent::Closed => f(ItemEvent::CloseItem), - - EditorEvent::Saved | EditorEvent::TitleChanged => { - f(ItemEvent::UpdateTab); - f(ItemEvent::UpdateBreadcrumbs); - } - - EditorEvent::Reparsed => { - f(ItemEvent::UpdateBreadcrumbs); - } - - EditorEvent::SelectionsChanged { local } if *local => { - f(ItemEvent::UpdateBreadcrumbs); - } - - EditorEvent::DirtyChanged => { - f(ItemEvent::UpdateTab); - } - - EditorEvent::BufferEdited => { - f(ItemEvent::Edit); - f(ItemEvent::UpdateBreadcrumbs); - } - - EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => { - f(ItemEvent::Edit); - } - - _ => {} - } - } - - fn deserialize( - project: Model, - _workspace: WeakView, - 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::() - .map_err(|_| anyhow!("Project item at stored path was not a buffer"))?; - Ok(pane.update(&mut cx, |_, cx| { - cx.new_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: Model, - buffer: Model, - cx: &mut ViewContext, - ) -> Self { - Self::for_buffer(buffer, Some(project), cx) - } -} - -impl EventEmitter for Editor {} - -pub(crate) enum BufferSearchHighlights {} -impl SearchableItem for Editor { - type Match = Range; - - 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 setting = EditorSettings::get_global(cx).seed_search_query_from_cursor; - let snapshot = &self.snapshot(cx).buffer_snapshot; - let selection = self.selections.newest::(cx); - - match setting { - SeedQuerySetting::Never => String::new(), - SeedQuerySetting::Selection | SeedQuerySetting::Always if !selection.is_empty() => { - snapshot - .text_for_range(selection.start..selection.end) - .collect() - } - SeedQuerySetting::Selection => String::new(), - SeedQuerySetting::Always => { - let (range, kind) = snapshot.surrounding_word(selection.start); - if kind == Some(CharKind::Word) { - let text: String = snapshot.text_for_range(range).collect(); - if !text.trim().is_empty() { - return text; - } - } - String::new() - } - } - } - - 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_executor().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: 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 Render for CursorPosition { - fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { - div().when_some(self.position, |el, position| { - 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(); - } - - el.child(Label::new(text).size(LabelSize::Small)) - }) - } -} - -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: &Model, - 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 deleted file mode 100644 index 42f502daed..0000000000 --- a/crates/editor2/src/link_go_to_definition.rs +++ /dev/null @@ -1,1279 +0,0 @@ -use crate::{ - display_map::DisplaySnapshot, - element::PointForPosition, - hover_popover::{self, InlayHover}, - Anchor, DisplayPoint, Editor, EditorSnapshot, GoToDefinition, GoToTypeDefinition, InlayId, - SelectPhase, -}; -use gpui::{px, Task, ViewContext}; -use language::{Bias, ToOffset}; -use lsp::LanguageServerId; -use project::{ - HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink, - ResolveState, -}; -use std::ops::Range; -use theme::ActiveTheme as _; -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 - 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 { - let style = gpui::HighlightStyle { - underline: Some(gpui::UnderlineStyle { - thickness: px(1.), - ..Default::default() - }), - color: Some(cx.theme().colors().link_text_hover), - ..Default::default() - }; - 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.focus_handle.is_focused(cx) { - 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)] -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::{Modifiers, ModifiersChangedEvent}; - 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.background_executor.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| { - crate::element::EditorElement::modifiers_changed( - editor, - &ModifiersChangedEvent { - modifiers: Modifiers { - command: 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.background_executor.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.background_executor.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| { - crate::element::EditorElement::modifiers_changed(editor, &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.background_executor.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.background_executor.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.background_executor.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| { - crate::element::EditorElement::modifiers_changed( - editor, - &ModifiersChangedEvent { - modifiers: Modifiers { - command: true, - ..Default::default() - }, - }, - cx, - ); - }); - requests.next().await; - cx.background_executor.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(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.background_executor.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.background_executor.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.background_executor.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.background_executor.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.background_executor.run_until_parked(); - assert!(requests.try_next().is_err()); - cx.assert_editor_text_highlights::(indoc! {" - fn test() { do_work(); } - fn do_work() { test(); } - "}); - cx.background_executor.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.background_executor.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.background_executor.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| { - crate::element::EditorElement::modifiers_changed( - editor, - &ModifiersChangedEvent { - modifiers: Modifiers { - command: 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| { - crate::element::EditorElement::modifiers_changed( - editor, - &ModifiersChangedEvent { - modifiers: Modifiers { - command: true, - ..Default::default() - }, - ..Default::default() - }, - cx, - ); - update_inlay_link_and_hover_points( - &editor.snapshot(cx), - hint_hover_position, - editor, - true, - false, - cx, - ); - }); - cx.background_executor.run_until_parked(); - cx.update_editor(|editor, cx| { - go_to_fetched_type_definition(editor, hint_hover_position, false, cx); - }); - cx.background_executor.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 deleted file mode 100644 index 24f3b22a5c..0000000000 --- a/crates/editor2/src/mouse_context_menu.rs +++ /dev/null @@ -1,110 +0,0 @@ -use crate::{ - DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition, - Rename, RevealInFinder, SelectMode, ToggleCodeActions, -}; -use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext}; - -pub struct MouseContextMenu { - pub(crate) position: Point, - pub(crate) context_menu: View, - _subscription: Subscription, -} - -pub fn deploy_context_menu( - editor: &mut Editor, - position: Point, - point: DisplayPoint, - cx: &mut ViewContext, -) { - if !editor.is_focused(cx) { - editor.focus(cx); - } - - // 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); - }); - - let context_menu = ui::ContextMenu::build(cx, |menu, _cx| { - menu.action("Rename Symbol", Box::new(Rename)) - .action("Go to Definition", Box::new(GoToDefinition)) - .action("Go to Type Definition", Box::new(GoToTypeDefinition)) - .action("Find All References", Box::new(FindAllReferences)) - .action( - "Code Actions", - Box::new(ToggleCodeActions { - deployed_from_indicator: false, - }), - ) - .separator() - .action("Reveal in Finder", Box::new(RevealInFinder)) - }); - let context_menu_focus = context_menu.focus_handle(cx); - cx.focus(&context_menu_focus); - - let _subscription = cx.subscribe(&context_menu, move |this, _, _event: &DismissEvent, cx| { - this.mouse_context_menu.take(); - if context_menu_focus.contains_focused(cx) { - this.focus(cx); - } - }); - - editor.mouse_context_menu = Some(MouseContextMenu { - position, - context_menu, - _subscription, - }); - 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.editor(|editor, _app| assert!(editor.mouse_context_menu.is_none())); - 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.is_some())); - } -} diff --git a/crates/editor2/src/movement.rs b/crates/editor2/src/movement.rs deleted file mode 100644 index cfccec253f..0000000000 --- a/crates/editor2/src/movement.rs +++ /dev/null @@ -1,926 +0,0 @@ -use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; -use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint}; -use gpui::{px, Pixels, TextSystem}; -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 text_system: Arc, - pub editor_style: EditorStyle, - pub rem_size: Pixels, -} - -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.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_display_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.display_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 = 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.into()), - ) -} - -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.into(), - SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(), - SelectionGoal::HorizontalRange { end, .. } => end.into(), - _ => map.x_for_display_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.display_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_display_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.into()), - ) -} - -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 gpui::{font, Context as _}; - 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 font = font("Helvetica"); - let font_size = px(14.0); - let buffer = MultiBuffer::build_simple(input_text, cx); - let buffer_snapshot = buffer.read(cx).snapshot(cx); - let display_map = - cx.new_model(|cx| DisplayMap::new(buffer, font, 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.update(cx, |editor, cx| editor.text_layout_details(cx)); - - let font = font("Helvetica"); - - let buffer = - cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abc\ndefg\nhijkl\nmn")); - let multibuffer = cx.new_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.new_model(|cx| DisplayMap::new(multibuffer, font, px(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_display_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.0), - 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_display_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.0), - false, - &text_layout_details - ), - ( - DisplayPoint::new(2, 3), - SelectionGoal::HorizontalPosition(col_4_x.0) - ), - ); - assert_eq!( - down( - &snapshot, - DisplayPoint::new(2, 3), - SelectionGoal::HorizontalPosition(col_4_x.0), - false, - &text_layout_details - ), - ( - DisplayPoint::new(3, 4), - SelectionGoal::HorizontalPosition(col_4_x.0) - ), - ); - - let col_5_x = - snapshot.x_for_display_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.0), - false, - &text_layout_details - ), - ( - DisplayPoint::new(3, 4), - SelectionGoal::HorizontalPosition(col_5_x.0) - ), - ); - assert_eq!( - down( - &snapshot, - DisplayPoint::new(3, 4), - SelectionGoal::HorizontalPosition(col_5_x.0), - false, - &text_layout_details - ), - ( - DisplayPoint::new(6, 5), - SelectionGoal::HorizontalPosition(col_5_x.0) - ), - ); - - let max_point_x = - snapshot.x_for_display_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.0) - ), - ); - assert_eq!( - down( - &snapshot, - DisplayPoint::new(7, 2), - SelectionGoal::HorizontalPosition(max_point_x.0), - false, - &text_layout_details - ), - ( - DisplayPoint::new(7, 2), - SelectionGoal::HorizontalPosition(max_point_x.0) - ), - ); - }); - } - - fn init_test(cx: &mut gpui::AppContext) { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - theme::init(theme::LoadThemes::JustBase, 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 deleted file mode 100644 index 6e37735c13..0000000000 --- a/crates/editor2/src/persistence.rs +++ /dev/null @@ -1,83 +0,0 @@ -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/rust_analyzer_ext.rs b/crates/editor2/src/rust_analyzer_ext.rs deleted file mode 100644 index 067d09d9ce..0000000000 --- a/crates/editor2/src/rust_analyzer_ext.rs +++ /dev/null @@ -1,119 +0,0 @@ -use std::sync::Arc; - -use anyhow::Context as _; -use gpui::{Context, View, ViewContext, VisualContext, WindowContext}; -use language::Language; -use multi_buffer::MultiBuffer; -use project::lsp_ext_command::ExpandMacro; -use text::ToPointUtf16; - -use crate::{element::register_action, Editor, ExpandMacroRecursively}; - -pub fn apply_related_actions(editor: &View, cx: &mut WindowContext) { - let is_rust_related = editor.update(cx, |editor, cx| { - editor - .buffer() - .read(cx) - .all_buffers() - .iter() - .any(|b| match b.read(cx).language() { - Some(l) => is_rust_language(l), - None => false, - }) - }); - - if is_rust_related { - register_action(editor, cx, expand_macro_recursively); - } -} - -pub fn expand_macro_recursively( - editor: &mut Editor, - _: &ExpandMacroRecursively, - cx: &mut ViewContext<'_, Editor>, -) { - if editor.selections.count() == 0 { - return; - } - let Some(project) = &editor.project else { - return; - }; - let Some(workspace) = editor.workspace() else { - return; - }; - - let multibuffer = editor.buffer().read(cx); - - let Some((trigger_anchor, rust_language, server_to_query, buffer)) = editor - .selections - .disjoint_anchors() - .into_iter() - .filter(|selection| selection.start == selection.end) - .filter_map(|selection| Some((selection.start.buffer_id?, selection.start))) - .filter_map(|(buffer_id, trigger_anchor)| { - let buffer = multibuffer.buffer(buffer_id)?; - let rust_language = buffer.read(cx).language_at(trigger_anchor.text_anchor)?; - if !is_rust_language(&rust_language) { - return None; - } - Some((trigger_anchor, rust_language, buffer)) - }) - .find_map(|(trigger_anchor, rust_language, buffer)| { - project - .read(cx) - .language_servers_for_buffer(buffer.read(cx), cx) - .into_iter() - .find_map(|(adapter, server)| { - if adapter.name.0.as_ref() == "rust-analyzer" { - Some(( - trigger_anchor, - Arc::clone(&rust_language), - server.server_id(), - buffer.clone(), - )) - } else { - None - } - }) - }) - else { - return; - }; - - let project = project.clone(); - let buffer_snapshot = buffer.read(cx).snapshot(); - let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot); - let expand_macro_task = project.update(cx, |project, cx| { - project.request_lsp( - buffer, - project::LanguageServerToQuery::Other(server_to_query), - ExpandMacro { position }, - cx, - ) - }); - cx.spawn(|_editor, mut cx| async move { - let macro_expansion = expand_macro_task.await.context("expand macro")?; - if macro_expansion.is_empty() { - log::info!("Empty macro expansion for position {position:?}"); - return Ok(()); - } - - let buffer = project.update(&mut cx, |project, cx| { - project.create_buffer(¯o_expansion.expansion, Some(rust_language), cx) - })??; - workspace.update(&mut cx, |workspace, cx| { - let buffer = cx.new_model(|cx| { - MultiBuffer::singleton(buffer, cx).with_title(macro_expansion.name) - }); - workspace.add_item( - Box::new(cx.new_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))), - cx, - ); - }) - }) - .detach_and_log_err(cx); -} - -fn is_rust_language(language: &Language) -> bool { - language.name().as_ref() == "Rust" -} diff --git a/crates/editor2/src/scroll.rs b/crates/editor2/src/scroll.rs deleted file mode 100644 index 0798870f76..0000000000 --- a/crates/editor2/src/scroll.rs +++ /dev/null @@ -1,460 +0,0 @@ -pub mod actions; -pub mod autoscroll; -pub mod scroll_amount; - -use crate::{ - display_map::{DisplaySnapshot, ToDisplayPoint}, - hover_popover::hide_hover, - persistence::DB, - Anchor, DisplayPoint, Editor, EditorEvent, EditorMode, InlayHintRefreshReason, - MultiBufferSnapshot, ToPoint, -}; -use gpui::{point, px, AppContext, Entity, Pixels, Task, ViewContext}; -use language::{Bias, Point}; -use std::{ - cmp::Ordering, - time::{Duration, Instant}, -}; -use util::ResultExt; -use workspace::{ItemId, WorkspaceId}; - -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: gpui::Point, - pub anchor: Anchor, -} - -impl ScrollAnchor { - fn new() -> Self { - Self { - offset: gpui::Point::default(), - anchor: Anchor::min(), - } - } - - 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.y = scroll_top + scroll_position.y; - } else { - scroll_position.y = 0.; - } - scroll_position - } - - pub fn top_row(&self, buffer: &MultiBufferSnapshot) -> u32 { - self.anchor.to_point(buffer).row - } -} - -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum Axis { - Vertical, - Horizontal, -} - -#[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 gpui::Point) -> Option { - const UNLOCK_PERCENT: f32 = 1.9; - const UNLOCK_LOWER_BOUND: Pixels = px(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 = point(px(0.), delta.y); - } - Some(Axis::Horizontal) => { - *delta = point(delta.x, px(0.)); - } - None => {} - } - - axis - } -} - -pub struct ScrollManager { - vertical_scroll_margin: f32, - anchor: ScrollAnchor, - ongoing: OngoingScroll, - autoscroll_request: Option<(Autoscroll, bool)>, - last_autoscroll: Option<(gpui::Point, f32, f32, AutoscrollStrategy)>, - show_scrollbars: bool, - hide_scrollbar_task: Option>, - dragging_scrollbar: bool, - 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, - dragging_scrollbar: false, - 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) -> gpui::Point { - self.anchor.scroll_position(snapshot) - } - - fn set_scroll_position( - &mut self, - 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. { - ( - ScrollAnchor { - anchor: Anchor::min(), - offset: scroll_position.max(&gpui::Point::default()), - }, - 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: point( - 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(EditorEvent::ScrollPositionChanged { local, autoscroll }); - self.show_scrollbar(cx); - self.autoscroll_request.take(); - if let Some(workspace_id) = workspace_id { - let item_id = cx.view().entity_id().as_u64() as ItemId; - - cx.foreground_executor() - .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_executor() - .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 is_dragging_scrollbar(&self) -> bool { - self.dragging_scrollbar - } - - pub fn set_is_dragging_scrollbar(&mut self, dragging: bool, cx: &mut ViewContext) { - if dragging != self.dragging_scrollbar { - self.dragging_scrollbar = dragging; - cx.notify(); - } - } - - pub fn clamp_scroll_left(&mut self, max: f32) -> bool { - if max < self.anchor.offset.x { - self.anchor.offset.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: 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(); - 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: u64, - 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/editor2/src/scroll/actions.rs b/crates/editor2/src/scroll/actions.rs deleted file mode 100644 index 21a4258f6f..0000000000 --- a/crates/editor2/src/scroll/actions.rs +++ /dev/null @@ -1,103 +0,0 @@ -use super::Axis; -use crate::{ - Autoscroll, Bias, Editor, EditorMode, NextScreen, ScrollAnchor, ScrollCursorBottom, - ScrollCursorCenter, ScrollCursorTop, -}; -use gpui::{Point, ViewContext}; - -impl Editor { - pub fn next_screen(&mut self, _: &NextScreen, cx: &mut ViewContext) { - if self.take_rename(true, cx).is_some() { - return; - } - - // todo!() - // if self.mouse_context_menu.read(cx).visible() { - // return None; - // } - - if matches!(self.mode, EditorMode::SingleLine) { - cx.propagate(); - return; - } - self.request_autoscroll(Autoscroll::Next, cx); - } - - pub fn scroll( - &mut self, - scroll_position: Point, - axis: Option, - cx: &mut ViewContext, - ) { - self.scroll_manager.update_ongoing_scroll(axis); - self.set_scroll_position(scroll_position, cx); - } - - pub fn scroll_cursor_top(&mut self, _: &ScrollCursorTop, cx: &mut ViewContext) { - let snapshot = self.snapshot(cx).display_snapshot; - let scroll_margin_rows = self.vertical_scroll_margin() as u32; - - let mut new_screen_top = self.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); - - self.set_scroll_anchor( - ScrollAnchor { - anchor: new_anchor, - offset: Default::default(), - }, - cx, - ) - } - - pub fn scroll_cursor_center(&mut self, _: &ScrollCursorCenter, cx: &mut ViewContext) { - let snapshot = self.snapshot(cx).display_snapshot; - let visible_rows = if let Some(visible_rows) = self.visible_line_count() { - visible_rows as u32 - } else { - return; - }; - - let mut new_screen_top = self.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); - - self.set_scroll_anchor( - ScrollAnchor { - anchor: new_anchor, - offset: Default::default(), - }, - cx, - ) - } - - pub fn scroll_cursor_bottom(&mut self, _: &ScrollCursorBottom, cx: &mut ViewContext) { - let snapshot = self.snapshot(cx).display_snapshot; - let scroll_margin_rows = self.vertical_scroll_margin() as u32; - let visible_rows = if let Some(visible_rows) = self.visible_line_count() { - visible_rows as u32 - } else { - return; - }; - - let mut new_screen_top = self.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); - - self.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 deleted file mode 100644 index ba70739942..0000000000 --- a/crates/editor2/src/scroll/autoscroll.rs +++ /dev/null @@ -1,253 +0,0 @@ -use std::{cmp, f32}; - -use gpui::{px, Pixels, 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: Pixels, - line_height: Pixels, - cx: &mut ViewContext, - ) -> bool { - let visible_lines = f32::from(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.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.y = target_top; - self.set_scroll_position_internal(scroll_position, local, true, cx); - } - if !needs_scroll_up && needs_scroll_down { - scroll_position.y = target_bottom - visible_lines; - self.set_scroll_position_internal(scroll_position, local, true, cx); - } - } - AutoscrollStrategy::Center => { - scroll_position.y = (target_top - margin).max(0.0); - self.set_scroll_position_internal(scroll_position, local, true, cx); - } - AutoscrollStrategy::Top => { - scroll_position.y = (target_top).max(0.0); - self.set_scroll_position_internal(scroll_position, local, true, cx); - } - AutoscrollStrategy::Bottom => { - scroll_position.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: Pixels, - scroll_width: Pixels, - max_glyph_width: Pixels, - 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 = px(0.); - target_right = px(0.); - } else { - 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 { - 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.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).into(); - 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 deleted file mode 100644 index 2cb22d1516..0000000000 --- a/crates/editor2/src/scroll/scroll_amount.rs +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 8d71916210..0000000000 --- a/crates/editor2/src/selections_collection.rs +++ /dev/null @@ -1,888 +0,0 @@ -use std::{ - cell::Ref, - iter, mem, - ops::{Deref, DerefMut, Range, Sub}, - sync::Arc, -}; - -use collections::HashMap; -use gpui::{AppContext, Model, Pixels}; -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: Model, - buffer: Model, - pub next_selection_id: usize, - pub line_mode: bool, - disjoint: Arc<[Selection]>, - pending: Option, -} - -impl SelectionsCollection { - pub fn new(display_map: Model, buffer: Model) -> 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 line = display_map.layout_row(row, &text_layout_details); - - let start_col = line.closest_index_for_x(positions.start) as u32; - if start_col < line_len || (is_empty && positions.start == line.width) { - let start = DisplayPoint::new(row, start_col); - let end_col = 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.into(), - end: positions.end.into(), - }, - }) - } 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) - where - I: IntoIterator>, - { - 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 deleted file mode 100644 index 4ce539ad79..0000000000 --- a/crates/editor2/src/test.rs +++ /dev/null @@ -1,74 +0,0 @@ -pub mod editor_lsp_test_context; -pub mod editor_test_context; - -use crate::{ - display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint}, - DisplayPoint, Editor, EditorMode, MultiBuffer, -}; - -use gpui::{Context, Model, Pixels, 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 font = cx.text_style().font(); - let font_size: Pixels = 14usize.into(); - - let buffer = MultiBuffer::build_simple(&unmarked_text, cx); - let display_map = cx.new_model(|cx| DisplayMap::new(buffer, font, 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: Model, cx: &mut ViewContext) -> Editor { - // todo!() - Editor::new(EditorMode::Full, buffer, None, /*None,*/ cx) -} - -pub(crate) fn build_editor_with_project( - project: Model, - buffer: Model, - cx: &mut ViewContext, -) -> Editor { - // todo!() - 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 deleted file mode 100644 index 7ee55cddba..0000000000 --- a/crates/editor2/src/test/editor_lsp_test_context.rs +++ /dev/null @@ -1,298 +0,0 @@ -use std::{ - borrow::Cow, - ops::{Deref, DerefMut, Range}, - sync::Arc, -}; - -use anyhow::Result; -use serde_json::json; - -use crate::{Editor, ToPoint}; -use collections::HashSet; -use futures::Future; -use gpui::{View, ViewContext, VisualTestContext}; -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::{AssertionContextManager, 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> { - 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_view(cx).unwrap(); - - let mut cx = VisualTestContext::from_window(*window.deref(), cx); - project - .update(&mut 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(&mut 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(&mut cx, |editor, cx| editor.focus(cx)); - - let lsp = fake_servers.next().await.unwrap(); - Self { - cx: EditorTestContext { - cx, - window: window.into(), - editor, - assertion_cx: AssertionContextManager::new(), - }, - 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(&mut 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 deleted file mode 100644 index bd5acb9945..0000000000 --- a/crates/editor2/src/test/editor_test_context.rs +++ /dev/null @@ -1,404 +0,0 @@ -use crate::{ - display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer, -}; -use collections::BTreeMap; -use futures::Future; -use gpui::{ - AnyWindowHandle, AppContext, Keystroke, ModelContext, View, ViewContext, VisualTestContext, -}; -use indoc::indoc; -use itertools::Itertools; -use language::{Buffer, BufferSnapshot}; -use parking_lot::RwLock; -use project::{FakeFs, Project}; -use std::{ - any::TypeId, - ops::{Deref, DerefMut, Range}, - sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, - }, -}; -use util::{ - assert_set_eq, - test::{generate_marked_text, marked_text_ranges}, -}; - -use super::build_editor_with_project; - -pub struct EditorTestContext<'a> { - pub cx: gpui::VisualTestContext<'a>, - pub window: AnyWindowHandle, - pub editor: View, - pub assertion_cx: AssertionContextManager, -} - -impl<'a> EditorTestContext<'a> { - pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> { - let fs = FakeFs::new(cx.executor()); - // 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 editor = cx.add_window(|cx| { - let editor = - build_editor_with_project(project, MultiBuffer::build_from_buffer(buffer, cx), cx); - editor.focus(cx); - editor - }); - let editor_view = editor.root_view(cx).unwrap(); - Self { - cx: VisualTestContext::from_window(*editor.deref(), cx), - window: editor.into(), - editor: editor_view, - assertion_cx: AssertionContextManager::new(), - } - } - - pub fn condition( - &self, - predicate: impl FnMut(&Editor, &AppContext) -> bool, - ) -> impl Future { - self.editor - .condition::(&self.cx, predicate) - } - - #[track_caller] - pub fn editor(&mut self, read: F) -> T - where - F: FnOnce(&Editor, &ViewContext) -> T, - { - self.editor - .update(&mut self.cx, |this, cx| read(&this, &cx)) - } - - #[track_caller] - pub fn update_editor(&mut self, update: F) -> T - where - F: FnOnce(&mut Editor, &mut ViewContext) -> T, - { - self.editor.update(&mut self.cx, update) - } - - pub fn multibuffer(&mut 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(&mut self) -> String { - self.multibuffer(|buffer, cx| buffer.snapshot(cx).text()) - } - - pub fn buffer(&mut 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(&mut self) -> BufferSnapshot { - self.buffer(|buffer, _| buffer.snapshot()) - } - - pub fn add_assertion_context(&self, context: String) -> ContextHandle { - self.assertion_cx.add_context(context) - } - - pub fn assertion_context(&self) -> String { - self.assertion_cx.context() - } - - 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. - self.cx.background_executor.run_until_parked(); - - keystrokes_under_test_handle - } - - pub fn run_until_parked(&mut self) { - self.cx.background_executor.run_until_parked(); - } - - pub fn ranges(&mut 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(&mut 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(&mut 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(&mut 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(&mut 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) - } - - #[track_caller] - fn editor_selections(&mut self) -> Vec> { - self.editor - .update(&mut 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 - } -} - -/// Tracks string context to be printed when assertions fail. -/// Often this is done by storing a context string in the manager and returning the handle. -#[derive(Clone)] -pub struct AssertionContextManager { - id: Arc, - contexts: Arc>>, -} - -impl AssertionContextManager { - pub fn new() -> Self { - Self { - id: Arc::new(AtomicUsize::new(0)), - contexts: Arc::new(RwLock::new(BTreeMap::new())), - } - } - - pub fn add_context(&self, context: String) -> ContextHandle { - let id = self.id.fetch_add(1, Ordering::Relaxed); - let mut contexts = self.contexts.write(); - contexts.insert(id, context); - ContextHandle { - id, - manager: self.clone(), - } - } - - pub fn context(&self) -> String { - let contexts = self.contexts.read(); - format!("\n{}\n", contexts.values().join("\n")) - } -} - -/// Used to track the lifetime of a piece of context so that it can be provided when an assertion fails. -/// For example, in the EditorTestContext, `set_state` returns a context handle so that if an assertion fails, -/// the state that was set initially for the failure can be printed in the error message -pub struct ContextHandle { - id: usize, - manager: AssertionContextManager, -} - -impl Drop for ContextHandle { - fn drop(&mut self) { - let mut contexts = self.manager.contexts.write(); - contexts.remove(&self.id); - } -} diff --git a/crates/feedback/Cargo.toml b/crates/feedback/Cargo.toml index ef7d08ffb3..0f6873335e 100644 --- a/crates/feedback/Cargo.toml +++ b/crates/feedback/Cargo.toml @@ -13,7 +13,7 @@ test-support = [] [dependencies] client = { package = "client2", path = "../client2" } db = { package = "db2", path = "../db2" } -editor = { package = "editor2", path = "../editor2" } +editor = { path = "../editor" } gpui = { package = "gpui2", path = "../gpui2" } language = { package = "language2", path = "../language2" } menu = { package = "menu2", path = "../menu2" } @@ -44,4 +44,4 @@ tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", urlencoding = "2.1.2" [dev-dependencies] -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index 3f62ac79b2..77174dd764 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -9,7 +9,7 @@ path = "src/file_finder.rs" doctest = false [dependencies] -editor = { package = "editor2", path = "../editor2" } +editor = { path = "../editor" } collections = { path = "../collections" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" } gpui = { package = "gpui2", path = "../gpui2" } @@ -26,7 +26,7 @@ postage.workspace = true serde.workspace = true [dev-dependencies] -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } language = { package = "language2", path = "../language2", features = ["test-support"] } workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } diff --git a/crates/go_to_line/Cargo.toml b/crates/go_to_line/Cargo.toml index 3e4f4a2e1d..631721ce6b 100644 --- a/crates/go_to_line/Cargo.toml +++ b/crates/go_to_line/Cargo.toml @@ -9,7 +9,7 @@ path = "src/go_to_line.rs" doctest = false [dependencies] -editor = { package = "editor2", path = "../editor2" } +editor = { path = "../editor" } gpui = { package = "gpui2", path = "../gpui2" } menu = { package = "menu2", path = "../menu2" } serde.workspace = true @@ -22,4 +22,4 @@ ui = { package = "ui2", path = "../ui2" } util = { path = "../util" } [dev-dependencies] -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/journal2/Cargo.toml b/crates/journal2/Cargo.toml index ce307948ad..72da3deb69 100644 --- a/crates/journal2/Cargo.toml +++ b/crates/journal2/Cargo.toml @@ -9,7 +9,7 @@ path = "src/journal2.rs" doctest = false [dependencies] -editor = { package = "editor2", path = "../editor2" } +editor = { path = "../editor" } gpui = { package = "gpui2", path = "../gpui2" } util = { path = "../util" } workspace2 = { path = "../workspace2" } @@ -24,4 +24,4 @@ log.workspace = true shellexpand = "2.1.0" [dev-dependencies] -editor = { package="editor2", path = "../editor2", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/language_selector/Cargo.toml b/crates/language_selector/Cargo.toml index ac9416eb87..b897b9062e 100644 --- a/crates/language_selector/Cargo.toml +++ b/crates/language_selector/Cargo.toml @@ -9,7 +9,7 @@ path = "src/language_selector.rs" doctest = false [dependencies] -editor = { package = "editor2", path = "../editor2" } +editor = { path = "../editor" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" } language = { package = "language2", path = "../language2" } gpui = { package = "gpui2", path = "../gpui2" } @@ -23,4 +23,4 @@ workspace = { package = "workspace2", path = "../workspace2" } anyhow.workspace = true [dev-dependencies] -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/language_tools/Cargo.toml b/crates/language_tools/Cargo.toml index 95b15067fa..0dd7387fc2 100644 --- a/crates/language_tools/Cargo.toml +++ b/crates/language_tools/Cargo.toml @@ -10,7 +10,7 @@ doctest = false [dependencies] collections = { path = "../collections" } -editor = { package = "editor2", path = "../editor2" } +editor = { path = "../editor" } settings = { package = "settings2", path = "../settings2" } theme = { package = "theme2", path = "../theme2" } language = { package = "language2", path = "../language2" } @@ -27,7 +27,7 @@ tree-sitter.workspace = true [dev-dependencies] client = { package = "client2", path = "../client2", features = ["test-support"] } -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } env_logger.workspace = true diff --git a/crates/outline2/Cargo.toml b/crates/outline2/Cargo.toml index d05359898c..3075c011c1 100644 --- a/crates/outline2/Cargo.toml +++ b/crates/outline2/Cargo.toml @@ -9,7 +9,7 @@ path = "src/outline.rs" doctest = false [dependencies] -editor = { package = "editor2", path = "../editor2" } +editor = { path = "../editor" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" } gpui = { package = "gpui2", path = "../gpui2" } ui = { package = "ui2", path = "../ui2" } @@ -26,4 +26,4 @@ postage.workspace = true smol.workspace = true [dev-dependencies] -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/picker/Cargo.toml b/crates/picker/Cargo.toml index 2422fe29a5..33401e4cab 100644 --- a/crates/picker/Cargo.toml +++ b/crates/picker/Cargo.toml @@ -9,7 +9,7 @@ path = "src/picker.rs" doctest = false [dependencies] -editor = { package = "editor2", path = "../editor2" } +editor = { path = "../editor" } ui = { package = "ui2", path = "../ui2" } gpui = { package = "gpui2", path = "../gpui2" } menu = { package = "menu2", path = "../menu2" } @@ -21,7 +21,7 @@ workspace = { package = "workspace2", path = "../workspace2"} parking_lot.workspace = true [dev-dependencies] -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } serde_json.workspace = true ctor.workspace = true diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index 457e6b25b0..ff75d05552 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -11,7 +11,7 @@ doctest = false [dependencies] collections = { path = "../collections" } db = { path = "../db2", package = "db2" } -editor = { path = "../editor2", package = "editor2" } +editor = { path = "../editor" } gpui = { path = "../gpui2", package = "gpui2" } menu = { path = "../menu2", package = "menu2" } project = { path = "../project2", package = "project2" } @@ -35,7 +35,7 @@ unicase = "2.6" [dev-dependencies] client = { path = "../client2", package = "client2", features = ["test-support"] } language = { path = "../language2", package = "language2", features = ["test-support"] } -editor = { path = "../editor2", package = "editor2", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui2", package = "gpui2", features = ["test-support"] } workspace = { path = "../workspace2", package = "workspace2", features = ["test-support"] } serde_json.workspace = true diff --git a/crates/project_symbols/Cargo.toml b/crates/project_symbols/Cargo.toml index fa38b2b954..5841c1d050 100644 --- a/crates/project_symbols/Cargo.toml +++ b/crates/project_symbols/Cargo.toml @@ -9,7 +9,7 @@ path = "src/project_symbols.rs" doctest = false [dependencies] -editor = { package = "editor2", path = "../editor2" } +editor = { path = "../editor" } fuzzy = {package = "fuzzy2", path = "../fuzzy2" } gpui = {package = "gpui2", path = "../gpui2" } picker = {path = "../picker" } @@ -27,7 +27,7 @@ smol.workspace = true [dev-dependencies] futures.workspace = true -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } settings = { package = "settings2", path = "../settings2", features = ["test-support"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } language = { package = "language2", path = "../language2", features = ["test-support"] } diff --git a/crates/quick_action_bar/Cargo.toml b/crates/quick_action_bar/Cargo.toml index 5a421a805a..0fb06fb98c 100644 --- a/crates/quick_action_bar/Cargo.toml +++ b/crates/quick_action_bar/Cargo.toml @@ -10,13 +10,13 @@ doctest = false [dependencies] assistant = { package = "assistant2", path = "../assistant2" } -editor = { package = "editor2", path = "../editor2" } +editor = { path = "../editor" } gpui = { package = "gpui2", path = "../gpui2" } search = { path = "../search" } workspace = { package = "workspace2", path = "../workspace2" } ui = { package = "ui2", path = "../ui2" } [dev-dependencies] -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index e369709d03..7d13d5967b 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -9,7 +9,7 @@ path = "src/recent_projects.rs" doctest = false [dependencies] -editor = { package = "editor2", path = "../editor2" } +editor = { path = "../editor" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" } gpui = { package = "gpui2", path = "../gpui2" } language = { package = "language2", path = "../language2" } @@ -27,4 +27,4 @@ postage.workspace = true smol.workspace = true [dev-dependencies] -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index 5bb318992e..b03e0727cc 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -11,7 +11,7 @@ doctest = false [dependencies] bitflags = "1" collections = { path = "../collections" } -editor = { package = "editor2", path = "../editor2" } +editor = { path = "../editor" } gpui = { package = "gpui2", path = "../gpui2" } language = { package = "language2", path = "../language2" } menu = { package = "menu2", path = "../menu2" } @@ -33,7 +33,7 @@ smol.workspace = true serde_json.workspace = true [dev-dependencies] client = { package = "client2", path = "../client2", features = ["test-support"] } -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } diff --git a/crates/storybook2/Cargo.toml b/crates/storybook2/Cargo.toml index 5fca449d00..ee2b008416 100644 --- a/crates/storybook2/Cargo.toml +++ b/crates/storybook2/Cargo.toml @@ -15,7 +15,7 @@ backtrace-on-stack-overflow = "0.3.0" chrono = "0.4" clap = { version = "4.4", features = ["derive", "string"] } dialoguer = { version = "0.11.0", features = ["fuzzy-select"] } -editor = { package = "editor2", path = "../editor2" } +editor = { path = "../editor" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" } gpui = { package = "gpui2", path = "../gpui2" } indoc.workspace = true diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml index 390cd0545f..74a0d26d2e 100644 --- a/crates/terminal_view/Cargo.toml +++ b/crates/terminal_view/Cargo.toml @@ -9,7 +9,7 @@ path = "src/terminal_view.rs" doctest = false [dependencies] -editor = { package = "editor2", path = "../editor2" } +editor = { path = "../editor" } language = { package = "language2", path = "../language2" } gpui = { package = "gpui2", path = "../gpui2" } project = { package = "project2", path = "../project2" } @@ -38,7 +38,7 @@ serde.workspace = true serde_derive.workspace = true [dev-dependencies] -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } client = { package = "client2", path = "../client2", features = ["test-support"]} project = { package = "project2", path = "../project2", features = ["test-support"]} diff --git a/crates/theme_selector/Cargo.toml b/crates/theme_selector/Cargo.toml index ac5529391a..ee17388ffa 100644 --- a/crates/theme_selector/Cargo.toml +++ b/crates/theme_selector/Cargo.toml @@ -10,7 +10,7 @@ doctest = false [dependencies] client = { package = "client2", path = "../client2" } -editor = { package = "editor2", path = "../editor2" } +editor = { path = "../editor" } feature_flags = { package = "feature_flags2", path = "../feature_flags2" } fs = { package = "fs2", path = "../fs2" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" } @@ -27,4 +27,4 @@ postage.workspace = true smol.workspace = true [dev-dependencies] -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 223f5442a3..72cddae989 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -26,7 +26,7 @@ serde_json.workspace = true collections = { path = "../collections" } command_palette = { path = "../command_palette" } -editor = { package = "editor2", path = "../editor2" } +editor = { path = "../editor" } gpui = { package = "gpui2", path = "../gpui2" } language = { package = "language2", path = "../language2" } search = { path = "../search" } @@ -42,7 +42,7 @@ indoc.workspace = true parking_lot.workspace = true futures.workspace = true -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } language = { package = "language2", path = "../language2", features = ["test-support"] } project = { package = "project2", path = "../project2", features = ["test-support"] } diff --git a/crates/welcome/Cargo.toml b/crates/welcome/Cargo.toml index 44e6f0fd92..c7ebc5c2a9 100644 --- a/crates/welcome/Cargo.toml +++ b/crates/welcome/Cargo.toml @@ -12,7 +12,7 @@ test-support = [] [dependencies] client = { package = "client2", path = "../client2" } -editor = { package = "editor2", path = "../editor2" } +editor = { path = "../editor" } fs = { package = "fs2", path = "../fs2" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" } gpui = { package = "gpui2", path = "../gpui2" } @@ -34,4 +34,4 @@ schemars.workspace = true serde.workspace = true [dev-dependencies] -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 8a9d0eb0ab..294ed5690c 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -33,7 +33,7 @@ copilot = { package = "copilot2", path = "../copilot2" } copilot_button = { path = "../copilot_button" } diagnostics = { path = "../diagnostics" } db = { package = "db2", path = "../db2" } -editor = { package="editor2", path = "../editor2" } +editor = { path = "../editor" } feedback = { path = "../feedback" } file_finder = { path = "../file_finder" } search = { path = "../search" }