Detailed changes
@@ -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",
@@ -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"] }
@@ -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
@@ -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"] }
@@ -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"] }
@@ -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"] }
@@ -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" }
@@ -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" }
@@ -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"] }
@@ -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"] }
@@ -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
@@ -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>) -> Self {
// Make sure we blink the cursors if the setting is re-enabled
- cx.observe_global::<SettingsStore, _>(move |this, cx| {
+ cx.observe_global::<SettingsStore>(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<Self>) {
- if settings::get::<EditorSettings>(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<Self>) {
+ 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 = ();
-}
@@ -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<Option<TypeId>, Arc<(HighlightStyle, Vec<Range<Anc
type InlayHighlights = BTreeMap<TypeId, HashMap<InlayId, (HighlightStyle, InlayHighlight)>>;
pub struct DisplayMap {
- buffer: ModelHandle<MultiBuffer>,
+ buffer: Model<MultiBuffer>,
buffer_subscription: BufferSubscription,
fold_map: FoldMap,
inlay_map: InlayMap,
tab_map: TabMap,
- wrap_map: ModelHandle<WrapMap>,
+ wrap_map: Model<WrapMap>,
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<MultiBuffer>,
- font_id: FontId,
- font_size: f32,
- wrap_width: Option<f32>,
+ buffer: Model<MultiBuffer>,
+ font: Font,
+ font_size: Pixels,
+ wrap_width: Option<Pixels>,
buffer_header_height: u8,
excerpt_header_height: u8,
cx: &mut ModelContext<Self>,
@@ -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<Self>) -> bool {
+ pub fn set_font(&self, font: Font, font_size: Pixels, cx: &mut ModelContext<Self>) -> 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<f32>, cx: &mut ModelContext<Self>) -> bool {
+ pub fn set_wrap_width(&self, width: Option<Pixels>, cx: &mut ModelContext<Self>) -> 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<MultiBuffer>, cx: &mut ModelContext<Self>) -> NonZeroU32 {
+ fn tab_size(buffer: &Model<MultiBuffer>, cx: &mut ModelContext<Self>) -> NonZeroU32 {
let language = buffer
.read(cx)
.as_singleton()
@@ -510,18 +505,18 @@ impl DisplaySnapshot {
&'a self,
display_rows: Range<u32>,
language_aware: bool,
- style: &'a EditorStyle,
+ editor_style: &'a EditorStyle,
) -> impl Iterator<Item = HighlightedChunk<'a>> {
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<LineLayout> {
+ 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<T>(&self, range: Range<T>) -> impl Iterator<Item = &Range<Anchor>>
+ pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Fold>
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::<SettingsStore, _, _>(|store, cx| {
+ cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings::<AllLanguageSettings>(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::<Vec<_>>();
@@ -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));
-
- let font_cache = cx.font_cache().clone();
+ editor.update(cx, |editor, cx| editor.text_layout_details(cx));
- 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<u32>,
- map: &ModelHandle<DisplayMap>,
+ map: &Model<DisplayMap>,
theme: &'a SyntaxTheme,
cx: &mut AppContext,
- ) -> Vec<(String, Option<Color>)> {
+ ) -> Vec<(String, Option<Hsla>)> {
chunks(rows, map, theme, cx)
.into_iter()
.map(|(text, color, _)| (text, color))
@@ -1876,12 +1818,12 @@ pub mod tests {
fn chunks<'a>(
rows: Range<u32>,
- map: &ModelHandle<DisplayMap>,
+ map: &Model<DisplayMap>,
theme: &'a SyntaxTheme,
cx: &mut AppContext,
- ) -> Vec<(String, Option<Color>, Option<Color>)> {
+ ) -> Vec<(String, Option<Hsla>, Option<Hsla>)> {
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
- let mut chunks: Vec<(String, Option<Color>, Option<Color>)> = Vec::new();
+ let mut chunks: Vec<(String, Option<Hsla>, Option<Hsla>)> = 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::<SettingsStore, _, _>(|store, cx| {
+ theme::init(LoadThemes::JustBase, cx);
+ cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, f);
});
}
@@ -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<dyn Fn(&mut BlockContext) -> AnyElement<Editor>>;
+pub type RenderBlock = Arc<dyn Fn(&mut BlockContext) -> AnyElement>;
pub struct Block {
id: BlockId,
@@ -69,7 +69,7 @@ where
pub position: P,
pub height: u8,
pub style: BlockStyle,
- pub render: Arc<dyn Fn(&mut BlockContext) -> AnyElement<Editor>>,
+ pub render: Arc<dyn Fn(&mut BlockContext) -> 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<Editor> {
+ 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);
-
- 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_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 = 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::<String>();
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::<Vec<_>>();
@@ -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 {
@@ -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::<Fold>();
+ let mut cursor = self.0.snapshot.folds.cursor::<FoldRange>();
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<Color>,
+ ellipses_color: Option<Hsla>,
+ 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::<Fold>();
+ let mut folds_cursor = self.snapshot.folds.cursor::<FoldRange>();
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<Fold>,
pub inlay_snapshot: InlaySnapshot,
pub version: usize,
- pub ellipses_color: Option<Color>,
+ pub ellipses_color: Option<Hsla>,
}
impl FoldSnapshot {
@@ -596,13 +604,13 @@ impl FoldSnapshot {
self.transforms.summary().output.longest_row
}
- pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Range<Anchor>>
+ pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Fold>
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<Anchor>);
+#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
+pub struct FoldId(usize);
+
+impl Into<ElementId> 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<Anchor>);
+
+impl Deref for FoldRange {
+ type Target = Range<Anchor>;
+
+ 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 Fold {
+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<Color>,
+ ellipses_color: Option<Hsla>,
}
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::<Vec<_>>();
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::<Vec<_>>();
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();
@@ -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);
}
}
@@ -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<u32>;
@@ -22,13 +21,9 @@ pub struct WrapMap {
pending_edits: VecDeque<(TabSnapshot, Vec<TabEdit>)>,
interpolated_edits: Patch<u32>,
edits_since_sync: Patch<u32>,
- wrap_width: Option<f32>,
+ wrap_width: Option<Pixels>,
background_task: Option<Task<()>>,
- 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<f32>,
+ font: Font,
+ font_size: Pixels,
+ wrap_width: Option<Pixels>,
cx: &mut AppContext,
- ) -> (ModelHandle<Self>, WrapSnapshot) {
- let handle = cx.add_model(|cx| {
+ ) -> (Model<Self>, 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<Self>,
) -> 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<f32>, cx: &mut ModelContext<Self>) -> bool {
+ pub fn set_wrap_width(
+ &mut self,
+ wrap_width: Option<Pixels>,
+ cx: &mut ModelContext<Self>,
+ ) -> 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<u32> {
#[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<f32>,
+ wrap_width: Option<Pixels>,
line_wrapper: &mut LineWrapper,
) -> String {
if let Some(wrap_width) = wrap_width {
@@ -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,
-};
-use link_go_to_definition::{
- hide_link_definition, show_link_definition, GoToDefinitionLink, InlayHighlight,
- LinkGoToDefinitionState,
+ Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language,
+ LanguageRegistry, LanguageServerName, OffsetRangeExt, Point, Selection, SelectionGoal,
+ TransactionId,
};
-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<Tag: 'static>(
+pub fn render_parsed_markdown(
+ element_id: impl Into<ElementId>,
parsed: &language::ParsedMarkdown,
editor_style: &EditorStyle,
- workspace: Option<WeakViewHandle<Workspace>>,
+ workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
-) -> Text {
- enum RenderedMarkdown {}
-
- let parsed = parsed.clone();
- let view_id = cx.view_id();
- let code_span_background_color = editor_style.document_highlight_read_background;
-
- let mut region_id = 0;
-
- Text::new(parsed.text, editor_style.text.clone())
- .with_highlights(
- parsed
- .highlights
- .iter()
- .filter_map(|(range, highlight)| {
- let highlight = highlight.to_highlight_style(&editor_style.syntax)?;
- Some((range.clone(), highlight))
- })
- .collect::<Vec<_>>(),
- )
- .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::<Editor, _>(MouseButton::Left, move |_, _, cx| match &link {
- markdown::Link::Web { url } => cx.platform().open_url(url),
- markdown::Link::Path { path } => {
- if let Some(workspace) = &workspace {
- _ = workspace.update(cx, |workspace, cx| {
- workspace.open_abs_path(path.clone(), false, cx).detach();
- });
- }
- }
- }),
- );
- }
-
- if region.code {
- cx.scene().push_quad(gpui::Quad {
- bounds,
- background: Some(code_span_background_color),
- border: Default::default(),
- corner_radii: (2.0).into(),
- });
+) -> 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();
+ });
+ }
}
- })
- .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<usize>,
}
-#[derive(Clone, Default, Deserialize, PartialEq)]
+#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ConfirmCodeAction {
#[serde(default)]
pub item_ix: Option<usize>,
}
-#[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,
- Newline,
- NewlineAbove,
- NewlineBelow,
- GoToDiagnostic,
- GoToPrevDiagnostic,
- GoToHunk,
- GoToPrevHunk,
- Indent,
- Outdent,
DeleteLine,
- DeleteToPreviousWordStart,
- DeleteToPreviousSubwordStart,
- DeleteToNextWordEnd,
- DeleteToNextSubwordEnd,
DeleteToBeginningOfLine,
DeleteToEndOfLine,
- CutToEndOfLine,
+ DeleteToNextSubwordEnd,
+ DeleteToNextWordEnd,
+ DeleteToPreviousSubwordStart,
+ DeleteToPreviousWordStart,
DuplicateLine,
ExpandMacroRecursively,
- MoveLineUp,
- MoveLineDown,
+ FindAllReferences,
+ Fold,
+ FoldSelectedRanges,
+ Format,
+ GoToDefinition,
+ GoToDefinitionSplit,
+ GoToDiagnostic,
+ GoToHunk,
+ GoToPrevDiagnostic,
+ GoToPrevHunk,
+ GoToTypeDefinition,
+ GoToTypeDefinitionSplit,
+ HalfPageDown,
+ HalfPageUp,
+ Hover,
+ Indent,
JoinLines,
- SortLinesCaseSensitive,
- SortLinesCaseInsensitive,
- ReverseLines,
- ShuffleLines,
- ConvertToUpperCase,
- ConvertToLowerCase,
- ConvertToTitleCase,
- ConvertToSnakeCase,
- ConvertToKebabCase,
- ConvertToUpperCamelCase,
- ConvertToLowerCamelCase,
- Transpose,
- Cut,
- Copy,
- Paste,
- Undo,
- Redo,
- MoveUp,
- PageUp,
+ LineDown,
+ LineUp,
MoveDown,
- PageDown,
MoveLeft,
+ MoveLineDown,
+ MoveLineUp,
MoveRight,
- MoveToPreviousWordStart,
- MoveToPreviousSubwordStart,
- MoveToNextWordEnd,
- MoveToNextSubwordEnd,
+ MoveToBeginning,
MoveToBeginningOfLine,
+ MoveToEnclosingBracket,
+ MoveToEnd,
MoveToEndOfLine,
- MoveToStartOfParagraph,
MoveToEndOfParagraph,
- MoveToBeginning,
- MoveToEnd,
- SelectUp,
+ 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,
- 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,
+ ToggleInlayHints,
+ ToggleSoftWrap,
+ Transpose,
+ Undo,
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
]
);
@@ -422,144 +421,41 @@ pub enum Direction {
}
pub fn init_settings(cx: &mut AppContext) {
- settings::register::<EditorSettings>(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::<Editor>(cx);
workspace::register_followable_item::<Editor>(cx);
workspace::register_deserializable_item::<Editor>(cx);
+ cx.observe_new_views(
+ |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
+ 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::<Weak<workspace::AppState>>();
+ 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::<Weak<workspace::AppState>>();
+ 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<f32>,
},
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<TextStyle>,
- pub theme: theme::Editor,
- pub theme_id: usize,
+ pub scrollbar_width: Pixels,
+ pub syntax: Arc<SyntaxTheme>,
+ 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<HighlightStyle>;
+// type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor;
+// type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
-type BackgroundHighlight = (fn(&Theme) -> Color, Vec<Range<Anchor>>);
-type InlayBackgroundHighlight = (fn(&Theme) -> Color, Vec<InlayHighlight>);
+type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Vec<Range<Anchor>>);
+type InlayBackgroundHighlight = (fn(&ThemeColors) -> Hsla, Vec<InlayHighlight>);
pub struct Editor {
- handle: WeakViewHandle<Self>,
- buffer: ModelHandle<MultiBuffer>,
- display_map: ModelHandle<DisplayMap>,
+ handle: WeakView<Self>,
+ focus_handle: FocusHandle,
+ buffer: Model<MultiBuffer>,
+ display_map: Model<DisplayMap>,
pub selections: SelectionsCollection,
pub scroll_manager: ScrollManager,
columnar_selection_tail: Option<Anchor>,
@@ -645,12 +545,9 @@ pub struct Editor {
ime_transaction: Option<TransactionId>,
active_diagnostics: Option<ActiveDiagnosticGroup>,
soft_wrap_mode_override: Option<language_settings::SoftWrap>,
- get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
- override_text_style: Option<Box<OverrideTextStyle>>,
- project: Option<ModelHandle<Project>>,
+ project: Option<Model<Project>>,
collaboration_hub: Option<Box<dyn CollaborationHub>>,
- focused: bool,
- blink_manager: ModelHandle<BlinkManager>,
+ blink_manager: Model<BlinkManager>,
pub show_local_selections: bool,
mode: EditorMode,
show_gutter: bool,
@@ -661,10 +558,10 @@ pub struct Editor {
inlay_background_highlights: TreeMap<Option<TypeId>, InlayBackgroundHighlight>,
nav_history: Option<ItemNavHistory>,
context_menu: RwLock<Option<ContextMenu>>,
- mouse_context_menu: ViewHandle<context_menu::ContextMenu>,
+ mouse_context_menu: Option<MouseContextMenu>,
completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
next_completion_id: CompletionId,
- available_code_actions: Option<(ModelHandle<Buffer>, Arc<[CodeAction]>)>,
+ available_code_actions: Option<(Model<Buffer>, Arc<[CodeAction]>)>,
code_actions_task: Option<Task<()>>,
document_highlights_task: Option<Task<()>>,
pending_rename: Option<RenameState>,
@@ -672,8 +569,8 @@ pub struct Editor {
cursor_shape: CursorShape,
collapse_matches: bool,
autoindent_mode: Option<AutoindentMode>,
- workspace: Option<(WeakViewHandle<Workspace>, i64)>,
- keymap_context_layers: BTreeMap<TypeId, KeymapContext>,
+ workspace: Option<(WeakView<Workspace>, i64)>,
+ keymap_context_layers: BTreeMap<TypeId, KeyContext>,
input_enabled: bool,
read_only: bool,
leader_peer_id: Option<PeerId>,
@@ -685,7 +582,10 @@ pub struct Editor {
inlay_hint_cache: InlayHintCache,
next_inlay_id: usize,
_subscriptions: Vec<Subscription>,
- pixel_position_of_newest_cursor: Option<Vector2F>,
+ pixel_position_of_newest_cursor: Option<gpui::Point<Pixels>>,
+ gutter_width: Pixels,
+ style: Option<EditorStyle>,
+ editor_actions: Vec<Box<dyn Fn(&mut ViewContext<Self>)>>,
}
pub struct EditorSnapshot {
@@ -841,7 +741,7 @@ struct SnippetState {
pub struct RenameState {
pub range: Range<Anchor>,
pub old_name: Arc<str>,
- pub editor: ViewHandle<Editor>,
+ pub editor: View<Editor>,
block_id: BlockId,
}
@@ -855,7 +755,7 @@ enum ContextMenu {
impl ContextMenu {
fn select_first(
&mut self,
- project: Option<&ModelHandle<Project>>,
+ project: Option<&Model<Project>>,
cx: &mut ViewContext<Editor>,
) -> bool {
if self.visible() {
@@ -871,7 +771,7 @@ impl ContextMenu {
fn select_prev(
&mut self,
- project: Option<&ModelHandle<Project>>,
+ project: Option<&Model<Project>>,
cx: &mut ViewContext<Editor>,
) -> bool {
if self.visible() {
@@ -887,7 +787,7 @@ impl ContextMenu {
fn select_next(
&mut self,
- project: Option<&ModelHandle<Project>>,
+ project: Option<&Model<Project>>,
cx: &mut ViewContext<Editor>,
) -> bool {
if self.visible() {
@@ -903,7 +803,7 @@ impl ContextMenu {
fn select_last(
&mut self,
- project: Option<&ModelHandle<Project>>,
+ project: Option<&Model<Project>>,
cx: &mut ViewContext<Editor>,
) -> bool {
if self.visible() {
@@ -927,13 +827,17 @@ impl ContextMenu {
fn render(
&self,
cursor_position: DisplayPoint,
- style: EditorStyle,
- workspace: Option<WeakViewHandle<Workspace>>,
+ style: &EditorStyle,
+ max_height: Pixels,
+ workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
- ) -> (DisplayPoint, AnyElement<Editor>) {
+ ) -> (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>,
+ buffer: Model<Buffer>,
completions: Arc<RwLock<Box<[Completion]>>>,
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<Project>>,
- cx: &mut ViewContext<Editor>,
- ) {
+ fn select_first(&mut self, project: Option<&Model<Project>>, cx: &mut ViewContext<Editor>) {
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<Project>>,
- cx: &mut ViewContext<Editor>,
- ) {
+ fn select_prev(&mut self, project: Option<&Model<Project>>, cx: &mut ViewContext<Editor>) {
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<Project>>,
- cx: &mut ViewContext<Editor>,
- ) {
+ fn select_next(&mut self, project: Option<&Model<Project>>, cx: &mut ViewContext<Editor>) {
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<Project>>,
- cx: &mut ViewContext<Editor>,
- ) {
+ fn select_last(&mut self, project: Option<&Model<Project>>, cx: &mut ViewContext<Editor>) {
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<Editor>,
) -> Option<Task<()>> {
- let settings = settings::get::<EditorSettings>(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;
};
@@ -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<bool>,
}
-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> {
Self::load_via_json_merge(default_value, user_values)
}
@@ -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);
-
- editor.update(cx, |editor, 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.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::<f32>::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::<f32>::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::<f32>::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::<f32>::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::<f32>::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::<f32>::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::<Vec<_>>(),
);
assert_set_eq!(
- cloned_editor.read_with(cx, |editor, cx| editor.selections.ranges::<Point>(cx)),
- editor.read_with(cx, |editor, cx| editor.selections.ranges(cx))
+ cloned_editor
+ .update(cx, |editor, cx| editor.selections.ranges::<Point>(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::<Workspace>::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<NavigationEntry> {
- 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<NavigationEntry> {
+ 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 via the mouse.
- // Nothing is added to the navigation history.
- editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx);
- editor.end_selection(cx);
- assert_eq!(
- editor.selections.display_ranges(cx),
- &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
- );
- assert!(pop_history(&mut editor, cx).is_none());
-
- // Move the cursor a large distance via the mouse.
- // The history can jump back to the previous position.
- editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx);
- editor.end_selection(cx);
- assert_eq!(
- editor.selections.display_ranges(cx),
- &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)]
- );
- let nav_entry = pop_history(&mut editor, cx).unwrap();
- editor.navigate(nav_entry.data.unwrap(), cx);
- assert_eq!(nav_entry.item.id(), cx.view_id());
- assert_eq!(
- editor.selections.display_ranges(cx),
- &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
- );
- assert!(pop_history(&mut editor, cx).is_none());
-
- // Set scroll position to check later
- editor.set_scroll_position(Vector2F::new(5.5, 5.5), cx);
- let original_scroll_position = editor.scroll_manager.anchor();
-
- // Jump to the end of the document and adjust scroll
- editor.move_to_end(&MoveToEnd, cx);
- editor.set_scroll_position(Vector2F::new(-2.5, -0.5), cx);
- assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
-
- let nav_entry = pop_history(&mut editor, cx).unwrap();
- editor.navigate(nav_entry.data.unwrap(), cx);
- assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
-
- // Ensure we don't panic when navigation data contains invalid anchors *and* points.
- let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
- invalid_anchor.text_anchor.buffer_id = Some(999);
- let invalid_point = Point::new(9999, 0);
- editor.navigate(
- Box::new(NavigationData {
- cursor_anchor: invalid_anchor,
- cursor_position: invalid_point,
- scroll_anchor: ScrollAnchor {
- anchor: invalid_anchor,
- offset: Default::default(),
- },
- scroll_top_row: invalid_point.row,
- }),
- cx,
- );
- assert_eq!(
- editor.selections.display_ranges(cx),
- &[editor.max_point(cx)..editor.max_point(cx)]
- );
- assert_eq!(
- editor.scroll_position(cx),
- vec2f(0., editor.max_point(cx).row() as f32)
- );
+ // 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());
- editor
+ // 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 small distance via the mouse.
+ // Nothing is added to the navigation history.
+ editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx);
+ editor.end_selection(cx);
+ assert_eq!(
+ editor.selections.display_ranges(cx),
+ &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
+ );
+ assert!(pop_history(&mut editor, cx).is_none());
+
+ // Move the cursor a large distance via the mouse.
+ // The history can jump back to the previous position.
+ editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx);
+ editor.end_selection(cx);
+ assert_eq!(
+ editor.selections.display_ranges(cx),
+ &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)]
+ );
+ let nav_entry = pop_history(&mut editor, cx).unwrap();
+ editor.navigate(nav_entry.data.unwrap(), cx);
+ assert_eq!(nav_entry.item.id(), cx.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());
+
+ // Set scroll position to check later
+ editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), cx);
+ let original_scroll_position = editor.scroll_manager.anchor();
+
+ // Jump to the end of the document and adjust scroll
+ editor.move_to_end(&MoveToEnd, cx);
+ editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), cx);
+ assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
+
+ let nav_entry = pop_history(&mut editor, cx).unwrap();
+ editor.navigate(nav_entry.data.unwrap(), cx);
+ assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
+
+ // Ensure we don't panic when navigation data contains invalid anchors *and* points.
+ let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
+ invalid_anchor.text_anchor.buffer_id = Some(999);
+ let invalid_point = Point::new(9999, 0);
+ editor.navigate(
+ Box::new(NavigationData {
+ cursor_anchor: invalid_anchor,
+ cursor_position: invalid_point,
+ scroll_anchor: ScrollAnchor {
+ anchor: invalid_anchor,
+ offset: Default::default(),
+ },
+ scroll_top_row: invalid_point.row,
+ }),
+ cx,
+ );
+ assert_eq!(
+ editor.selections.display_ranges(cx),
+ &[editor.max_point(cx)..editor.max_point(cx)]
+ );
+ assert_eq!(
+ editor.scroll_position(cx),
+ 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::<f32>::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::<f32>::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),
@@ -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<EditorStyle>,
+ editor: View<Editor>,
+ style: EditorStyle,
}
impl EditorElement {
- pub fn new(style: EditorStyle) -> Self {
+ pub fn new(editor: &View<Editor>, style: EditorStyle) -> Self {
Self {
- style: Arc::new(style),
+ editor: editor.clone(),
+ style,
}
}
- fn attach_mouse_handlers(
- position_map: &Arc<PositionMap>,
- 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<Editor>,
- ) {
- enum EditorElementMouseHandlers {}
- let view_id = cx.view_id();
- cx.scene().push_mouse_region(
- MouseRegion::new::<EditorElementMouseHandlers>(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;
- }
+ ) -> bool {
+ let pending_selection = editor.has_pending_selection();
- 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()
- }
- }
- }),
- );
+ 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);
- enum GutterHandlers {}
- let view_id = cx.view_id();
- let region_id = cx.view_id() + 1;
- cx.scene().push_mouse_region(
- MouseRegion::new::<GutterHandlers>(view_id, region_id, gutter_bounds).on_hover(
- |hover, editor: &mut Editor, cx| {
- editor.gutter_hover(
- &GutterHover {
- hovered: hover.started,
- },
- cx,
- );
- },
- ),
- )
+ 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::<LinkGoToDefinitionState>(cx);
+ }
+
+ false
}
- fn mouse_down(
+ fn mouse_left_down(
editor: &mut Editor,
- MouseButtonEvent {
- position,
- modifiers:
- Modifiers {
- shift,
- ctrl,
- alt,
- cmd,
- ..
- },
- mut click_count,
- ..
- }: MouseButtonEvent,
+ event: &MouseDownEvent,
position_map: &PositionMap,
- text_bounds: RectF,
- gutter_bounds: RectF,
- cx: &mut EventContext<Editor>,
- ) -> bool {
- if gutter_bounds.contains_point(position) {
+ text_bounds: Bounds<Pixels>,
+ gutter_bounds: Bounds<Pixels>,
+ stacking_order: &StackingOrder,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ 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_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<Editor>,
- ) -> bool {
- if !text_bounds.contains_point(position) {
- return false;
+ text_bounds: Bounds<Pixels>,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ 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<Editor>,
- ) -> bool {
+ text_bounds: Bounds<Pixels>,
+ stacking_order: &StackingOrder,
+ cx: &mut ViewContext<Editor>,
+ ) {
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<Editor>,
- ) -> bool {
- // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed
- // Don't trigger hover popover if mouse is hovering over context menu
- let point = if text_bounds.contains_point(position) {
- position_map
- .point_for_position(text_bounds, position)
- .as_valid()
- } else {
- None
- };
-
- update_go_to_definition_link(
- editor,
- point.map(GoToDefinitionTrigger::Text),
- cmd,
- shift,
- cx,
- );
-
- if editor.has_pending_selection() {
- let mut scroll_delta = Vector2F::zero();
-
- let vertical_margin = position_map.line_height.min(text_bounds.height() / 3.0);
- let top = text_bounds.origin_y() + vertical_margin;
- let bottom = text_bounds.lower_left().y() - vertical_margin;
- if position.y() < top {
- scroll_delta.set_y(-scale_vertical_mouse_autoscroll_delta(top - position.y()))
- }
- if position.y() > bottom {
- scroll_delta.set_y(scale_vertical_mouse_autoscroll_delta(position.y() - bottom))
- }
-
- let horizontal_margin = position_map.line_height.min(text_bounds.width() / 3.0);
- let left = text_bounds.origin_x() + horizontal_margin;
- let right = text_bounds.upper_right().x() - horizontal_margin;
- if position.x() < left {
- scroll_delta.set_x(-scale_horizontal_mouse_autoscroll_delta(
- left - position.x(),
- ))
- }
- if position.x() > right {
- scroll_delta.set_x(scale_horizontal_mouse_autoscroll_delta(
- position.x() - right,
- ))
- }
+ text_bounds: Bounds<Pixels>,
+ _gutter_bounds: Bounds<Pixels>,
+ _stacking_order: &StackingOrder,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ if !editor.has_pending_selection() {
+ 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 mut scroll_delta = gpui::Point::<f32>::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);
+ }
- 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
+ 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,
- MouseMovedEvent {
- modifiers: Modifiers { shift, cmd, .. },
- position,
- ..
- }: MouseMovedEvent,
+ event: &MouseMoveEvent,
position_map: &PositionMap,
- text_bounds: RectF,
+ text_bounds: Bounds<Pixels>,
+ gutter_bounds: Bounds<Pixels>,
+ stacking_order: &StackingOrder,
cx: &mut ViewContext<Editor>,
- ) -> 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<Editor>,
- ) -> 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<Pixels>,
+ text_bounds: Bounds<Pixels>,
layout: &LayoutState,
- cx: &mut ViewContext<Editor>,
+ 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<Pixels>,
layout: &mut LayoutState,
- editor: &mut Editor,
- cx: &mut ViewContext<Editor>,
+ 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::<ProjectSettings>(cx).git.git_gutter,
+ ProjectSettings::get_global(cx).git.git_gutter,
Some(GitGutterSetting::TrackedFiles)
);
@@ -60,8 +60,8 @@ pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, 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<u32>, 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<u32>, 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(),
@@ -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,
)
}
@@ -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<Editor>) {
let head = editor.selections.newest_display(cx).head();
@@ -41,7 +38,7 @@ pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
/// 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<DisplayPoint>, cx: &mut ViewContext<Editor>) {
- if settings::get::<EditorSettings>(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<Editor>) {
- if settings::get::<EditorSettings>(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::<HoverState>(
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::<HoverState>(
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<u32>,
- workspace: Option<WeakViewHandle<Workspace>>,
+ max_size: Size<Pixels>,
+ workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
- ) -> Option<(DisplayPoint, Vec<AnyElement<Editor>>)> {
+ ) -> Option<(DisplayPoint, Vec<AnyElement>)> {
// 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<Project>,
+ pub project: Model<Project>,
symbol_range: RangeInEditor,
pub blocks: Vec<HoverBlock>,
parsed_content: ParsedMarkdown,
@@ -472,29 +471,28 @@ impl InfoPopover {
pub fn render(
&mut self,
style: &EditorStyle,
- workspace: Option<WeakViewHandle<Workspace>>,
+ max_size: Size<Pixels>,
+ workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
- ) -> AnyElement<Editor> {
- MouseEventHandler::new::<InfoPopover, _>(0, cx, |_, cx| {
- Flex::column()
- .scrollable::<HoverBlock>(0, None, cx)
- .with_child(crate::render_parsed_markdown::<HoverBlock>(
- &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<Editor>) -> AnyElement<Editor> {
- 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<Pixels>,
+ cx: &mut ViewContext<Editor>,
+ ) -> 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 tooltip_style = theme::current(cx).tooltip.clone();
+ 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,
+ },
+ };
- MouseEventHandler::new::<DiagnosticPopover, _>(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::<PrimaryDiagnostic>(
- 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::<lsp::request::HoverRequest, _, _>(|_, _| 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<HoverBlock>,
- expected_marked_text: String,
- expected_styles: Vec<HighlightStyle>,
- }
+ struct Row {
+ blocks: Vec<HoverBlock>,
+ expected_marked_text: String,
+ expected_styles: Vec<HighlightStyle>,
+ }
- 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 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 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
- 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));
-
- let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
- let expected_highlights = ranges
- .into_iter()
- .zip(expected_styles.iter().cloned())
- .collect::<Vec<_>>();
- 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:?}"
- );
- }
+ 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::<Vec<_>>();
+ assert_eq!(
+ rendered.text, expected_text,
+ "wrong text for input {blocks:?}"
+ );
- editor
- });
+ 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]
@@ -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());
@@ -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<MultiBuffer>,
+ multi_buffer: &Model<MultiBuffer>,
new_hint_settings: InlayHintSettings,
visible_hints: Vec<Inlay>,
cx: &mut ViewContext<Editor>,
@@ -302,7 +302,7 @@ impl InlayHintCache {
pub fn spawn_hint_refresh(
&mut self,
reason: &'static str,
- excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
+ excerpts_to_query: HashMap<ExcerptId, (Model<Buffer>, Global, Range<usize>)>,
invalidate: InvalidationStrategy,
cx: &mut ViewContext<Editor>,
) -> Option<InlaySplice> {
@@ -355,7 +355,7 @@ impl InlayHintCache {
fn new_allowed_hint_kinds_splice(
&self,
- multi_buffer: &ModelHandle<MultiBuffer>,
+ multi_buffer: &Model<MultiBuffer>,
visible_hints: &[Inlay],
new_kinds: &HashSet<Option<InlayHintKind>>,
cx: &mut ViewContext<Editor>,
@@ -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<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
+ excerpts_to_query: HashMap<ExcerptId, (Model<Buffer>, Global, Range<usize>)>,
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<Buffer>,
+ excerpt_buffer: &Model<Buffer>,
excerpt_visible_range: Range<usize>,
cx: &mut ModelContext<'_, MultiBuffer>,
) -> Option<QueryRanges> {
@@ -760,7 +760,7 @@ fn new_update_task(
visible_hints: Arc<Vec<Inlay>>,
cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
lsp_request_limiter: Arc<Semaphore>,
- 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>,
+ editor: gpui::WeakView<Editor>,
multi_buffer_snapshot: MultiBufferSnapshot,
buffer_snapshot: BufferSnapshot,
visible_hints: Arc<Vec<Inlay>>,
@@ -846,7 +846,7 @@ async fn fetch_and_update_hints(
invalidate: bool,
fetch_range: Range<language::Anchor>,
lsp_request_limiter: Arc<Semaphore>,
- 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::notification::Progress>(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::<Editor>()
- .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::<lsp::request::InlayHintRequest, _, _>(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::<Editor>()
- .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::<lsp::request::InlayHintRequest, _, _>(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::<lsp::request::InlayHintRefreshRequest>(())
.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::<lsp::request::InlayHintRefreshRequest>(())
.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::<lsp::request::InlayHintRefreshRequest>(())
.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::<Editor>()
- .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>,
+ editor: &WindowHandle<Editor>,
cx: &mut gpui::TestAppContext,
) -> Range<Point> {
- 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::<Vec<_>>();
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::<Vec<_>>();
- 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::<Vec<_>>();
+ 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::<Vec<_>>();
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<Deterministic>,
- 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::<Vec<_>>().join("")),
- "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
- }),
- )
- .await;
+ "/a",
+ json!({
+ "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
+ "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().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();
-
- 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");
- });
+ 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.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()
- });
-
- 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.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(),
@@ -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 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::{BreadcrumbText, FollowableItemHandle, ItemHandle};
use workspace::{
- item::{FollowableItem, Item, ItemEvent, ProjectItem},
+ 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::Pane>,
- workspace: ViewHandle<Workspace>,
+ pane: View<workspace::Pane>,
+ workspace: View<Workspace>,
remote_id: ViewId,
state: &mut Option<proto::view::Variant>,
- cx: &mut AppContext,
- ) -> Option<Task<Result<ViewHandle<Self>>>> {
+ cx: &mut WindowContext,
+ ) -> Option<Task<Result<View<Self>>>> {
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::<Self>();
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<proto::view::Variant> {
+ fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
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<workspace::item::FollowEvent> {
+ 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<proto::update_view::Variant>,
- 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>,
+ project: &Model<Project>,
message: update_view::Variant,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
@@ -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<Editor>,
- project: ModelHandle<Project>,
+ this: WeakView<Editor>,
+ project: Model<Project>,
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::<Vec<_>>()
- });
+ })?;
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<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
if let Ok(data) = data.downcast::<NavigationData>() {
let newest_selection = self.selections.newest::<Point>(cx);
@@ -551,7 +558,7 @@ impl Item for Editor {
}
}
- fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
+ fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
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<Cow<str>> {
- 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<SharedString> {
+ let path = path_for_buffer(&self.buffer, detail, true, cx)?;
+ Some(path.to_string_lossy().to_string().into())
}
- fn tab_content<T: 'static>(
- &self,
- detail: Option<usize>,
- style: &theme::Tab,
- cx: &AppContext,
- ) -> AnyElement<T> {
- 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<usize>, 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<Self>) -> Option<Self>
+ fn clone_on_split(
+ &self,
+ _workspace_id: WorkspaceId,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<View<Editor>>
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<Self>) {
@@ -646,11 +666,7 @@ impl Item for Editor {
}
}
- fn save(
- &mut self,
- project: ModelHandle<Project>,
- cx: &mut ViewContext<Self>,
- ) -> Task<Result<()>> {
+ fn save(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
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>,
+ project: Model<Project>,
abs_path: PathBuf,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
@@ -710,11 +732,7 @@ impl Item for Editor {
})
}
- fn reload(
- &mut self,
- project: ModelHandle<Project>,
- cx: &mut ViewContext<Self>,
- ) -> Task<Result<()>> {
+ fn reload(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
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<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+ fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(handle.clone()))
}
- fn pixel_position_of_cursor(&self, _: &AppContext) -> Option<Vector2F> {
+ fn pixel_position_of_cursor(&self, _: &AppContext) -> Option<gpui::Point<Pixels>> {
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<Vec<BreadcrumbText>> {
+ fn breadcrumbs(&self, variant: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
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<Self>) {
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>,
+ buffer: Model<Buffer>,
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<Project>,
- _workspace: WeakViewHandle<Workspace>,
+ project: Model<Project>,
+ _workspace: WeakView<Workspace>,
workspace_id: workspace::WorkspaceId,
item_id: ItemId,
cx: &mut ViewContext<Pane>,
- ) -> Task<Result<ViewHandle<Self>>> {
+ ) -> Task<Result<View<Self>>> {
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::<Buffer>()
- .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<Project>,
- buffer: ModelHandle<Buffer>,
+ project: Model<Project>,
+ buffer: Model<Buffer>,
cx: &mut ViewContext<Self>,
) -> Self {
Self::for_buffer(buffer, Some(project), cx)
}
}
+impl EventEmitter<SearchEvent> for Editor {}
+
pub(crate) enum BufferSearchHighlights {}
impl SearchableItem for Editor {
type Match = Range<Anchor>;
- fn to_search_event(
- &mut self,
- event: &Self::Event,
- _: &mut ViewContext<Self>,
- ) -> Option<SearchEvent> {
- 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>) {
self.clear_background_highlights::<BufferSearchHighlights>(cx);
}
@@ -931,13 +949,13 @@ impl SearchableItem for Editor {
fn update_matches(&mut self, matches: Vec<Range<Anchor>>, cx: &mut ViewContext<Self>) {
self.highlight_background::<BufferSearchHighlights>(
matches,
- |theme| theme.search.match_background,
+ |theme| theme.search_match_background,
cx,
);
}
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
- let setting = settings::get::<EditorSettings>(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::<usize>(cx);
@@ -1060,7 +1078,7 @@ impl SearchableItem for Editor {
cx: &mut ViewContext<Self>,
) -> Task<Vec<Range<Anchor>>> {
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<Editor>, cx: &mut ViewContext<Self>) {
+ fn update_position(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
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<Self>) -> AnyElement<Self> {
- if let Some(position) = self.position {
- let theme = &theme::current(cx).workspace.status_bar;
+impl Render for CursorPosition {
+ fn render(&mut self, _: &mut ViewContext<Self>) -> 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<MultiBuffer>,
+ buffer: &Model<MultiBuffer>,
height: usize,
include_filename: bool,
cx: &'a AppContext,
@@ -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::<LinkGoToDefinitionState>(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::<GotoDefinition, _, _>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<GotoDefinition, _, _>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<GotoDefinition, _, _>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<GotoDefinition, _, _>(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::<GotoDefinition, _, _>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>()
- .map(|ranges| ranges.as_ref().clone().1)
- .unwrap_or_default();
+ let snapshot = editor.snapshot(cx);
+ let actual_ranges = snapshot
+ .text_highlight_ranges::<LinkGoToDefinitionState>()
+ .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;
+ }
+ "});
}
}
@@ -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<Pixels>,
+ pub(crate) context_menu: View<ui::ContextMenu>,
+ _subscription: Subscription,
+}
pub fn deploy_context_menu(
editor: &mut Editor,
- position: Vector2F,
+ position: Point<Pixels>,
point: DisplayPoint,
cx: &mut ViewContext<Editor>,
) {
- 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()));
}
}
@@ -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<FontCache>,
- pub text_layout_cache: Arc<TextLayoutCache>,
+ pub text_system: Arc<TextSystem>,
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);
@@ -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<Editor>, 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<Task<anyhow::Result<()>>> {
+ 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 {
@@ -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<f32>,
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<f32> {
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<Axis> {
+ pub fn filter(&self, delta: &mut gpui::Point<Pixels>) -> Option<Axis> {
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, f32, AutoscrollStrategy)>,
show_scrollbars: bool,
hide_scrollbar_task: Option<Task<()>>,
+ dragging_scrollbar: bool,
visible_line_count: Option<f32>,
}
@@ -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<f32> {
self.anchor.scroll_position(snapshot)
}
fn set_scroll_position(
&mut self,
- scroll_position: Vector2F,
+ scroll_position: gpui::Point<f32>,
map: &DisplaySnapshot,
local: bool,
autoscroll: bool,
workspace_id: Option<i64>,
cx: &mut ViewContext<Editor>,
) {
- 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<Editor>,
) {
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::<ScrollbarAutoHide>().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<Editor>) {
+ 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<Self>) {
+ pub fn set_scroll_position(
+ &mut self,
+ scroll_position: gpui::Point<f32>,
+ cx: &mut ViewContext<Self>,
+ ) {
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<f32>,
local: bool,
autoscroll: bool,
cx: &mut ViewContext<Self>,
@@ -337,7 +361,7 @@ impl Editor {
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
}
- pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
+ pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> gpui::Point<f32> {
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<Self>) {
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<Editor>,
) {
@@ -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);
@@ -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<Editor>) -> Option<()> {
+ pub fn next_screen(&mut self, _: &NextScreen, cx: &mut ViewContext<Editor>) {
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<f32>,
axis: Option<Axis>,
cx: &mut ViewContext<Self>,
) {
@@ -74,17 +33,17 @@ impl Editor {
self.set_scroll_position(scroll_position, cx);
}
- fn scroll_cursor_top(editor: &mut Editor, _: &ScrollCursorTop, cx: &mut ViewContext<Editor>) {
- 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<Editor>) {
+ 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<Editor>,
- ) {
- 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<Editor>) {
+ 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<Editor>,
- ) {
- 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<Editor>) {
+ 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(),
@@ -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<Editor>,
) -> 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<Self>,
) -> 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
@@ -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<DisplayMap>,
- buffer: ModelHandle<MultiBuffer>,
+ display_map: Model<DisplayMap>,
+ buffer: Model<MultiBuffer>,
pub next_selection_id: usize,
pub line_mode: bool,
disjoint: Arc<[Selection<Anchor>]>,
@@ -34,7 +34,7 @@ pub struct SelectionsCollection {
}
impl SelectionsCollection {
- pub fn new(display_map: ModelHandle<DisplayMap>, buffer: ModelHandle<MultiBuffer>) -> Self {
+ pub fn new(display_map: Model<DisplayMap>, buffer: Model<MultiBuffer>) -> Self {
Self {
display_map,
buffer,
@@ -306,19 +306,19 @@ impl SelectionsCollection {
&mut self,
display_map: &DisplaySnapshot,
row: u32,
- positions: &Range<f32>,
+ positions: &Range<Pixels>,
reversed: bool,
text_layout_details: &TextLayoutDetails,
) -> Option<Selection<Point>> {
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<I: IntoIterator<Item = Range<Anchor>>>(&mut self, ranges: I) {
+ pub fn select_anchor_ranges<I>(&mut self, ranges: I)
+ where
+ I: IntoIterator<Item = Range<Anchor>>,
+ {
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
let selections = ranges
.into_iter()
@@ -614,7 +617,6 @@ impl<'a> MutableSelectionsCollection<'a> {
}
})
.collect::<Vec<_>>();
-
self.select_anchors(selections)
}
@@ -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<DisplayPoint>) {
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<MultiBuffer>,
- cx: &mut ViewContext<Editor>,
-) -> Editor {
- Editor::new(EditorMode::Full, buffer, None, None, cx)
+pub(crate) fn build_editor(buffer: Model<MultiBuffer>, cx: &mut ViewContext<Editor>) -> Editor {
+ // todo!()
+ Editor::new(EditorMode::Full, buffer, None, /*None,*/ cx)
}
pub(crate) fn build_editor_with_project(
- project: ModelHandle<Project>,
- buffer: ModelHandle<MultiBuffer>,
+ project: Model<Project>,
+ buffer: Model<MultiBuffer>,
cx: &mut ViewContext<Editor>,
) -> Editor {
- Editor::new(EditorMode::Full, buffer, Some(project), None, cx)
+ // todo!()
+ Editor::new(EditorMode::Full, buffer, Some(project), /*None,*/ cx)
}
@@ -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<Workspace>,
+ pub workspace: View<Workspace>,
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::<Editor>(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<Workspace>) -> T,
{
- self.workspace.update(self.cx.cx, update)
+ self.workspace.update(&mut self.cx.cx, update)
}
pub fn handle_request<T, F, Fut>(
@@ -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<Editor>,
+ pub editor: View<Editor>,
+ 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<Output = ()> {
- self.editor.condition(self.cx, predicate)
+ self.editor
+ .condition::<crate::EditorEvent>(&self.cx, predicate)
}
- pub fn editor<F, T>(&self, read: F) -> T
+ #[track_caller]
+ pub fn editor<F, T>(&mut self, read: F) -> T
where
F: FnOnce(&Editor, &ViewContext<Editor>) -> 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<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
{
- self.editor.update(self.cx, update)
+ self.editor.update(&mut self.cx, update)
}
- pub fn multibuffer<F, T>(&self, read: F) -> T
+ pub fn multibuffer<F, T>(&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<F, T>(&self, read: F) -> T
+ pub fn buffer<F, T>(&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<Range<usize>> {
+ pub fn run_until_parked(&mut self) {
+ self.cx.background_executor.run_until_parked();
+ }
+
+ pub fn ranges(&mut self, marked_text: &str) -> Vec<Range<usize>> {
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<language::Anchor> {
+ pub fn text_anchor_range(&mut self, marked_text: &str) -> Range<language::Anchor> {
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<Range<usize>> {
+ #[track_caller]
+ fn editor_selections(&mut self) -> Vec<Range<usize>> {
self.editor
- .read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
+ .update(&mut self.cx, |editor, cx| {
+ editor.selections.all::<usize>(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<AtomicUsize>,
+ contexts: Arc<RwLock<BTreeMap<usize, String>>>,
+}
+
+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);
+ }
+}
@@ -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
@@ -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>) -> Self {
- // Make sure we blink the cursors if the setting is re-enabled
- cx.observe_global::<SettingsStore>(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>) {
- 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<Self>) {
- if epoch == self.blink_epoch {
- self.blinking_paused = false;
- self.blink_cursors(epoch, cx);
- }
- }
-
- fn blink_cursors(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {
- 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<Self>) {
- 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>) {
- self.enabled = false;
- }
-
- pub fn visible(&self) -> bool {
- self.visible
- }
-}
@@ -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<Option<TypeId>, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>;
-type InlayHighlights = BTreeMap<TypeId, HashMap<InlayId, (HighlightStyle, InlayHighlight)>>;
-
-pub struct DisplayMap {
- buffer: Model<MultiBuffer>,
- buffer_subscription: BufferSubscription,
- fold_map: FoldMap,
- inlay_map: InlayMap,
- tab_map: TabMap,
- wrap_map: Model<WrapMap>,
- block_map: BlockMap,
- text_highlights: TextHighlights,
- inlay_highlights: InlayHighlights,
- pub clip_at_line_ends: bool,
-}
-
-impl DisplayMap {
- pub fn new(
- buffer: Model<MultiBuffer>,
- font: Font,
- font_size: Pixels,
- wrap_width: Option<Pixels>,
- buffer_header_height: u8,
- excerpt_header_height: u8,
- cx: &mut ModelContext<Self>,
- ) -> 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<Self>) -> 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>) {
- self.fold(
- other
- .folds_in_range(0..other.buffer_snapshot.len())
- .map(|fold| fold.range.to_offset(&other.buffer_snapshot)),
- cx,
- );
- }
-
- pub fn fold<T: ToOffset>(
- &mut self,
- ranges: impl IntoIterator<Item = Range<T>>,
- cx: &mut ModelContext<Self>,
- ) {
- 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<T: ToOffset>(
- &mut self,
- ranges: impl IntoIterator<Item = Range<T>>,
- inclusive: bool,
- cx: &mut ModelContext<Self>,
- ) {
- 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<Item = BlockProperties<Anchor>>,
- cx: &mut ModelContext<Self>,
- ) -> Vec<BlockId> {
- 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<BlockId, RenderBlock>) {
- self.block_map.replace(styles);
- }
-
- pub fn remove_blocks(&mut self, ids: HashSet<BlockId>, cx: &mut ModelContext<Self>) {
- 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<Range<Anchor>>,
- style: HighlightStyle,
- ) {
- self.text_highlights
- .insert(Some(type_id), Arc::new((style, ranges)));
- }
-
- pub fn highlight_inlays(
- &mut self,
- type_id: TypeId,
- highlights: Vec<InlayHighlight>,
- 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<Anchor>])> {
- 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<Self>) -> 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<Pixels>, cx: &mut ModelContext<Self>) -> bool {
- self.wrap_map
- .update(cx, |map, cx| map.set_wrap_width(width, cx))
- }
-
- pub fn current_inlays(&self) -> impl Iterator<Item = &Inlay> {
- self.inlay_map.current_inlays()
- }
-
- pub fn splice_inlays(
- &mut self,
- to_remove: Vec<InlayId>,
- to_insert: Vec<Inlay>,
- cx: &mut ModelContext<Self>,
- ) {
- 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<MultiBuffer>, cx: &mut ModelContext<Self>) -> 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<HighlightStyle>,
- pub suggestion_highlight_style: Option<HighlightStyle>,
-}
-
-pub struct HighlightedChunk<'a> {
- pub chunk: &'a str,
- pub style: Option<HighlightStyle>,
- 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<Point>) -> Range<Point> {
- 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<Item = &str> {
- 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<Item = &str> {
- (0..=display_row).into_iter().rev().flat_map(|row| {
- self.block_snapshot
- .chunks(row..row + 1, false, Highlights::default())
- .map(|h| h.text)
- .collect::<Vec<_>>()
- .into_iter()
- .rev()
- })
- }
-
- pub fn chunks<'a>(
- &'a self,
- display_rows: Range<u32>,
- language_aware: bool,
- inlay_highlight_style: Option<HighlightStyle>,
- suggestion_highlight_style: Option<HighlightStyle>,
- ) -> 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<u32>,
- language_aware: bool,
- editor_style: &'a EditorStyle,
- ) -> impl Iterator<Item = HighlightedChunk<'a>> {
- 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<LineLayout> {
- 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<Item = (char, DisplayPoint)> + '_ {
- 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<Item = (char, DisplayPoint)> + '_ {
- 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<T>(&self, range: Range<T>) -> impl Iterator<Item = &Fold>
- where
- T: ToOffset,
- {
- self.fold_snapshot.folds_in_range(range)
- }
-
- pub fn blocks_in_range(
- &self,
- rows: Range<u32>,
- ) -> impl Iterator<Item = (u32, &TransformBlock)> {
- self.block_snapshot.blocks_in_range(rows)
- }
-
- pub fn intersects_fold<T: ToOffset>(&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<u32> {
- 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<FoldStatus> {
- 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<Range<Point>> {
- 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<Tag: ?Sized + 'static>(
- &self,
- ) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
- let type_id = TypeId::of::<Tag>();
- self.text_highlights.get(&Some(type_id)).cloned()
- }
-
- #[cfg(any(test, feature = "test-support"))]
- pub fn inlay_highlights<Tag: ?Sized + 'static>(
- &self,
- ) -> Option<&HashMap<InlayId, (HighlightStyle, InlayHighlight)>> {
- let type_id = TypeId::of::<Tag>();
- 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<Item = u32> {
- 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::<String>();
- 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::<SettingsStore, _>(|store, cx| {
- store.update_user_settings::<AllLanguageSettings>(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::<Vec<_>>();
- 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::<String>(),
- "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::<String>(),
- "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::<String>(),
- "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::<String>()
- .lines()
- .next(),
- Some(" b bbbbb")
- );
- assert_eq!(
- map.update(cx, |map, cx| map.snapshot(cx))
- .text_chunks(2)
- .collect::<String>()
- .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::<MyType>(),
- 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::<String>(),
- "โ
ฮฑ\nฮฒ \n๐ฮฒ ฮณ"
- );
- assert_eq!(map.text_chunks(1).collect::<String>(), "ฮฒ \n๐ฮฒ ฮณ");
- assert_eq!(map.text_chunks(2).collect::<String>(), "๐ฮฒ ฮณ");
-
- 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<u32>,
- map: &Model<DisplayMap>,
- theme: &'a SyntaxTheme,
- cx: &mut AppContext,
- ) -> Vec<(String, Option<Hsla>)> {
- chunks(rows, map, theme, cx)
- .into_iter()
- .map(|(text, color, _)| (text, color))
- .collect()
- }
-
- fn chunks<'a>(
- rows: Range<u32>,
- map: &Model<DisplayMap>,
- theme: &'a SyntaxTheme,
- cx: &mut AppContext,
- ) -> Vec<(String, Option<Hsla>, Option<Hsla>)> {
- let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
- let mut chunks: Vec<(String, Option<Hsla>, Option<Hsla>)> = 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::<SettingsStore, _>(|store, cx| {
- store.update_user_settings::<AllLanguageSettings>(cx, f);
- });
- }
-}
@@ -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<WrapSnapshot>,
- blocks: Vec<Arc<Block>>,
- transforms: RefCell<SumTree<Transform>>,
- buffer_header_height: u8,
- excerpt_header_height: u8,
-}
-
-pub struct BlockMapWriter<'a>(&'a mut BlockMap);
-
-pub struct BlockSnapshot {
- wrap_snapshot: WrapSnapshot,
- transforms: SumTree<Transform>,
-}
-
-#[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<dyn Fn(&mut BlockContext) -> AnyElement>;
-
-pub struct Block {
- id: BlockId,
- position: Anchor,
- height: u8,
- style: BlockStyle,
- render: Mutex<RenderBlock>,
- disposition: BlockDisposition,
-}
-
-#[derive(Clone)]
-pub struct BlockProperties<P>
-where
- P: Clone,
-{
- pub position: P,
- pub height: u8,
- pub style: BlockStyle,
- pub render: Arc<dyn Fn(&mut BlockContext) -> 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<TransformBlock>,
-}
-
-#[allow(clippy::large_enum_variant)]
-#[derive(Clone)]
-pub enum TransformBlock {
- Custom(Arc<Block>),
- ExcerptHeader {
- id: ExcerptId,
- buffer: BufferSnapshot,
- range: ExcerptRange<text::Anchor>,
- 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<u32>) -> 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<u32>) -> BlockMapWriter {
- self.sync(&wrap_snapshot, edits);
- *self.wrap_snapshot.borrow_mut() = wrap_snapshot;
- BlockMapWriter(self)
- }
-
- fn sync(&self, wrap_snapshot: &WrapSnapshot, mut edits: Patch<u32>) {
- 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::<WrapRow>();
- 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<BlockId, RenderBlock>) {
- for block in &self.blocks {
- if let Some(render) = renderers.remove(&block.id) {
- *block.render.lock() = render;
- }
- }
- }
-}
-
-fn push_isomorphic(tree: &mut SumTree<Transform>, 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<Item = BlockProperties<Anchor>>,
- ) -> Vec<BlockId> {
- 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<BlockId>) {
- 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<u32>,
- 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<u32>,
- ) -> impl Iterator<Item = (u32, &TransformBlock)> {
- let mut cursor = self.transforms.cursor::<BlockRow>();
- 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<Self::Item> {
- 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<u32>;
-
- fn next(&mut self) -> Option<Self::Item> {
- 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::<Vec<_>>();
-
- // 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::<Vec<_>>(),
- &[
- 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::<String>();
- 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::<Vec<_>>();
-
- 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::<Vec<_>>();
- 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::<Vec<_>>();
- 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::<String>();
- assert_eq!(
- actual_text, expected_text,
- "incorrect text starting from row {}",
- start_row
- );
- assert_eq!(
- blocks_snapshot
- .buffer_rows(start_row as u32)
- .collect::<Vec<_>>(),
- &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::<Vec<_>>(),
- 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<TransformBlock> 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)
- }
- }
-}
@@ -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<T: ToOffset>(
- &mut self,
- ranges: impl IntoIterator<Item = Range<T>>,
- ) -> (FoldSnapshot, Vec<FoldEdit>) {
- 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::<FoldRange>();
- 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<T: ToOffset>(
- &mut self,
- ranges: impl IntoIterator<Item = Range<T>>,
- inclusive: bool,
- ) -> (FoldSnapshot, Vec<FoldEdit>) {
- 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::<usize>();
- 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<Hsla>,
- 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<InlayEdit>,
- ) -> (FoldSnapshot, Vec<FoldEdit>) {
- let edits = self.sync(inlay_snapshot, edits);
- self.check_invariants();
- (self.snapshot.clone(), edits)
- }
-
- pub fn write(
- &mut self,
- inlay_snapshot: InlaySnapshot,
- edits: Vec<InlayEdit>,
- ) -> (FoldMapWriter, FoldSnapshot, Vec<FoldEdit>) {
- 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<InlayEdit>,
- ) -> Vec<FoldEdit> {
- 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::<InlayOffset>();
- 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::<FoldRange>();
- 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<Transform>,
- folds: SumTree<Fold>,
- pub inlay_snapshot: InlaySnapshot,
- pub version: usize,
- pub ellipses_color: Option<Hsla>,
-}
-
-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<FoldPoint>) -> 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<T>(&self, range: Range<T>) -> impl Iterator<Item = &Fold>
- 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<T>(&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::<InlayOffset>();
- 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::<InlayPoint>();
- 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<FoldOffset>,
- 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<Item = char> {
- 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<Fold>,
- range: Range<T>,
- 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<InlayEdit>) {
- 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<FoldEdit>) {
- 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<ElementId> 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<Anchor>);
-
-impl Deref for FoldRange {
- type Target = Range<Anchor>;
-
- 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<u32>;
-
- fn next(&mut self) -> Option<Self::Item> {
- 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<Hsla>,
-}
-
-impl<'a> Iterator for FoldChunks<'a> {
- type Item = Chunk<'a>;
-
- fn next(&mut self) -> Option<Self::Item> {
- 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<TypeId>,
- style: HighlightStyle,
-}
-
-impl PartialOrd for HighlightEndpoint {
- fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
- 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<FoldOffset>;
-
-#[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::<Vec<_>>();
- 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::<String>();
- 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::<String>(),
- text,
- );
- }
-
- let mut fold_row = 0;
- while fold_row < expected_buffer_rows.len() as u32 {
- assert_eq!(
- snapshot.buffer_rows(fold_row).collect::<Vec<_>>(),
- 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::<HashSet<_>>();
- 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::<Vec<_>>();
-
- assert_eq!(
- snapshot
- .folds_in_range(start..end)
- .cloned()
- .collect::<Vec<_>>(),
- 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::<Vec<_>>(),
- [Some(0), Some(3), Some(5), Some(6)]
- );
- assert_eq!(snapshot.buffer_rows(3).collect::<Vec<_>>(), [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<Range<usize>> {
- 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<FoldEdit>)> {
- 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
- }
- }
-}
@@ -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<Inlay>,
-}
-
-#[derive(Clone)]
-pub struct InlaySnapshot {
- pub buffer: MultiBufferSnapshot,
- transforms: SumTree<Transform>,
- 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<T: Into<Rope>>(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<InlayOffset>;
-
-#[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<TypeId>,
- style: HighlightStyle,
-}
-
-impl PartialOrd for HighlightEndpoint {
- fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
- 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<Chunk<'a>>,
- inlay_chunks: Option<text::Chunks<'a>>,
- inlay_chunk: Option<&'a str>,
- output_offset: InlayOffset,
- max_output_offset: InlayOffset,
- inlay_highlight_style: Option<HighlightStyle>,
- suggestion_highlight_style: Option<HighlightStyle>,
- highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
- active_highlights: BTreeMap<Option<TypeId>, 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<Self::Item> {
- 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<u32>;
-
- fn next(&mut self) -> Option<Self::Item> {
- 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<text::Edit<usize>>,
- ) -> (InlaySnapshot, Vec<InlayEdit>) {
- 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<InlayId>,
- to_insert: Vec<Inlay>,
- ) -> (InlaySnapshot, Vec<InlayEdit>) {
- 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<Item = &Inlay> {
- self.inlays.iter()
- }
-
- #[cfg(test)]
- pub(crate) fn randomly_mutate(
- &mut self,
- next_inlay_id: &mut usize,
- rng: &mut rand::rngs::StdRng,
- ) -> (InlaySnapshot, Vec<InlayEdit>) {
- 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::<String>();
-
- 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<InlayOffset>) -> 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::<TextSummary, _>(prefix_start..prefix_end);
- }
- Some(Transform::Inlay(inlay)) => {
- let prefix_end = overshoot;
- summary += inlay.text.cursor(0).summary::<TextSummary>(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<InlayOffset>,
- 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<InlayOffset>,
- text_highlights: &TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>,
- highlight_endpoints: &mut Vec<HighlightEndpoint>,
- ) {
- 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<Transform>, 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<_>>(),
- 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::<String>();
- 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::<Vec<_>>();
- 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::<Vec<_>>();
- 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::<Vec<_>>(),
- &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::<Vec<_>>();
- 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::<String>();
- 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);
- }
-}
@@ -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<FoldEdit>,
- tab_size: NonZeroU32,
- ) -> (TabSnapshot, Vec<TabEdit>) {
- 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<TabPoint>) -> 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<TabPoint>,
- 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<Item = char>, 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<Item = char>,
- 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<Point> for TabPoint {
- fn from(point: Point) -> Self {
- Self(point)
- }
-}
-
-pub type TabEdit = text::Edit<TabPoint>;
-
-#[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<Self::Item> {
- 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::<String>(),
- &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::<String>();
- 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::<String>();
- let expected_summary = TextSummary::from(expected_text.as_str());
- assert_eq!(
- tabs_snapshot
- .chunks(start..end, false, Highlights::default())
- .map(|c| c.text)
- .collect::<String>(),
- 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})"
- );
- }
- }
-}
@@ -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<u32>;
-
-pub struct WrapMap {
- snapshot: WrapSnapshot,
- pending_edits: VecDeque<(TabSnapshot, Vec<TabEdit>)>,
- interpolated_edits: Patch<u32>,
- edits_since_sync: Patch<u32>,
- wrap_width: Option<Pixels>,
- background_task: Option<Task<()>>,
- font_with_size: (Font, Pixels),
-}
-
-#[derive(Clone)]
-pub struct WrapSnapshot {
- tab_snapshot: TabSnapshot,
- transforms: SumTree<Transform>,
- 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<u32>,
- 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<Pixels>,
- cx: &mut AppContext,
- ) -> (Model<Self>, 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<TabEdit>,
- cx: &mut ModelContext<Self>,
- ) -> (WrapSnapshot, Patch<u32>) {
- 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<Self>,
- ) -> 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<Pixels>,
- cx: &mut ModelContext<Self>,
- ) -> 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>) {
- 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<Self>) {
- 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<u32> {
- let mut new_transforms;
- if tab_edits.is_empty() {
- new_transforms = self.transforms.clone();
- } else {
- let mut old_cursor = self.transforms.cursor::<TabPoint>();
-
- 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<u32> {
- #[derive(Debug)]
- struct RowEdit {
- old_rows: Range<u32>,
- new_rows: Range<u32>,
- }
-
- 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::<TabPoint>();
-
- 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::<Transform>::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<u32> {
- let mut wrap_edits = Vec::new();
- let mut old_cursor = self.transforms.cursor::<TransformSummary>();
- let mut new_cursor = new_snapshot.transforms.cursor::<TransformSummary>();
- 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<u32>,
- 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<u32> {
- let mut cursor = self.transforms.cursor::<WrapPoint>();
- 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::<WrapPoint>();
- 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<u32> {
- 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::<Vec<_>>(),
- &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<Self::Item> {
- 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<u32>;
-
- fn next(&mut self) -> Option<Self::Item> {
- 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<Transform>, 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<Transform> {
- 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<WrapEdit>) {
- 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::<String>();
- 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::<String>();
-
- 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<Pixels>,
- 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<Item = &str> {
- 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::<String>();
- 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::<Vec<_>>()
- .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::<String>();
- assert_eq!(
- expected_text,
- actual_text,
- "chunks != highlighted_chunks for rows {:?}",
- start_row..end_row
- );
- }
- }
- }
-}
@@ -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<ElementId>,
- parsed: &language::ParsedMarkdown,
- editor_style: &EditorStyle,
- workspace: Option<WeakView<Workspace>>,
- cx: &mut ViewContext<Editor>,
-) -> 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<usize>,
-}
-
-#[derive(PartialEq, Clone, Deserialize, Default)]
-pub struct ConfirmCodeAction {
- #[serde(default)]
- pub item_ix: Option<usize>,
-}
-
-#[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::<Editor>(cx);
- workspace::register_followable_item::<Editor>(cx);
- workspace::register_deserializable_item::<Editor>(cx);
- cx.observe_new_views(
- |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
- 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::<Weak<workspace::AppState>>();
- 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::<Weak<workspace::AppState>>();
- 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<Anchor>];
-}
-
-#[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<f32>,
- },
- End,
-}
-
-#[derive(Clone, Debug)]
-pub enum SelectMode {
- Character,
- Word(Range<Anchor>),
- Line(Range<Anchor>),
- 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<SyntaxTheme>,
- 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<HighlightStyle>;
-
-type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Vec<Range<Anchor>>);
-type InlayBackgroundHighlight = (fn(&ThemeColors) -> Hsla, Vec<InlayHighlight>);
-
-pub struct Editor {
- handle: WeakView<Self>,
- focus_handle: FocusHandle,
- buffer: Model<MultiBuffer>,
- display_map: Model<DisplayMap>,
- pub selections: SelectionsCollection,
- pub scroll_manager: ScrollManager,
- columnar_selection_tail: Option<Anchor>,
- add_selections_state: Option<AddSelectionsState>,
- select_next_state: Option<SelectNextState>,
- select_prev_state: Option<SelectNextState>,
- selection_history: SelectionHistory,
- autoclose_regions: Vec<AutocloseRegion>,
- snippet_stack: InvalidationStack<SnippetState>,
- select_larger_syntax_node_stack: Vec<Box<[Selection<usize>]>>,
- ime_transaction: Option<TransactionId>,
- active_diagnostics: Option<ActiveDiagnosticGroup>,
- soft_wrap_mode_override: Option<language_settings::SoftWrap>,
- project: Option<Model<Project>>,
- collaboration_hub: Option<Box<dyn CollaborationHub>>,
- blink_manager: Model<BlinkManager>,
- pub show_local_selections: bool,
- mode: EditorMode,
- show_gutter: bool,
- show_wrap_guides: Option<bool>,
- placeholder_text: Option<Arc<str>>,
- highlighted_rows: Option<Range<u32>>,
- background_highlights: BTreeMap<TypeId, BackgroundHighlight>,
- inlay_background_highlights: TreeMap<Option<TypeId>, InlayBackgroundHighlight>,
- nav_history: Option<ItemNavHistory>,
- context_menu: RwLock<Option<ContextMenu>>,
- mouse_context_menu: Option<MouseContextMenu>,
- completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
- next_completion_id: CompletionId,
- available_code_actions: Option<(Model<Buffer>, Arc<[CodeAction]>)>,
- code_actions_task: Option<Task<()>>,
- document_highlights_task: Option<Task<()>>,
- pending_rename: Option<RenameState>,
- searchable: bool,
- cursor_shape: CursorShape,
- collapse_matches: bool,
- autoindent_mode: Option<AutoindentMode>,
- workspace: Option<(WeakView<Workspace>, i64)>,
- keymap_context_layers: BTreeMap<TypeId, KeyContext>,
- input_enabled: bool,
- read_only: bool,
- leader_peer_id: Option<PeerId>,
- remote_id: Option<ViewId>,
- 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<Subscription>,
- pixel_position_of_newest_cursor: Option<gpui::Point<Pixels>>,
- gutter_width: Pixels,
- style: Option<EditorStyle>,
- editor_actions: Vec<Box<dyn Fn(&mut ViewContext<Self>)>>,
-}
-
-pub struct EditorSnapshot {
- pub mode: EditorMode,
- pub show_gutter: bool,
- pub display_snapshot: DisplaySnapshot,
- pub placeholder_text: Option<Arc<str>>,
- is_focused: bool,
- scroll_anchor: ScrollAnchor,
- ongoing_scroll: OngoingScroll,
-}
-
-pub struct RemoteSelection {
- pub replica_id: ReplicaId,
- pub selection: Selection<Anchor>,
- pub cursor_shape: CursorShape,
- pub peer_id: PeerId,
- pub line_mode: bool,
- pub participant_index: Option<ParticipantIndex>,
-}
-
-#[derive(Clone, Debug)]
-struct SelectionHistoryEntry {
- selections: Arc<[Selection<Anchor>]>,
- select_next_state: Option<SelectNextState>,
- select_prev_state: Option<SelectNextState>,
- add_selections_state: Option<AddSelectionsState>,
-}
-
-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<TransactionId, (Arc<[Selection<Anchor>]>, Option<Arc<[Selection<Anchor>]>>)>,
- mode: SelectionHistoryMode,
- undo_stack: VecDeque<SelectionHistoryEntry>,
- redo_stack: VecDeque<SelectionHistoryEntry>,
-}
-
-impl SelectionHistory {
- fn insert_transaction(
- &mut self,
- transaction_id: TransactionId,
- selections: Arc<[Selection<Anchor>]>,
- ) {
- self.selections_by_transaction
- .insert(transaction_id, (selections, None));
- }
-
- #[allow(clippy::type_complexity)]
- fn transaction(
- &self,
- transaction_id: TransactionId,
- ) -> Option<&(Arc<[Selection<Anchor>]>, Option<Arc<[Selection<Anchor>]>>)> {
- self.selections_by_transaction.get(&transaction_id)
- }
-
- #[allow(clippy::type_complexity)]
- fn transaction_mut(
- &mut self,
- transaction_id: TransactionId,
- ) -> Option<&mut (Arc<[Selection<Anchor>]>, Option<Arc<[Selection<Anchor>]>>)> {
- 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<usize>,
-}
-
-#[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::<Self>())
- .field("wordwise", &self.wordwise)
- .field("done", &self.done)
- .finish()
- }
-}
-
-#[derive(Debug)]
-struct AutocloseRegion {
- selection_id: usize,
- range: Range<Anchor>,
- pair: BracketPair,
-}
-
-#[derive(Debug)]
-struct SnippetState {
- ranges: Vec<Vec<Range<Anchor>>>,
- active_index: usize,
-}
-
-pub struct RenameState {
- pub range: Range<Anchor>,
- pub old_name: Arc<str>,
- pub editor: View<Editor>,
- block_id: BlockId,
-}
-
-struct InvalidationStack<T>(Vec<T>);
-
-enum ContextMenu {
- Completions(CompletionsMenu),
- CodeActions(CodeActionsMenu),
-}
-
-impl ContextMenu {
- fn select_first(
- &mut self,
- project: Option<&Model<Project>>,
- cx: &mut ViewContext<Editor>,
- ) -> 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<Project>>,
- cx: &mut ViewContext<Editor>,
- ) -> 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<Project>>,
- cx: &mut ViewContext<Editor>,
- ) -> 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<Project>>,
- cx: &mut ViewContext<Editor>,
- ) -> 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<WeakView<Workspace>>,
- cx: &mut ViewContext<Editor>,
- ) -> (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<Buffer>,
- completions: Arc<RwLock<Box<[Completion]>>>,
- match_candidates: Arc<[StringMatchCandidate]>,
- matches: Arc<[StringMatch]>,
- selected_item: usize,
- scroll_handle: UniformListScrollHandle,
-}
-
-impl CompletionsMenu {
- fn select_first(&mut self, project: Option<&Model<Project>>, cx: &mut ViewContext<Editor>) {
- 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<Project>>, cx: &mut ViewContext<Editor>) {
- 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<Project>>, cx: &mut ViewContext<Editor>) {
- 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<Project>>, cx: &mut ViewContext<Editor>) {
- 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<Editor>,
- ) -> Option<Task<()>> {
- 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<Project>>,
- cx: &mut ViewContext<Editor>,
- ) {
- 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<RwLock<Box<[Completion]>>>,
- completion_index: usize,
- completion: lsp::CompletionItem,
- client: Arc<Client>,
- language_registry: Arc<LanguageRegistry>,
- ) {
- 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<lsp::LanguageServer>,
- completions: Arc<RwLock<Box<[Completion]>>>,
- completion_index: usize,
- completion: lsp::CompletionItem,
- language_registry: Arc<LanguageRegistry>,
- ) {
- 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::<lsp::request::ResolveCompletionItem>(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<WeakView<Workspace>>,
- cx: &mut ViewContext<Editor>,
- ) -> 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::<Div>(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<Buffer>,
- selected_item: usize,
- scroll_handle: UniformListScrollHandle,
- deployed_from_indicator: bool,
-}
-
-impl CodeActionsMenu {
- fn select_first(&mut self, cx: &mut ViewContext<Editor>) {
- self.selected_item = 0;
- self.scroll_handle.scroll_to_item(self.selected_item);
- cx.notify()
- }
-
- fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
- 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<Editor>) {
- 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<Editor>) {
- 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<Editor>,
- ) -> (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<ExcerptId>,
- pending_refresh: Task<Option<()>>,
- pending_cycling_refresh: Task<Option<()>>,
- cycled: bool,
- completions: Vec<copilot::Completion>,
- active_completion_index: usize,
- suggestion: Option<Inlay>,
-}
-
-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<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(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<Anchor>,
- primary_message: String,
- blocks: HashMap<BlockId, Diagnostic>,
- 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<Editor>);
-
-enum GotoDefinitionKind {
- Symbol,
- Type,
-}
-
-#[derive(Debug, Clone)]
-enum InlayHintRefreshReason {
- Toggle(bool),
- SettingsChange(InlayHintSettings),
- NewLinesShown,
- BufferEdited(HashSet<Arc<Language>>),
- RefreshRequested,
- ExcerptsRemoved(Vec<ExcerptId>),
-}
-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>) -> 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>) -> 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>) -> 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<Buffer>,
- project: Option<Model<Project>>,
- cx: &mut ViewContext<Self>,
- ) -> Self {
- let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
- Self::new(EditorMode::Full, buffer, project, cx)
- }
-
- pub fn for_multibuffer(
- buffer: Model<MultiBuffer>,
- project: Option<Model<Project>>,
- cx: &mut ViewContext<Self>,
- ) -> Self {
- Self::new(EditorMode::Full, buffer, project, cx)
- }
-
- pub fn clone(&self, cx: &mut ViewContext<Self>) -> 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<MultiBuffer>,
- project: Option<Model<Project>>,
- cx: &mut ViewContext<Self>,
- ) -> 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::<SettingsStore>(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<Workspace>,
- ) {
- 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<Workspace>,
- ) {
- 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<PeerId> {
- self.leader_peer_id
- }
-
- pub fn buffer(&self) -> &Model<MultiBuffer> {
- &self.buffer
- }
-
- pub fn workspace(&self) -> Option<View<Workspace>> {
- self.workspace.as_ref()?.0.upgrade()
- }
-
- pub fn pane(&self, cx: &AppContext) -> Option<View<Pane>> {
- 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<Arc<Language>> {
- // self.buffer.read(cx).language_at(point, cx)
- // }
-
- // pub fn file_at<'a, T: ToOffset>(&self, point: T, cx: &'a AppContext) -> Option<Arc<dyn File>> {
- // self.buffer.read(cx).read(cx).file_at(point).cloned()
- // }
-
- pub fn active_excerpt(
- &self,
- cx: &AppContext,
- ) -> Option<(ExcerptId, Model<Buffer>, Range<text::Anchor>)> {
- self.buffer
- .read(cx)
- .excerpt_containing(self.selections.newest_anchor().head(), cx)
- }
-
- // pub fn style(&self, cx: &AppContext) -> EditorStyle {
- // build_style(
- // settings::get::<ThemeSettings>(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<dyn CollaborationHub>) {
- 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<Arc<str>>,
- cx: &mut ViewContext<Self>,
- ) {
- 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>) {
- 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<T: std::marker::Copy>(&self, range: &Range<T>) -> Range<T> {
- 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<Self>) {
- 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<Tag: 'static>(
- &mut self,
- context: KeyContext,
- cx: &mut ViewContext<Self>,
- ) {
- self.keymap_context_layers
- .insert(TypeId::of::<Tag>(), context);
- cx.notify();
- }
-
- pub fn remove_keymap_context_layer<Tag: 'static>(&mut self, cx: &mut ViewContext<Self>) {
- self.keymap_context_layers.remove(&TypeId::of::<Tag>());
- 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<Self>,
- ) {
- 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<R>(
- &mut self,
- autoscroll: Option<Autoscroll>,
- cx: &mut ViewContext<Self>,
- 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<I, S, T>(&mut self, edits: I, cx: &mut ViewContext<Self>)
- where
- I: IntoIterator<Item = (Range<S>, T)>,
- S: ToOffset,
- T: Into<Arc<str>>,
- {
- if self.read_only {
- return;
- }
-
- self.buffer
- .update(cx, |buffer, cx| buffer.edit(edits, None, cx));
- }
-
- pub fn edit_with_autoindent<I, S, T>(&mut self, edits: I, cx: &mut ViewContext<Self>)
- where
- I: IntoIterator<Item = (Range<S>, T)>,
- S: ToOffset,
- T: Into<Arc<str>>,
- {
- 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<I, S, T>(
- &mut self,
- edits: I,
- original_indent_columns: Vec<u32>,
- cx: &mut ViewContext<Self>,
- ) where
- I: IntoIterator<Item = (Range<S>, T)>,
- S: ToOffset,
- T: Into<Arc<str>>,
- {
- 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>) {
- 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<Self>,
- ) {
- let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
- let tail = self.selections.newest::<usize>(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<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));
- 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<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));
- let tail = self.selections.newest::<Point>(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<f32>,
- cx: &mut ViewContext<Self>,
- ) {
- 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>) {
- self.columnar_selection_tail.take();
- if self.selections.pending_anchor().is_some() {
- let selections = self.selections.all::<usize>(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<Self>,
- ) {
- 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::<Vec<_>>();
-
- 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<Self>) {
- 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<Self>) {
- let text: Arc<str> = 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::<usize, _>(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::<Vec<_>>();
-
- 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>) {
- self.transact(cx, |this, cx| {
- let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = {
- let selections = this.selections.all::<usize>(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::<usize>();
-
- let trailing_whitespace_len = buffer
- .chars_at(end)
- .take_while(|c| c.is_whitespace() && *c != '\n')
- .map(|c| c.len_utf8())
- .sum::<usize>();
-
- 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<Self>) {
- 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<Self>) {
- 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>) {
- 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<AutoindentMode>,
- cx: &mut ViewContext<Self>,
- ) {
- if self.read_only {
- return;
- }
-
- let text: Arc<str> = 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::<Vec<_>>()
- };
- 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<Self>) {
- 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<Self>) {
- let selections = self.selections.all::<usize>(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<Item = Selection<D>>,
- buffer: &'a MultiBufferSnapshot,
- ) -> impl Iterator<Item = (Selection<D>, 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<Anchor>],
- 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<String> {
- 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::<String>(),
- )
- } else {
- None
- }
- }
-
- pub fn toggle_inlay_hints(&mut self, _: &ToggleInlayHints, cx: &mut ViewContext<Self>) {
- 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<Self>) {
- 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<Inlay> {
- 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<Arc<Language>>>,
- cx: &mut ViewContext<Editor>,
- ) -> HashMap<ExcerptId, (Model<Buffer>, clock::Global, Range<usize>)> {
- 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<InlayId>,
- to_insert: Vec<Inlay>,
- cx: &mut ViewContext<Self>,
- ) {
- 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<Self>,
- ) -> Option<Task<Result<()>>> {
- 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<Self>) {
- 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<Self>,
- ) -> Option<Task<Result<()>>> {
- 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::<usize>(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::<String>();
-
- 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<Range<isize>> = 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<Self>) {
- 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<Self>,
- ) -> Option<Task<Result<()>>> {
- 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<Editor>,
- workspace: WeakView<Workspace>,
- 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::<Vec<_>>();
- 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::<usize>(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::<usize>(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::<Self>(
- ranges_to_highlight,
- |theme| theme.editor_highlighted_line_background,
- cx,
- );
- });
- })?;
-
- Ok(())
- }
-
- fn refresh_code_actions(&mut self, cx: &mut ViewContext<Self>) -> 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<Self>) -> 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::<DocumentHighlightRead>(
- read_ranges,
- |theme| theme.editor_document_highlight_read_background,
- cx,
- );
- this.highlight_background::<DocumentHighlightWrite>(
- 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<Self>,
- ) -> 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<Self>,
- ) -> 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<Self>) {
- 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<Self>) {
- 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<Self>,
- ) {
- 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<Self>) -> 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<Self>) -> 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<Self>,
- ) -> 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<Self>) -> Option<Inlay> {
- 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<Self>) {
- 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>) {
- 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<Self>,
- ) -> Option<IconButton> {
- 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<Option<(FoldStatus, u32, bool)>>,
- _style: &EditorStyle,
- gutter_hovered: bool,
- _line_height: Pixels,
- _gutter_margin: Pixels,
- cx: &mut ViewContext<Self>,
- ) -> Vec<Option<IconButton>> {
- 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<Editor>,
- ) -> 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<Self>) -> Option<ContextMenu> {
- 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<usize>],
- snippet: Snippet,
- cx: &mut ViewContext<Self>,
- ) -> Result<()> {
- let tabstops = self.buffer.update(cx, |buffer, cx| {
- let snippet_text: Arc<str> = 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::<Vec<_>>();
- tabstop_ranges.sort_unstable_by(|a, b| a.start.cmp(&b.start, snapshot));
- tabstop_ranges
- })
- .collect::<Vec<_>>()
- });
-
- 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<Self>) -> bool {
- self.move_to_snippet_tabstop(Bias::Right, cx)
- }
-
- pub fn move_to_prev_snippet_tabstop(&mut self, cx: &mut ViewContext<Self>) -> bool {
- self.move_to_snippet_tabstop(Bias::Left, cx)
- }
-
- pub fn move_to_snippet_tabstop(&mut self, bias: Bias, cx: &mut ViewContext<Self>) -> 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>) {
- self.transact(cx, |this, cx| {
- this.select_all(&SelectAll, cx);
- this.insert("", cx);
- });
- }
-
- pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext<Self>) {
- self.transact(cx, |this, cx| {
- this.select_autoclose_pair(cx);
- let mut selections = this.selections.all::<Point>(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>) {
- 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<Self>) {
- if self.move_to_prev_snippet_tabstop(cx) {
- return;
- }
-
- self.outdent(&Outdent, cx);
- }
-
- pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
- 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::<String>()));
- 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<Self>) {
- let mut selections = self.selections.all::<Point>(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<Point>,
- edits: &mut Vec<(Range<Point>, 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::<String>(),
- ));
-
- // 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<Self>) {
- let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
- let selections = self.selections.all::<Point>(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<str> = "".into();
- buffer.edit(
- deletion_ranges
- .into_iter()
- .map(|range| (range, empty_str.clone())),
- None,
- cx,
- );
- });
- let selections = this.selections.all::<usize>(cx);
- this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
- });
- }
-
- pub fn delete_line(&mut self, _: &DeleteLine, cx: &mut ViewContext<Self>) {
- let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
- let selections = self.selections.all::<Point>(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<str> = "".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<Self>) {
- let mut row_ranges = Vec::<Range<u32>>::new();
- for selection in self.selections.all::<Point>(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>,
- ) {
- self.manipulate_lines(cx, |lines| lines.sort())
- }
-
- pub fn sort_lines_case_insensitive(
- &mut self,
- _: &SortLinesCaseInsensitive,
- cx: &mut ViewContext<Self>,
- ) {
- self.manipulate_lines(cx, |lines| lines.sort_by_key(|line| line.to_lowercase()))
- }
-
- pub fn reverse_lines(&mut self, _: &ReverseLines, cx: &mut ViewContext<Self>) {
- self.manipulate_lines(cx, |lines| lines.reverse())
- }
-
- pub fn shuffle_lines(&mut self, _: &ShuffleLines, cx: &mut ViewContext<Self>) {
- self.manipulate_lines(cx, |lines| lines.shuffle(&mut thread_rng()))
- }
-
- fn manipulate_lines<Fn>(&mut self, cx: &mut ViewContext<Self>, 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::<Point>(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::<String>();
- 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>) {
- self.manipulate_text(cx, |text| text.to_uppercase())
- }
-
- pub fn convert_to_lower_case(&mut self, _: &ConvertToLowerCase, cx: &mut ViewContext<Self>) {
- self.manipulate_text(cx, |text| text.to_lowercase())
- }
-
- pub fn convert_to_title_case(&mut self, _: &ConvertToTitleCase, cx: &mut ViewContext<Self>) {
- 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>) {
- self.manipulate_text(cx, |text| text.to_case(Case::Snake))
- }
-
- pub fn convert_to_kebab_case(&mut self, _: &ConvertToKebabCase, cx: &mut ViewContext<Self>) {
- self.manipulate_text(cx, |text| text.to_case(Case::Kebab))
- }
-
- pub fn convert_to_upper_camel_case(
- &mut self,
- _: &ConvertToUpperCamelCase,
- cx: &mut ViewContext<Self>,
- ) {
- 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>,
- ) {
- self.manipulate_text(cx, |text| text.to_case(Case::Camel))
- }
-
- fn manipulate_text<Fn>(&mut self, cx: &mut ViewContext<Self>, 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::<usize>(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::<String>();
- 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<Self>) {
- let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
- let buffer = &display_map.buffer_snapshot;
- let selections = self.selections.all::<Point>(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::<String>();
- 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<Self>) {
- 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::<Point>(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::<String>();
-
- 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<Self>) {
- 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::<Point>(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<Self>) {
- 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<usize>, 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::<usize>(cx);
- this.change_selections(Some(Autoscroll::fit()), cx, |s| {
- s.select(selections);
- });
- });
- }
-
- pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext<Self>) {
- let mut text = String::new();
- let buffer = self.buffer.read(cx).snapshot(cx);
- let mut selections = self.selections.all::<Point>(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<Self>) {
- let selections = self.selections.all::<Point>(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>) {
- 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::<Vec<ClipboardSelection>>() {
- let old_selections = this.selections.all::<usize>(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::<usize>(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<Self>) {
- 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<Self>) {
- 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>) {
- self.buffer
- .update(cx, |buffer, cx| buffer.finalize_last_transaction(cx));
- }
-
- pub fn move_left(&mut self, _: &MoveLeft, cx: &mut ViewContext<Self>) {
- 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>) {
- 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>) {
- 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>) {
- 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<Self>) {
- 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<Self>) {
- 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<Self>) {
- 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>) {
- 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<Self>) {
- 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<Self>) {
- 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<Self>) {
- 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<Self>) {
- 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<Self>) {
- 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<Self>) {
- 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>,
- ) {
- 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>,
- ) {
- 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>,
- ) {
- 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>,
- ) {
- 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>,
- ) {
- 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>,
- ) {
- 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>) {
- 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>,
- ) {
- 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>) {
- 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>,
- ) {
- 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>) {
- 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>,
- ) {
- 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>,
- ) {
- 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>,
- ) {
- 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>,
- ) {
- 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>) {
- 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>,
- ) {
- 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>) {
- 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>) {
- 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<Self>,
- ) {
- 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<Self>,
- ) {
- 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<Self>,
- ) {
- 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<Self>,
- ) {
- 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<Self>) {
- 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<Self>) {
- let mut selection = self.selections.last::<Point>(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<Self>) {
- 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<ItemNavHistory>) {
- 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<Point>,
- cx: &mut ViewContext<Self>,
- ) {
- 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<Self>) {
- let buffer = self.buffer.read(cx).snapshot(cx);
- let mut selection = self.selections.first::<usize>(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<Self>) {
- 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<Self>) {
- let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
- let mut selections = self.selections.all::<Point>(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<Self>,
- ) {
- let mut to_unfold = Vec::new();
- let mut new_selection_ranges = Vec::new();
- {
- let selections = self.selections.all::<Point>(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>) {
- self.add_selection(true, cx);
- }
-
- pub fn add_selection_below(&mut self, _: &AddSelectionBelow, cx: &mut ViewContext<Self>) {
- self.add_selection(false, cx);
- }
-
- fn add_selection(&mut self, above: bool, cx: &mut ViewContext<Self>) {
- let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
- let mut selections = self.selections.all::<Point>(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<Autoscroll>,
- cx: &mut ViewContext<Self>,
- ) -> Result<()> {
- fn select_next_match_ranges(
- this: &mut Editor,
- range: Range<usize>,
- replace_newest: bool,
- auto_scroll: Option<Autoscroll>,
- cx: &mut ViewContext<Editor>,
- ) {
- 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::<usize>(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::<String>();
-
- 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::<String>();
- 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<Self>,
- ) -> 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<Self>) -> 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<Self>,
- ) -> 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::<usize>(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::<String>();
- let query = query.chars().rev().collect::<String>();
- 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::<String>();
- let query = query.chars().rev().collect::<String>();
- 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<Self>) {
- let text_layout_details = &self.text_layout_details(cx);
- self.transact(cx, |this, cx| {
- let mut selections = this.selections.all::<Point>(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<str> = "".into();
- let mut suffixes_inserted = Vec::new();
-
- fn comment_prefix_range(
- snapshot: &MultiBufferSnapshot,
- row: u32,
- comment_prefix: &str,
- comment_prefix_whitespace: &str,
- ) -> Range<Point> {
- 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<Point> {
- 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::<Point>(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::<Point>(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<Self>,
- ) {
- 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::<usize>(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::<Vec<_>>();
-
- 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<Self>,
- ) {
- 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>,
- ) {
- 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>) {
- 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>) {
- 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>) {
- self.go_to_diagnostic_impl(Direction::Next, cx)
- }
-
- fn go_to_prev_diagnostic(&mut self, _: &GoToPrevDiagnostic, cx: &mut ViewContext<Self>) {
- self.go_to_diagnostic_impl(Direction::Prev, cx)
- }
-
- pub fn go_to_diagnostic_impl(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
- let buffer = self.buffer.read(cx).snapshot(cx);
- let selection = self.selections.newest::<usize>(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<Self>) {
- let snapshot = self
- .display_map
- .update(cx, |display_map, cx| display_map.snapshot(cx));
- let selection = self.selections.newest::<Point>(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<Self>) {
- let snapshot = self
- .display_map
- .update(cx, |display_map, cx| display_map.snapshot(cx));
- let selection = self.selections.newest::<Point>(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<Item = DiffHunk<u32>>,
- cx: &mut ViewContext<Editor>,
- ) -> 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>) {
- self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, false, cx);
- }
-
- pub fn go_to_type_definition(&mut self, _: &GoToTypeDefinition, cx: &mut ViewContext<Self>) {
- self.go_to_definition_of_kind(GotoDefinitionKind::Type, false, cx);
- }
-
- pub fn go_to_definition_split(&mut self, _: &GoToDefinitionSplit, cx: &mut ViewContext<Self>) {
- self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, true, cx);
- }
-
- pub fn go_to_type_definition_split(
- &mut self,
- _: &GoToTypeDefinitionSplit,
- cx: &mut ViewContext<Self>,
- ) {
- 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<Self>,
- ) {
- let Some(workspace) = self.workspace() else {
- return;
- };
- let buffer = self.buffer.read(cx);
- let head = self.selections.newest::<usize>(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<GoToDefinitionLink>,
- split: bool,
- cx: &mut ViewContext<Editor>,
- ) {
- 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<Self> =
- 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::<String>()
- )
- })
- }
- 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::<Vec<_>>();
- (title, location_tasks)
- })
- .context("location tasks preparation")?;
-
- let locations = futures::future::join_all(location_tasks)
- .await
- .into_iter()
- .filter_map(|location| location.transpose())
- .collect::<Result<_>>()
- .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<Editor>,
- ) -> Task<anyhow::Result<Option<Location>>> {
- 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<Self>,
- ) -> Option<Task<Result<()>>> {
- let buffer = self.buffer.read(cx);
- let head = self.selections.newest::<usize>(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::<String>()
- )
- })
- .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<Location>,
- replica_id: ReplicaId,
- title: String,
- split: bool,
- cx: &mut ViewContext<Workspace>,
- ) {
- // 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::<Self>(
- 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<Self>) -> Option<Task<Result<()>>> {
- 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<str> = 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::<String>()
- .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::<DocumentHighlightWrite>(cx)
- .into_iter()
- .flat_map(|(_, ranges)| ranges.into_iter())
- .chain(
- this.clear_background_highlights::<DocumentHighlightRead>(cx)
- .into_iter()
- .flat_map(|(_, ranges)| ranges.into_iter()),
- )
- .collect();
-
- this.highlight_text::<Rename>(
- 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<Self>,
- ) -> Option<Task<Result<()>>> {
- 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<Self>,
- ) -> Option<RenameState> {
- 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::<Rename>(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::<usize>(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<Self>) -> Option<Task<Result<()>>> {
- 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<Project>,
- trigger: FormatTrigger,
- cx: &mut ViewContext<Self>,
- ) -> Task<Result<()>> {
- 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<Self>) {
- 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<Self>) {
- cx.show_character_palette();
- }
-
- fn refresh_active_diagnostics(&mut self, cx: &mut ViewContext<Editor>) {
- 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<Self>) -> 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::<Point>(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::<Vec<_>>();
- 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<Self>) {
- 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<Selection<Anchor>>,
- pending_selection: Option<Selection<Anchor>>,
- cx: &mut ViewContext<Self>,
- ) {
- 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<Self>,
- update: impl FnOnce(&mut Self, &mut ViewContext<Self>),
- ) -> Option<TransactionId> {
- 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>) {
- 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<Self>,
- ) -> Option<TransactionId> {
- 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<Self>) {
- 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<Self>) {
- 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::<Point>(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<Self>) {
- let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
- let buffer = &display_map.buffer_snapshot;
- let selections = self.selections.all::<Point>(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::<Vec<_>>();
-
- self.unfold_ranges(ranges, true, true, cx);
- }
-
- pub fn unfold_at(&mut self, unfold_at: &UnfoldAt, cx: &mut ViewContext<Self>) {
- 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::<Point>(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<Self>) {
- let selections = self.selections.all::<Point>(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<T: ToOffset + Clone>(
- &mut self,
- ranges: impl IntoIterator<Item = Range<T>>,
- auto_scroll: bool,
- cx: &mut ViewContext<Self>,
- ) {
- 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<T: ToOffset + Clone>(
- &mut self,
- ranges: impl IntoIterator<Item = Range<T>>,
- inclusive: bool,
- auto_scroll: bool,
- cx: &mut ViewContext<Self>,
- ) {
- 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<Self>) {
- if hovered != self.gutter_hovered {
- self.gutter_hovered = hovered;
- cx.notify();
- }
- }
-
- pub fn insert_blocks(
- &mut self,
- blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
- autoscroll: Option<Autoscroll>,
- cx: &mut ViewContext<Self>,
- ) -> Vec<BlockId> {
- 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<BlockId, RenderBlock>,
- autoscroll: Option<Autoscroll>,
- cx: &mut ViewContext<Self>,
- ) {
- 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<BlockId>,
- autoscroll: Option<Autoscroll>,
- cx: &mut ViewContext<Self>,
- ) {
- 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<String> {
- 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<Arc<str>>, cx: &mut ViewContext<Self>) {
- 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>,
- ) {
- self.soft_wrap_mode_override = Some(mode);
- cx.notify();
- }
-
- pub fn set_style(&mut self, style: EditorStyle, cx: &mut ViewContext<Self>) {
- 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<Pixels>, 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<Self>) {
- 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>) {
- self.show_gutter = show_gutter;
- cx.notify();
- }
-
- pub fn set_show_wrap_guides(&mut self, show_gutter: bool, cx: &mut ViewContext<Self>) {
- self.show_wrap_guides = Some(show_gutter);
- cx.notify();
- }
-
- pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
- 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<Self>) {
- 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<Self>) {
- 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<Range<u32>>) {
- self.highlighted_rows = rows;
- }
-
- pub fn highlighted_rows(&self) -> Option<Range<u32>> {
- self.highlighted_rows.clone()
- }
-
- pub fn highlight_background<T: 'static>(
- &mut self,
- ranges: Vec<Range<Anchor>>,
- color_fetcher: fn(&ThemeColors) -> Hsla,
- cx: &mut ViewContext<Self>,
- ) {
- self.background_highlights
- .insert(TypeId::of::<T>(), (color_fetcher, ranges));
- cx.notify();
- }
-
- pub fn highlight_inlay_background<T: 'static>(
- &mut self,
- ranges: Vec<InlayHighlight>,
- color_fetcher: fn(&ThemeColors) -> Hsla,
- cx: &mut ViewContext<Self>,
- ) {
- // TODO: no actual highlights happen for inlays currently, find a way to do that
- self.inlay_background_highlights
- .insert(Some(TypeId::of::<T>()), (color_fetcher, ranges));
- cx.notify();
- }
-
- pub fn clear_background_highlights<T: 'static>(
- &mut self,
- cx: &mut ViewContext<Self>,
- ) -> Option<BackgroundHighlight> {
- let text_highlights = self.background_highlights.remove(&TypeId::of::<T>());
- let inlay_highlights = self
- .inlay_background_highlights
- .remove(&Some(TypeId::of::<T>()));
- 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<Self>,
- ) -> Vec<(Range<DisplayPoint>, 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<Item = &Range<Anchor>> {
- let read_highlights = self
- .background_highlights
- .get(&TypeId::of::<DocumentHighlightRead>())
- .map(|h| &h.1);
- let write_highlights = self
- .background_highlights
- .get(&TypeId::of::<DocumentHighlightWrite>())
- .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<Anchor>,
- display_snapshot: &DisplaySnapshot,
- theme: &ThemeColors,
- ) -> Vec<(Range<DisplayPoint>, 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<T: 'static>(
- &self,
- search_range: Range<Anchor>,
- display_snapshot: &DisplaySnapshot,
- count: usize,
- ) -> Vec<RangeInclusive<DisplayPoint>> {
- let mut results = Vec::new();
- let Some((_, ranges)) = self.background_highlights.get(&TypeId::of::<T>()) 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<Point>, end: Option<Point>| {
- 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<Point> = None;
- let mut end_row: Option<Point> = 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<T: 'static>(
- &mut self,
- ranges: Vec<Range<Anchor>>,
- style: HighlightStyle,
- cx: &mut ViewContext<Self>,
- ) {
- self.display_map.update(cx, |map, _| {
- map.highlight_text(TypeId::of::<T>(), ranges, style)
- });
- cx.notify();
- }
-
- pub fn highlight_inlays<T: 'static>(
- &mut self,
- highlights: Vec<InlayHighlight>,
- style: HighlightStyle,
- cx: &mut ViewContext<Self>,
- ) {
- self.display_map.update(cx, |map, _| {
- map.highlight_inlays(TypeId::of::<T>(), highlights, style)
- });
- cx.notify();
- }
-
- pub fn text_highlights<'a, T: 'static>(
- &'a self,
- cx: &'a AppContext,
- ) -> Option<(HighlightStyle, &'a [Range<Anchor>])> {
- self.display_map.read(cx).text_highlights(TypeId::of::<T>())
- }
-
- pub fn clear_highlights<T: 'static>(&mut self, cx: &mut ViewContext<Self>) {
- let cleared = self
- .display_map
- .update(cx, |map, _| map.clear_highlights(TypeId::of::<T>()));
- 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<MultiBuffer>, cx: &mut ViewContext<Self>) {
- cx.notify();
- }
-
- fn on_buffer_event(
- &mut self,
- multibuffer: Model<MultiBuffer>,
- event: &multi_buffer::Event,
- cx: &mut ViewContext<Self>,
- ) {
- 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::<HashSet<_>>();
- 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<DisplayMap>, cx: &mut ViewContext<Self>) {
- cx.notify();
- }
-
- fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
- 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<Self>) {
- 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::<usize>(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::<Self>(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<Self>,
- ) {
- 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::<Editor>()
- .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<Vec<Range<OffsetUtf16>>> {
- let snapshot = self.buffer.read(cx).read(cx);
- let (_, ranges) = self.text_highlights::<InputComposition>(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<OffsetUtf16>,
- cx: &AppContext,
- ) -> Vec<Range<OffsetUtf16>> {
- let selections = self.selections.all::<OffsetUtf16>(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<String>,
- 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<String>,
- _cx: &AppContext,
- ) {
- }
-
- #[cfg(not(any(test, feature = "test-support")))]
- fn report_editor_event(
- &self,
- operation: &'static str,
- file_extension: Option<String>,
- 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::<SettingsStore>()
- .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<Self>) {
- 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<Chunk> = 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<Range<isize>>,
- cx: &mut ViewContext<Self>,
- ) {
- 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::<OffsetUtf16>(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<Self>) {
- 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>) {
- 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<A: 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::<A>(), 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<PeerId, Collaborator>;
- fn user_participant_indices<'a>(
- &self,
- cx: &'a AppContext,
- ) -> &'a HashMap<u64, ParticipantIndex>;
-}
-
-impl CollaborationHub for Model<Project> {
- fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
- self.read(cx).collaborators()
- }
-
- fn user_participant_indices<'a>(
- &self,
- cx: &'a AppContext,
- ) -> &'a HashMap<u64, ParticipantIndex> {
- 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<Point>>,
- selection: &Selection<Point>,
- display_map: &DisplaySnapshot,
- selections: &mut std::iter::Peekable<std::slice::Iter<Selection<Point>>>,
-) -> (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<Point>, 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<Anchor>,
- collaboration_hub: &dyn CollaborationHub,
- cx: &'a AppContext,
- ) -> impl 'a + Iterator<Item = RemoteSelection> {
- 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::<HashMap<_, _>>();
- 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<T: ToOffset>(&self, position: T) -> Option<&Arc<Language>> {
- self.display_snapshot.buffer_snapshot.language_at(position)
- }
-
- pub fn is_focused(&self) -> bool {
- self.is_focused
- }
-
- pub fn placeholder_text(&self) -> Option<&Arc<str>> {
- self.placeholder_text.as_ref()
- }
-
- pub fn scroll_position(&self) -> gpui::Point<f32> {
- 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<str>,
- },
- InputHandled {
- utf16_range_to_replace: Option<Range<isize>>,
- text: Arc<str>,
- },
- ExcerptsAdded {
- buffer: Model<Buffer>,
- predecessor: ExcerptId,
- excerpts: Vec<(ExcerptId, ExcerptRange<language::Anchor>)>,
- },
- ExcerptsRemoved {
- ids: Vec<ExcerptId>,
- },
- BufferEdited,
- Edited,
- Reparsed,
- Focused,
- Blurred,
- DirtyChanged,
- Saved,
- TitleChanged,
- DiffBaseChanged,
- SelectionsChanged {
- local: bool,
- },
- ScrollPositionChanged {
- local: bool,
- autoscroll: bool,
- },
- Closed,
-}
-
-impl EventEmitter<EditorEvent> 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<usize>,
- cx: &mut ViewContext<Self>,
- ) -> Option<String> {
- 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<Self>) -> Option<Range<usize>> {
- // 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::<OffsetUtf16>(cx).range();
- Some(range.start.0..range.end.0)
- }
-
- fn marked_text_range(&self, cx: &mut ViewContext<Self>) -> Option<Range<usize>> {
- let snapshot = self.buffer.read(cx).read(cx);
- let range = self.text_highlights::<InputComposition>(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>) {
- self.clear_highlights::<InputComposition>(cx);
- self.ime_transaction.take();
- }
-
- fn replace_text_in_range(
- &mut self,
- range_utf16: Option<Range<usize>>,
- text: &str,
- cx: &mut ViewContext<Self>,
- ) {
- 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::<OffsetUtf16>(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<Range<usize>>,
- text: &str,
- new_selected_range_utf16: Option<Range<usize>>,
- cx: &mut ViewContext<Self>,
- ) {
- 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::<OffsetUtf16>(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::<Vec<_>>()
- };
-
- if text.is_empty() {
- this.unmark_text(cx);
- } else {
- this.highlight_text::<InputComposition>(
- 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::<Vec<_>>();
-
- 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::<InputComposition>(cx).is_none() {
- self.ime_transaction.take();
- }
- }
-
- fn bounds_for_range(
- &mut self,
- range_utf16: Range<usize>,
- element_bounds: gpui::Bounds<Pixels>,
- cx: &mut ViewContext<Self>,
- ) -> Option<gpui::Bounds<Pixels>> {
- 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<usize>;
- fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range<Point>;
- fn display_range(&self, map: &DisplaySnapshot) -> Range<DisplayPoint>;
- fn spanned_rows(&self, include_end_if_at_line_start: bool, map: &DisplaySnapshot)
- -> Range<u32>;
-}
-
-impl<T: ToPoint + ToOffset> SelectionExt for Selection<T> {
- fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range<Point> {
- 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<usize> {
- 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<DisplayPoint> {
- 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<u32> {
- 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<T: InvalidationRegion> InvalidationStack<T> {
- fn invalidate<S>(&mut self, selections: &[Selection<S>], 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<T> Default for InvalidationStack<T> {
- fn default() -> Self {
- Self(Default::default())
- }
-}
-
-impl<T> Deref for InvalidationStack<T> {
- type Target = Vec<T>;
-
- fn deref(&self) -> &Self::Target {
- &self.0
- }
-}
-
-impl<T> DerefMut for InvalidationStack<T> {
- fn deref_mut(&mut self) -> &mut Self::Target {
- &mut self.0
- }
-}
-
-impl InvalidationRegion for SnippetState {
- fn ranges(&self) -> &[Range<Anchor>] {
- &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<Range<usize>>) {
- 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<Item = (Range<usize>, 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<usize>, 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<Item = &'a str> + '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<Anchor>;
-}
-
-impl<T: ToOffset> RangeToAnchorExt for Range<T> {
- fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range<Anchor> {
- snapshot.anchor_after(self.start)..snapshot.anchor_before(self.end)
- }
-}
@@ -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<bool>,
- pub hover_popover_enabled: Option<bool>,
- pub show_completions_on_input: Option<bool>,
- pub show_completion_documentation: Option<bool>,
- pub use_on_type_format: Option<bool>,
- pub scrollbar: Option<ScrollbarContent>,
- pub relative_line_numbers: Option<bool>,
- pub seed_search_query_from_cursor: Option<SeedQuerySetting>,
-}
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-pub struct ScrollbarContent {
- pub show: Option<ShowScrollbar>,
- pub git_diff: Option<bool>,
- pub selections: Option<bool>,
-}
-
-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> {
- Self::load_via_json_merge(default_value, user_values)
- }
-}
@@ -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<DisplayPoint>,
- active_rows: Range<u32>,
-}
-
-impl SelectionLayout {
- fn new<T: ToPoint + ToDisplayPoint + Clone>(
- selection: Selection<T>,
- 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<Editor>,
- style: EditorStyle,
-}
-
-impl EditorElement {
- pub fn new(editor: &View<Editor>, 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<Editor>,
- ) -> 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::<LinkGoToDefinitionState>(cx);
- }
-
- false
- }
-
- fn mouse_left_down(
- editor: &mut Editor,
- event: &MouseDownEvent,
- position_map: &PositionMap,
- text_bounds: Bounds<Pixels>,
- gutter_bounds: Bounds<Pixels>,
- stacking_order: &StackingOrder,
- cx: &mut ViewContext<Editor>,
- ) {
- 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<Pixels>,
- cx: &mut ViewContext<Editor>,
- ) {
- 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<Pixels>,
- stacking_order: &StackingOrder,
- cx: &mut ViewContext<Editor>,
- ) {
- 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<Pixels>,
- _gutter_bounds: Bounds<Pixels>,
- _stacking_order: &StackingOrder,
- cx: &mut ViewContext<Editor>,
- ) {
- 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::<f32>::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<Pixels>,
- gutter_bounds: Bounds<Pixels>,
- stacking_order: &StackingOrder,
- cx: &mut ViewContext<Editor>,
- ) {
- 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<Editor>,
- ) {
- 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<Pixels>,
- text_bounds: Bounds<Pixels>,
- 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<Pixels>,
- 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<Pixels>, 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<Pixels>,
- 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<DisplayPoint>; 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<Pixels>,
- 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>) -> Pixels {
- bounds.upper_right().x - self.style.scrollbar_width
- }
-
- fn paint_scrollbar(
- &mut self,
- bounds: Bounds<Pixels>,
- 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::<crate::items::BufferSearchHighlights>(
- 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<DisplayPoint>,
- color: Hsla,
- corner_radius: Pixels,
- line_end_overshoot: Pixels,
- layout: &LayoutState,
- content_origin: gpui::Point<Pixels>,
- bounds: Bounds<Pixels>,
- 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<Pixels>,
- 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<u32>,
- snapshot: &EditorSnapshot,
- ) -> Vec<DisplayDiffHunk> {
- 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<u32>,
- relative_to: Option<u32>,
- ) -> HashMap<u32, u32> {
- let mut relative_rows: HashMap<u32, u32> = 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::<Vec<_>>();
-
- 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<u32>,
- active_rows: &BTreeMap<u32, bool>,
- newest_selection_head: DisplayPoint,
- is_singleton: bool,
- snapshot: &EditorSnapshot,
- cx: &ViewContext<Editor>,
- ) -> (
- Vec<Option<ShapedLine>>,
- Vec<Option<(FoldStatus, BufferRow, bool)>>,
- ) {
- 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<u32>,
- line_number_layouts: &[Option<ShapedLine>],
- snapshot: &EditorSnapshot,
- cx: &ViewContext<Editor>,
- ) -> Vec<LineWithInvisibles> {
- 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<Pixels>, 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::<SmallVec<[_; 2]>>();
-
- 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<SelectionLayout>)> = 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<Selection<Point>> = 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::<Point>(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<u32>,
- 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<Editor>,
- ) -> (Pixels, Vec<BlockLayout>) {
- let mut block_id = 0;
- let (fixed_blocks, non_fixed_blocks) = snapshot
- .blocks_in_range(rows.clone())
- .partition::<Vec<_>, _>(|(_, block)| match block {
- TransformBlock::ExcerptHeader { .. } => false,
- TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed,
- });
-
- let render_block = |block: &TransformBlock,
- available_space: Size<AvailableSpace>,
- block_id: usize,
- editor: &mut Editor,
- cx: &mut ViewContext<Editor>| {
- 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<Pixels>,
- gutter_bounds: Bounds<Pixels>,
- text_bounds: Bounds<Pixels>,
- 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<Invisible>,
-}
-
-impl LineWithInvisibles {
- fn from_chunks<'a>(
- chunks: impl Iterator<Item = HighlightedChunk<'a>>,
- text_style: &TextStyle,
- max_line_len: usize,
- max_line_count: usize,
- line_number_layouts: &[Option<ShapedLine>],
- editor_mode: EditorMode,
- cx: &WindowContext,
- ) -> Vec<Self> {
- 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<Pixels>,
- whitespace_setting: ShowWhitespaceSetting,
- selection_ranges: &[Range<DisplayPoint>],
- 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<DisplayPoint>],
- layout: &LayoutState,
- content_origin: gpui::Point<Pixels>,
- 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<Self::State>,
- 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<gpui::Pixels>,
- _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<gpui::ElementId> {
- self.editor.element_id()
- }
-
- fn into_element(self) -> Self::Element {
- self
- }
-}
-
-type BufferRow = u32;
-
-pub struct LayoutState {
- position_map: Arc<PositionMap>,
- gutter_size: Size<Pixels>,
- gutter_padding: Pixels,
- gutter_margin: Pixels,
- text_size: gpui::Size<Pixels>,
- mode: EditorMode,
- wrap_guides: SmallVec<[(Pixels, bool); 2]>,
- visible_anchor_range: Range<Anchor>,
- visible_display_row_range: Range<u32>,
- active_rows: BTreeMap<u32, bool>,
- highlighted_rows: Option<Range<u32>>,
- line_numbers: Vec<Option<ShapedLine>>,
- display_hunks: Vec<DisplayDiffHunk>,
- blocks: Vec<BlockLayout>,
- highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
- selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
- scrollbar_row_range: Range<f32>,
- show_scrollbars: bool,
- is_singleton: bool,
- max_row: u32,
- context_menu: Option<(DisplayPoint, AnyElement)>,
- code_actions_indicator: Option<CodeActionsIndicator>,
- hover_popovers: Option<(DisplayPoint, Vec<AnyElement>)>,
- fold_indicators: Vec<Option<IconButton>>,
- tab_invisible: ShapedLine,
- space_invisible: ShapedLine,
-}
-
-struct CodeActionsIndicator {
- row: u32,
- button: IconButton,
-}
-
-struct PositionMap {
- size: Size<Pixels>,
- line_height: Pixels,
- scroll_position: gpui::Point<Pixels>,
- scroll_max: gpui::Point<f32>,
- em_width: Pixels,
- em_advance: Pixels,
- line_layouts: Vec<LineWithInvisibles>,
- 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<DisplayPoint> {
- 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<Pixels>,
- position: gpui::Point<Pixels>,
- ) -> 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<AvailableSpace>,
- style: BlockStyle,
-}
-
-fn layout_line(
- row: u32,
- snapshot: &EditorSnapshot,
- style: &EditorStyle,
- cx: &WindowContext,
-) -> Result<ShapedLine> {
- 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<Pixels>,
- block_width: Pixels,
- line_height: Pixels,
- color: Hsla,
- shape: CursorShape,
- block_text: Option<ShapedLine>,
-}
-
-impl Cursor {
- pub fn new(
- origin: gpui::Point<Pixels>,
- block_width: Pixels,
- line_height: Pixels,
- color: Hsla,
- shape: CursorShape,
- block_text: Option<ShapedLine>,
- ) -> Cursor {
- Cursor {
- origin,
- block_width,
- line_height,
- color,
- shape,
- block_text,
- }
- }
-
- pub fn bounding_rect(&self, origin: gpui::Point<Pixels>) -> Bounds<Pixels> {
- Bounds {
- origin: self.origin + origin,
- size: size(self.block_width, self.line_height),
- }
- }
-
- pub fn paint(&self, origin: gpui::Point<Pixels>, 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<HighlightedRangeLine>,
- 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<Pixels>, 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<Pixels>,
- 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<u32>>(),
- 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::<Vec<_>>(),
- &[false, false, false, true]
- );
-
- // Don't panic.
- let bounds = Bounds::<Pixels>::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::<Vec<_>>();
- 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<Invisible> {
- 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<T: Action>(
- view: &View<Editor>,
- cx: &mut WindowContext,
- listener: impl Fn(&mut Editor, &T, &mut ViewContext<Editor>) + 'static,
-) {
- let view = view.clone();
- cx.on_action(TypeId::of::<T>(), 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<Option<Pixels>>,
- cx: &mut ViewContext<Editor>,
-) -> Option<Size<Pixels>> {
- 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))
-}
@@ -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<u32>,
- 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<u32>, 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::<Vec<_>>(),
- &expected,
- );
-
- assert_eq!(
- snapshot
- .git_diff_hunks_in_range_rev(0..12)
- .map(|hunk| (hunk.status(), hunk.buffer_range))
- .collect::<Vec<_>>(),
- expected
- .iter()
- .rev()
- .cloned()
- .collect::<Vec<_>>()
- .as_slice(),
- );
- }
-}
@@ -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>) {
- editor.clear_background_highlights::<MatchingBracketHighlight>(cx);
-
- let newest_selection = editor.selections.newest::<usize>(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::<MatchingBracketHighlight>(
- 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::<MatchingBracketHighlight>(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::<MatchingBracketHighlight>(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::<MatchingBracketHighlight>(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::<MatchingBracketHighlight>(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::<MatchingBracketHighlight>(indoc! {r#"
- pub fn test("Test argument") {
- another_test(1, 2, 3);
- }
- "#});
- }
-}
@@ -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<Editor>) {
- 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<DisplayPoint>, cx: &mut ViewContext<Editor>) {
- 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<InlayHintLabelPart>,
- hint_start: InlayOffset,
- hovered_offset: InlayOffset,
-) -> Option<(InlayHintLabelPart, Range<InlayOffset>)> {
- 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<Editor>) {
- 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::<HoverState>(
- 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<Editor>) -> 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::<HoverState>(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<Editor>,
-) {
- 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::<usize>(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::<HoverState>(
- vec![symbol_range],
- |theme| theme.element_hover, // todo! update theme
- cx,
- );
- } else {
- this.clear_background_highlights::<HoverState>(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<LanguageRegistry>,
- language: Option<Arc<Language>>,
-) -> 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<InfoPopover>,
- pub diagnostic_popover: Option<DiagnosticPopover>,
- pub triggered_from: Option<Anchor>,
- pub info_task: Option<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<u32>,
- max_size: Size<Pixels>,
- workspace: Option<WeakView<Workspace>>,
- cx: &mut ViewContext<Editor>,
- ) -> Option<(DisplayPoint, Vec<AnyElement>)> {
- // 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<Project>,
- symbol_range: RangeInEditor,
- pub blocks: Vec<HoverBlock>,
- parsed_content: ParsedMarkdown,
-}
-
-impl InfoPopover {
- pub fn render(
- &mut self,
- style: &EditorStyle,
- max_size: Size<Pixels>,
- workspace: Option<WeakView<Workspace>>,
- cx: &mut ViewContext<Editor>,
- ) -> 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<Anchor>,
- primary_diagnostic: Option<DiagnosticEntry<Anchor>>,
-}
-
-impl DiagnosticPopover {
- pub fn render(
- &self,
- style: &EditorStyle,
- max_size: Size<Pixels>,
- cx: &mut ViewContext<Editor>,
- ) -> 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::<lsp::request::HoverRequest, _, _>(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::<lsp::request::HoverRequest, _, _>(|_, _| 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::<lsp::request::HoverRequest, _, _>(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::<lsp::request::HoverRequest, _, _>(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::<lsp::request::HoverRequest, _, _>(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::<lsp::request::HoverRequest, _, _>(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<HoverBlock>,
- expected_marked_text: String,
- expected_styles: Vec<HighlightStyle>,
- }
-
- 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 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
- - 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::<Vec<_>>();
- 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>(T);
-
- fn main() {
- let variableห = TestNewType(TestStruct);
- }
- "});
-
- let hint_start_offset = cx.ranges(indoc! {"
- struct TestStruct;
-
- // ==================
-
- struct TestNewType<T>(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>(T);
-
- fn main() {
- let variable = TestNewType(TestStruct);
- }
- "});
- let struct_target_range = cx.lsp_range(indoc! {"
- struct ยซTestStructยป;
-
- // ==================
-
- struct TestNewType<T>(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<TestStruct>";
- let closure_uri = uri.clone();
- cx.lsp
- .handle_request::<lsp::request::InlayHintRequest, _, _>(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>(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::<lsp::request::InlayHintResolveRequest, _, _>(
- 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<TestStruct>`
- 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"
- );
- });
- }
-}
@@ -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<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
- allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
- version: usize,
- pub(super) enabled: bool,
- update_tasks: HashMap<ExcerptId, TasksForRanges>,
- lsp_request_limiter: Arc<Semaphore>,
-}
-
-#[derive(Debug)]
-struct TasksForRanges {
- tasks: Vec<Task<()>>,
- sorted_ranges: Vec<Range<language::Anchor>>,
-}
-
-#[derive(Debug)]
-pub struct CachedExcerptHints {
- version: usize,
- buffer_version: Global,
- buffer_id: u64,
- ordered_hints: Vec<InlayId>,
- hints_by_id: HashMap<InlayId, InlayHint>,
-}
-
-#[derive(Debug, Clone, Copy)]
-pub enum InvalidationStrategy {
- RefreshRequested,
- BufferEdited,
- None,
-}
-
-#[derive(Debug, Default)]
-pub struct InlaySplice {
- pub to_remove: Vec<InlayId>,
- pub to_insert: Vec<Inlay>,
-}
-
-#[derive(Debug)]
-struct ExcerptHintsUpdate {
- excerpt_id: ExcerptId,
- remove_from_visible: Vec<InlayId>,
- remove_from_cache: HashSet<InlayId>,
- add_to_cache: Vec<InlayHint>,
-}
-
-#[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<language::Anchor>,
- ) -> Vec<Range<language::Anchor>> {
- let mut ranges_to_query = Vec::new();
- let mut latest_cached_range = None::<&mut Range<language::Anchor>>;
- 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<language::Anchor>) {
- 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<MultiBuffer>,
- new_hint_settings: InlayHintSettings,
- visible_hints: Vec<Inlay>,
- cx: &mut ViewContext<Editor>,
- ) -> ControlFlow<Option<InlaySplice>> {
- 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<ExcerptId, (Model<Buffer>, Global, Range<usize>)>,
- invalidate: InvalidationStrategy,
- cx: &mut ViewContext<Editor>,
- ) -> Option<InlaySplice> {
- 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<MultiBuffer>,
- visible_hints: &[Inlay],
- new_kinds: &HashSet<Option<InlayHintKind>>,
- cx: &mut ViewContext<Editor>,
- ) -> Option<InlaySplice> {
- 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::<ExcerptId, Vec<(Anchor, InlayId)>>::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<ExcerptId>) -> Option<InlaySplice> {
- 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<InlayHint> {
- self.hints
- .get(&excerpt_id)?
- .read()
- .hints_by_id
- .get(&hint_id)
- .cloned()
- }
-
- pub fn hints(&self) -> Vec<InlayHint> {
- 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<ExcerptId, (Model<Buffer>, Global, Range<usize>)>,
- 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<Range<language::Anchor>>,
- visible: Vec<Range<language::Anchor>>,
- after_visible: Vec<Range<language::Anchor>>,
-}
-
-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<Buffer>,
- excerpt_visible_range: Range<usize>,
- cx: &mut ModelContext<'_, MultiBuffer>,
-) -> Option<QueryRanges> {
- 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<Vec<Inlay>>,
- cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
- lsp_request_limiter: Arc<Semaphore>,
- 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<language::Anchor>, 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<Editor>,
- multi_buffer_snapshot: MultiBufferSnapshot,
- buffer_snapshot: BufferSnapshot,
- visible_hints: Arc<Vec<Inlay>>,
- cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
- query: ExcerptQuery,
- invalidate: bool,
- fetch_range: Range<language::Anchor>,
- lsp_request_limiter: Arc<Semaphore>,
- 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<language::Anchor>,
- new_excerpt_hints: Vec<InlayHint>,
- buffer_snapshot: &BufferSnapshot,
- cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
- visible_hints: &[Inlay],
-) -> Option<ExcerptHintsUpdate> {
- let mut add_to_cache = Vec::<InlayHint>::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<language::Anchor>,
- 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::<lsp::request::InlayHintRequest, _, _>(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::<lsp::request::InlayHintRefreshRequest>(())
- .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::<lsp::request::InlayHintRequest, _, _>(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::request::WorkDoneProgressCreate>(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::notification::Progress>(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::notification::Progress>(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::<lsp::request::InlayHintRequest, _, _>(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::<lsp::request::InlayHintRequest, _, _>(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::<lsp::request::InlayHintRequest, _, _>(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::<lsp::request::InlayHintRefreshRequest>(())
- .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::<lsp::request::InlayHintRefreshRequest>(())
- .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::<lsp::request::InlayHintRefreshRequest>(())
- .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::<lsp::request::InlayHintRequest, _, _>(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::<lsp::request::InlayHintRequest, _, _>(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<Editor>,
- cx: &mut gpui::TestAppContext,
- ) -> Range<Point> {
- 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::<Vec<_>>();
- 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::<Vec<_>>();
- 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::<Vec<_>>();
- 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::<Vec<_>>();
- 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::<Vec<_>>().join("")),
- "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().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::<lsp::request::InlayHintRequest, _, _>(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::<Vec<_>>().join("")),
- "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().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::<lsp::request::InlayHintRequest, _, _>(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::<lsp::request::InlayHintRequest, _, _>(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::<lsp::request::InlayHintRequest, _, _>(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<Editor>, 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<String> {
- 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<String> {
- let mut hints = editor
- .visible_inlay_hints(cx)
- .into_iter()
- .map(|hint| hint.text.to_string())
- .collect::<Vec<_>>();
- hints.sort();
- hints
- }
-}
@@ -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<ViewId> {
- self.remote_id
- }
-
- fn from_state_proto(
- pane: View<workspace::Pane>,
- workspace: View<Workspace>,
- remote_id: ViewId,
- state: &mut Option<proto::view::Variant>,
- cx: &mut WindowContext,
- ) -> Option<Task<Result<View<Self>>>> {
- 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::<HashSet<_>>();
- let buffers = project.update(cx, |project, cx| {
- buffer_ids
- .iter()
- .map(|id| project.open_buffer_by_id(*id, cx))
- .collect::<Vec<_>>()
- });
-
- 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::<Self>();
- 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<PeerId>, cx: &mut ViewContext<Self>) {
- 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<proto::view::Variant> {
- 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<workspace::item::FollowEvent> {
- 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<proto::update_view::Variant>,
- 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<Project>,
- message: update_view::Variant,
- cx: &mut ViewContext<Self>,
- ) -> Task<Result<()>> {
- 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<Editor>,
- project: Model<Project>,
- 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::<HashSet<_>>();
- 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::<Vec<_>>()
- })?;
- 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::<Vec<_>>();
- 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::<Vec<_>>();
- 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<language::Anchor>,
-) -> Option<proto::Excerpt> {
- 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<Anchor>) -> 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<ExcerptRange<language::Anchor>> {
- 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<Selection<Anchor>> {
- 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<Anchor> {
- 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<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
- if let Ok(data) = data.downcast::<NavigationData>() {
- let newest_selection = self.selections.newest::<Point>(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<SharedString> {
- 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<SharedString> {
- let path = path_for_buffer(&self.buffer, detail, true, cx)?;
- Some(path.to_string_lossy().to_string().into())
- }
-
- fn tab_content(&self, detail: Option<usize>, 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<Self>,
- ) -> Option<View<Editor>>
- where
- Self: Sized,
- {
- Some(cx.new_view(|cx| self.clone(cx)))
- }
-
- fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
- self.nav_history = Some(history);
- }
-
- fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
- let selection = self.selections.newest_anchor();
- self.push_to_nav_history(selection.head(), None, cx);
- }
-
- fn workspace_deactivated(&mut self, cx: &mut ViewContext<Self>) {
- 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<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
- 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<Project>,
- abs_path: PathBuf,
- cx: &mut ViewContext<Self>,
- ) -> Task<Result<()>> {
- 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<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
- 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<Self>) -> Option<Box<dyn SearchableItemHandle>> {
- Some(Box::new(handle.clone()))
- }
-
- fn pixel_position_of_cursor(&self, _: &AppContext) -> Option<gpui::Point<Pixels>> {
- self.pixel_position_of_newest_cursor
- }
-
- fn breadcrumb_location(&self) -> ToolbarItemLocation {
- ToolbarItemLocation::PrimaryLeft
- }
-
- fn breadcrumbs(&self, variant: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
- 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<Self>) {
- 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<Buffer>,
- 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<Project>,
- _workspace: WeakView<Workspace>,
- workspace_id: workspace::WorkspaceId,
- item_id: ItemId,
- cx: &mut ViewContext<Pane>,
- ) -> Task<Result<View<Self>>> {
- 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::<Buffer>()
- .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<Project>,
- buffer: Model<Buffer>,
- cx: &mut ViewContext<Self>,
- ) -> Self {
- Self::for_buffer(buffer, Some(project), cx)
- }
-}
-
-impl EventEmitter<SearchEvent> for Editor {}
-
-pub(crate) enum BufferSearchHighlights {}
-impl SearchableItem for Editor {
- type Match = Range<Anchor>;
-
- fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
- self.clear_background_highlights::<BufferSearchHighlights>(cx);
- }
-
- fn update_matches(&mut self, matches: Vec<Range<Anchor>>, cx: &mut ViewContext<Self>) {
- self.highlight_background::<BufferSearchHighlights>(
- matches,
- |theme| theme.search_match_background,
- cx,
- );
- }
-
- fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
- let setting = EditorSettings::get_global(cx).seed_search_query_from_cursor;
- let snapshot = &self.snapshot(cx).buffer_snapshot;
- let selection = self.selections.newest::<usize>(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<Range<Anchor>>,
- cx: &mut ViewContext<Self>,
- ) {
- 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<Self::Match>, cx: &mut ViewContext<Self>) {
- 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<Self>,
- ) {
- let text = self.buffer.read(cx);
- let text = text.snapshot(cx);
- let text = text.text_for_range(identifier.clone()).collect::<Vec<_>>();
- 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<Range<Anchor>>,
- current_index: usize,
- direction: Direction,
- count: usize,
- cx: &mut ViewContext<Self>,
- ) -> 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<project::search::SearchQuery>,
- cx: &mut ViewContext<Self>,
- ) -> Task<Vec<Range<Anchor>>> {
- 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<Range<Anchor>>,
- cx: &mut ViewContext<Self>,
- ) -> Option<usize> {
- active_match_index(
- &matches,
- &self.selections.newest_anchor().head(),
- &self.buffer().read(cx).snapshot(cx),
- )
- }
-}
-
-pub fn active_match_index(
- ranges: &[Range<Anchor>],
- cursor: &Anchor,
- buffer: &MultiBufferSnapshot,
-) -> Option<usize> {
- 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<Point>,
- selected_count: usize,
- _observe_active_editor: Option<Subscription>,
-}
-
-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<Editor>, cx: &mut ViewContext<Self>) {
- let editor = editor.read(cx);
- let buffer = editor.buffer().read(cx).snapshot(cx);
-
- self.selected_count = 0;
- let mut last_selection: Option<Selection<usize>> = None;
- for selection in editor.selections.all::<usize>(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<Self>) -> 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<Self>,
- ) {
- if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(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<MultiBuffer>,
- height: usize,
- include_filename: bool,
- cx: &'a AppContext,
-) -> Option<Cow<'a, Path>> {
- 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<Cow<'a, Path>> {
- // 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<Path>` 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<Path>,
- full_path: PathBuf,
- }
-
- impl language::File for TestFile {
- fn path(&self) -> &Arc<Path> {
- &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!()
- }
- }
-}
@@ -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<TriggerPoint>,
- pub symbol_range: Option<RangeInEditor>,
- pub kind: Option<LinkDefinitionKind>,
- pub definitions: Vec<GoToDefinitionLink>,
- pub task: Option<Task<Option<()>>>,
-}
-
-#[derive(Debug, Eq, PartialEq, Clone)]
-pub enum RangeInEditor {
- Text(Range<Anchor>),
- Inlay(InlayHighlight),
-}
-
-impl RangeInEditor {
- pub fn as_text_range(&self) -> Option<Range<Anchor>> {
- 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<usize>,
-}
-
-#[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<GoToDefinitionTrigger>,
- cmd_held: bool,
- shift_held: bool,
- cx: &mut ViewContext<Editor>,
-) {
- 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<Editor>,
-) {
- 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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(
- vec![text_range],
- style,
- cx,
- ),
- RangeInEditor::Inlay(highlight) => this
- .highlight_inlays::<LinkGoToDefinitionState>(
- 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<Editor>) {
- 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::<LinkGoToDefinitionState>(cx);
-}
-
-pub fn go_to_fetched_definition(
- editor: &mut Editor,
- point: PointForPosition,
- split: bool,
- cx: &mut ViewContext<Editor>,
-) {
- 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<Editor>,
-) {
- 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<Editor>,
-) {
- 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::<GotoTypeDefinition, _, _>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<GotoTypeDefinition, _, _>(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::<GotoDefinition, _, _>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<GotoDefinition, _, _>(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::<LinkGoToDefinitionState>(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::<GotoDefinition, _, _>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<GotoDefinition, _, _>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<LinkGoToDefinitionState>(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::<GotoDefinition, _, _>(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::<LinkGoToDefinitionState>(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::<GotoDefinition, _, _>(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::<GotoDefinition, _, _>(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::<LinkGoToDefinitionState>(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::<lsp::request::InlayHintRequest, _, _>(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::<LinkGoToDefinitionState>()
- .into_iter()
- .flat_map(|highlights| highlights.values().map(|(_, highlight)| highlight))
- .collect::<Vec<_>>();
-
- 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::<LinkGoToDefinitionState>()
- .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;
- }
- "});
- }
-}
@@ -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<Pixels>,
- pub(crate) context_menu: View<ui::ContextMenu>,
- _subscription: Subscription,
-}
-
-pub fn deploy_context_menu(
- editor: &mut Editor,
- position: Point<Pixels>,
- point: DisplayPoint,
- cx: &mut ViewContext<Editor>,
-) {
- 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()));
- }
-}
@@ -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<TextSystem>,
- 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<Item = (char, Range<usize>)> + '_ {
- 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<Item = (char, Range<usize>)> + '_ {
- 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<DisplayPoint> {
- 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<DisplayPoint>,
-) -> Vec<Range<DisplayPoint>> {
- 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);
- }
-}
@@ -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<WorkspaceDb> =
- &[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<Option<PathBuf>> {
- 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<Option<(u32, f32, f32)>> {
- 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
- }
- }
-}
@@ -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<Editor>, 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"
-}
@@ -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<f32>,
- 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<f32> {
- 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<Axis>,
-}
-
-impl OngoingScroll {
- fn new() -> Self {
- Self {
- last_event: Instant::now() - SCROLL_EVENT_SEPARATION,
- axis: None,
- }
- }
-
- pub fn filter(&self, delta: &mut gpui::Point<Pixels>) -> Option<Axis> {
- 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, f32, AutoscrollStrategy)>,
- show_scrollbars: bool,
- hide_scrollbar_task: Option<Task<()>>,
- dragging_scrollbar: bool,
- visible_line_count: Option<f32>,
-}
-
-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<Axis>) {
- self.ongoing.last_event = Instant::now();
- self.ongoing.axis = axis;
- }
-
- pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point<f32> {
- self.anchor.scroll_position(snapshot)
- }
-
- fn set_scroll_position(
- &mut self,
- scroll_position: gpui::Point<f32>,
- map: &DisplaySnapshot,
- local: bool,
- autoscroll: bool,
- workspace_id: Option<i64>,
- cx: &mut ViewContext<Editor>,
- ) {
- 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<i64>,
- cx: &mut ViewContext<Editor>,
- ) {
- 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<Editor>) {
- if !self.show_scrollbars {
- self.show_scrollbars = true;
- cx.notify();
- }
-
- if cx.default_global::<ScrollbarAutoHide>().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<Editor>) {
- 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>) {
- self.scroll_manager.vertical_scroll_margin = margin_rows as f32;
- cx.notify();
- }
-
- pub fn visible_line_count(&self) -> Option<f32> {
- self.scroll_manager.visible_line_count
- }
-
- pub(crate) fn set_visible_line_count(&mut self, lines: f32, cx: &mut ViewContext<Self>) {
- 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<f32>,
- cx: &mut ViewContext<Self>,
- ) {
- self.set_scroll_position_internal(scroll_position, true, false, cx);
- }
-
- pub(crate) fn set_scroll_position_internal(
- &mut self,
- scroll_position: gpui::Point<f32>,
- local: bool,
- autoscroll: bool,
- cx: &mut ViewContext<Self>,
- ) {
- 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<Self>) -> gpui::Point<f32> {
- 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<Self>) {
- 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<Self>,
- ) {
- 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<Self>) {
- 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<Editor>,
- ) {
- 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);
- }
- }
-}
@@ -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<Editor>) {
- 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<f32>,
- axis: Option<Axis>,
- cx: &mut ViewContext<Self>,
- ) {
- 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<Editor>) {
- 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<Editor>) {
- 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<Editor>) {
- 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,
- )
- }
-}
@@ -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<Editor>,
- ) -> 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::<Point>(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<Self>,
- ) -> bool {
- let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
- let selections = self.selections.all::<Point>(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>) {
- self.scroll_manager.autoscroll_request = Some((autoscroll, true));
- cx.notify();
- }
-
- pub(crate) fn request_autoscroll_remotely(
- &mut self,
- autoscroll: Autoscroll,
- cx: &mut ViewContext<Self>,
- ) {
- self.scroll_manager.autoscroll_request = Some((autoscroll, false));
- cx.notify();
- }
-}
@@ -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.),
- }
- }
-}
@@ -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<Anchor>,
- pub mode: SelectMode,
-}
-
-#[derive(Debug, Clone)]
-pub struct SelectionsCollection {
- display_map: Model<DisplayMap>,
- buffer: Model<MultiBuffer>,
- pub next_selection_id: usize,
- pub line_mode: bool,
- disjoint: Arc<[Selection<Anchor>]>,
- pending: Option<PendingSelection>,
-}
-
-impl SelectionsCollection {
- pub fn new(display_map: Model<DisplayMap>, buffer: Model<MultiBuffer>) -> 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<Anchor>]> {
- self.disjoint.clone()
- }
-
- pub fn pending_anchor(&self) -> Option<Selection<Anchor>> {
- self.pending
- .as_ref()
- .map(|pending| pending.selection.clone())
- }
-
- pub fn pending<D: TextDimension + Ord + Sub<D, Output = D>>(
- &self,
- cx: &AppContext,
- ) -> Option<Selection<D>> {
- self.pending_anchor()
- .as_ref()
- .map(|pending| pending.map(|p| p.summary::<D>(&self.buffer(cx))))
- }
-
- pub fn pending_mode(&self) -> Option<SelectMode> {
- self.pending.as_ref().map(|pending| pending.mode.clone())
- }
-
- pub fn all<'a, D>(&self, cx: &AppContext) -> Vec<Selection<D>>
- where
- D: 'a + TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
- {
- let disjoint_anchors = &self.disjoint;
- let mut disjoint =
- resolve_multiple::<D, _>(disjoint_anchors.iter(), &self.buffer(cx)).peekable();
-
- let mut pending_opt = self.pending::<D>(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<Selection<Point>> {
- let mut selections = self.all::<Point>(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<Selection<DisplayPoint>>) {
- if self.line_mode {
- let selections = self.all::<Point>(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<Anchor>,
- cx: &AppContext,
- ) -> Vec<Selection<D>>
- where
- D: 'a + TextDimension + Ord + Sub<D, Output = D> + 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<Selection<DisplayPoint>>) {
- let display_map = self.display_map(cx);
- let selections = self
- .all::<Point>(cx)
- .into_iter()
- .map(|selection| selection.map(|point| point.to_display_point(&display_map)))
- .collect();
- (display_map, selections)
- }
-
- pub fn newest_anchor(&self) -> &Selection<Anchor> {
- self.pending
- .as_ref()
- .map(|s| &s.selection)
- .or_else(|| self.disjoint.iter().max_by_key(|s| s.id))
- .unwrap()
- }
-
- pub fn newest<D: TextDimension + Ord + Sub<D, Output = D>>(
- &self,
- cx: &AppContext,
- ) -> Selection<D> {
- resolve(self.newest_anchor(), &self.buffer(cx))
- }
-
- pub fn newest_display(&self, cx: &mut AppContext) -> Selection<DisplayPoint> {
- 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<Anchor> {
- self.disjoint
- .iter()
- .min_by_key(|s| s.id)
- .or_else(|| self.pending.as_ref().map(|p| &p.selection))
- .unwrap()
- }
-
- pub fn oldest<D: TextDimension + Ord + Sub<D, Output = D>>(
- &self,
- cx: &AppContext,
- ) -> Selection<D> {
- resolve(self.oldest_anchor(), &self.buffer(cx))
- }
-
- pub fn first_anchor(&self) -> Selection<Anchor> {
- self.disjoint[0].clone()
- }
-
- pub fn first<D: TextDimension + Ord + Sub<D, Output = D>>(
- &self,
- cx: &AppContext,
- ) -> Selection<D> {
- self.all(cx).first().unwrap().clone()
- }
-
- pub fn last<D: TextDimension + Ord + Sub<D, Output = D>>(
- &self,
- cx: &AppContext,
- ) -> Selection<D> {
- self.all(cx).last().unwrap().clone()
- }
-
- #[cfg(any(test, feature = "test-support"))]
- pub fn ranges<D: TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug>(
- &self,
- cx: &AppContext,
- ) -> Vec<Range<D>> {
- self.all::<D>(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<Range<DisplayPoint>> {
- 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<Pixels>,
- reversed: bool,
- text_layout_details: &TextLayoutDetails,
- ) -> Option<Selection<Point>> {
- 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<R>(
- &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<MultiBufferSnapshot> {
- 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<Anchor>, 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<DisplayPoint>, 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<Anchor>, 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<T>(&mut self, range: Range<T>)
- where
- T: 'a + ToOffset + ToPoint + TextDimension + Ord + Sub<T, Output = T> + 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<T>(&mut self, mut selections: Vec<Selection<T>>)
- 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<Selection<Anchor>>) {
- let buffer = self.buffer.read(self.cx).snapshot(self.cx);
- let resolved_selections =
- resolve_multiple::<usize, _>(&selections, &buffer).collect::<Vec<_>>();
- self.select(resolved_selections);
- }
-
- pub fn select_ranges<I, T>(&mut self, ranges: I)
- where
- I: IntoIterator<Item = Range<T>>,
- 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<I>(&mut self, ranges: I)
- where
- I: IntoIterator<Item = Range<usize>>,
- {
- 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::<Vec<_>>();
-
- self.select(selections)
- }
-
- pub fn select_anchor_ranges<I>(&mut self, ranges: I)
- where
- I: IntoIterator<Item = Range<Anchor>>,
- {
- 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::<Vec<_>>();
- self.select_anchors(selections)
- }
-
- pub fn new_selection_id(&mut self) -> usize {
- post_inc(&mut self.next_selection_id)
- }
-
- pub fn select_display_ranges<T>(&mut self, ranges: T)
- where
- T: IntoIterator<Item = Range<DisplayPoint>>,
- {
- 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<DisplayPoint>),
- ) {
- let mut changed = false;
- let display_map = self.display_map();
- let selections = self
- .all::<Point>(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<usize>),
- ) {
- let mut changed = false;
- let snapshot = self.buffer().clone();
- let selections = self
- .all::<usize>(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<DisplayPoint>,
- ) {
- 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<usize, ExcerptId> {
- 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::<usize>(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<Item = Selection<D>>
-where
- D: TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
- I: 'a + IntoIterator<Item = &'a Selection<Anchor>>,
-{
- let (to_summarize, selections) = selections.into_iter().tee();
- let mut summaries = snapshot
- .summaries_for_anchors::<D, _>(
- to_summarize
- .flat_map(|s| [&s.start, &s.end])
- .collect::<Vec<_>>(),
- )
- .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<D: TextDimension + Ord + Sub<D, Output = D>>(
- selection: &Selection<Anchor>,
- buffer: &MultiBufferSnapshot,
-) -> Selection<D> {
- selection.map(|p| p.summary::<D>(buffer))
-}
@@ -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<DisplayPoint>) {
- 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<Editor>) {
- 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<Editor>,
-) {
- 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<MultiBuffer>, cx: &mut ViewContext<Editor>) -> Editor {
- // todo!()
- Editor::new(EditorMode::Full, buffer, None, /*None,*/ cx)
-}
-
-pub(crate) fn build_editor_with_project(
- project: Model<Project>,
- buffer: Model<MultiBuffer>,
- cx: &mut ViewContext<Editor>,
-) -> Editor {
- // todo!()
- Editor::new(EditorMode::Full, buffer, Some(project), /*None,*/ cx)
-}
@@ -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<Workspace>,
- 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::<Editor>(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<char> = 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<usize>) -> 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<F, T>(&mut self, update: F) -> T
- where
- F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
- {
- self.workspace.update(&mut self.cx.cx, update)
- }
-
- pub fn handle_request<T, F, Fut>(
- &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<Output = Result<T::Result>>,
- {
- let url = self.buffer_lsp_url.clone();
- self.lsp.handle_request::<T, _, _>(move |params, cx| {
- let url = url.clone();
- handler(url, params, cx)
- })
- }
-
- pub fn notify<T: notification::Notification>(&self, params: T::Params) {
- self.lsp.notify::<T>(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
- }
-}
@@ -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<Editor>,
- 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<Output = ()> {
- self.editor
- .condition::<crate::EditorEvent>(&self.cx, predicate)
- }
-
- #[track_caller]
- pub fn editor<F, T>(&mut self, read: F) -> T
- where
- F: FnOnce(&Editor, &ViewContext<Editor>) -> T,
- {
- self.editor
- .update(&mut self.cx, |this, cx| read(&this, &cx))
- }
-
- #[track_caller]
- pub fn update_editor<F, T>(&mut self, update: F) -> T
- where
- F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
- {
- self.editor.update(&mut self.cx, update)
- }
-
- pub fn multibuffer<F, T>(&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<F, T>(&mut self, update: F) -> T
- where
- F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> 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<F, T>(&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<F, T>(&mut self, update: F) -> T
- where
- F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> 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<const COUNT: usize>(
- &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<Range<usize>> {
- 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<language::Anchor> {
- 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<Tag: 'static>(&mut self, marked_text: &str) {
- let expected_ranges = self.ranges(marked_text);
- let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
- let snapshot = editor.snapshot(cx);
- editor
- .background_highlights
- .get(&TypeId::of::<Tag>())
- .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<Tag: ?Sized + 'static>(&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<Range<usize>> = snapshot
- .text_highlight_ranges::<Tag>()
- .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<Range<usize>>) {
- 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<Range<usize>> {
- self.editor
- .update(&mut self.cx, |editor, cx| {
- editor.selections.all::<usize>(cx)
- })
- .into_iter()
- .map(|s| {
- if s.reversed {
- s.end..s.start
- } else {
- s.start..s.end
- }
- })
- .collect::<Vec<_>>()
- }
-
- #[track_caller]
- fn assert_selections(
- &mut self,
- expected_selections: Vec<Range<usize>>,
- 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<AtomicUsize>,
- contexts: Arc<RwLock<BTreeMap<usize, String>>>,
-}
-
-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);
- }
-}
@@ -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"] }
@@ -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"] }
@@ -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"] }
@@ -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"] }
@@ -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"] }
@@ -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
@@ -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"] }
@@ -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
@@ -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
@@ -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"] }
@@ -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"] }
@@ -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"] }
@@ -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"] }
@@ -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
@@ -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"]}
@@ -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"] }
@@ -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"] }
@@ -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"] }
@@ -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" }