mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-24 06:12:25 +03:00
Remove 2 suffix for editor
Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
parent
bcad3a5847
commit
588976d27a
108
Cargo.lock
generated
108
Cargo.lock
generated
@ -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));
|
||||
editor.update(cx, |editor, cx| editor.text_layout_details(cx));
|
||||
|
||||
let font_cache = cx.font_cache().clone();
|
||||
|
||||
let family_id = font_cache
|
||||
.load_family(&["Helvetica"], &Default::default())
|
||||
.unwrap();
|
||||
let font_id = font_cache
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
let font_size = 12.0;
|
||||
let wrap_width = Some(64.);
|
||||
let font_size = px(12.0);
|
||||
let wrap_width = Some(px(64.));
|
||||
|
||||
let text = "one two three four five\nsix seven eight";
|
||||
let buffer = MultiBuffer::build_simple(text, cx);
|
||||
let map = cx.add_model(|cx| {
|
||||
DisplayMap::new(buffer.clone(), font_id, font_size, wrap_width, 1, 1, cx)
|
||||
let map = cx.new_model(|cx| {
|
||||
DisplayMap::new(
|
||||
buffer.clone(),
|
||||
font("Helvetica"),
|
||||
font_size,
|
||||
wrap_width,
|
||||
1,
|
||||
1,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
|
||||
@ -1347,7 +1320,7 @@ pub mod tests {
|
||||
DisplayPoint::new(0, 7)
|
||||
);
|
||||
|
||||
let x = snapshot.x_for_point(DisplayPoint::new(1, 10), &text_layout_details);
|
||||
let x = snapshot.x_for_display_point(DisplayPoint::new(1, 10), &text_layout_details);
|
||||
assert_eq!(
|
||||
movement::up(
|
||||
&snapshot,
|
||||
@ -1358,33 +1331,33 @@ pub mod tests {
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(0, 7),
|
||||
SelectionGoal::HorizontalPosition(x)
|
||||
SelectionGoal::HorizontalPosition(x.0)
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
movement::down(
|
||||
&snapshot,
|
||||
DisplayPoint::new(0, 7),
|
||||
SelectionGoal::HorizontalPosition(x),
|
||||
SelectionGoal::HorizontalPosition(x.0),
|
||||
false,
|
||||
&text_layout_details
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(1, 10),
|
||||
SelectionGoal::HorizontalPosition(x)
|
||||
SelectionGoal::HorizontalPosition(x.0)
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
movement::down(
|
||||
&snapshot,
|
||||
DisplayPoint::new(1, 10),
|
||||
SelectionGoal::HorizontalPosition(x),
|
||||
SelectionGoal::HorizontalPosition(x.0),
|
||||
false,
|
||||
&text_layout_details
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(2, 4),
|
||||
SelectionGoal::HorizontalPosition(x)
|
||||
SelectionGoal::HorizontalPosition(x.0)
|
||||
)
|
||||
);
|
||||
|
||||
@ -1400,7 +1373,9 @@ pub mod tests {
|
||||
);
|
||||
|
||||
// Re-wrap on font size changes
|
||||
map.update(cx, |map, cx| map.set_font(font_id, font_size + 3., cx));
|
||||
map.update(cx, |map, cx| {
|
||||
map.set_font(font("Helvetica"), px(font_size.0 + 3.), cx)
|
||||
});
|
||||
|
||||
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
|
||||
assert_eq!(
|
||||
@ -1416,17 +1391,11 @@ pub mod tests {
|
||||
|
||||
let text = sample_text(6, 6, 'a');
|
||||
let buffer = MultiBuffer::build_simple(&text, cx);
|
||||
let family_id = cx
|
||||
.font_cache()
|
||||
.load_family(&["Helvetica"], &Default::default())
|
||||
.unwrap();
|
||||
let font_id = cx
|
||||
.font_cache()
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
let font_size = 14.0;
|
||||
let map =
|
||||
cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
|
||||
|
||||
let font_size = px(14.0);
|
||||
let map = cx.new_model(|cx| {
|
||||
DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx)
|
||||
});
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
@ -1470,9 +1439,9 @@ pub mod tests {
|
||||
}"#
|
||||
.unindent();
|
||||
|
||||
let theme = SyntaxTheme::new(vec![
|
||||
("mod.body".to_string(), Color::red().into()),
|
||||
("fn.name".to_string(), Color::blue().into()),
|
||||
let theme = SyntaxTheme::new_test(vec![
|
||||
("mod.body", Hsla::red().into()),
|
||||
("fn.name", Hsla::blue().into()),
|
||||
]);
|
||||
let language = Arc::new(
|
||||
Language::new(
|
||||
@ -1495,38 +1464,33 @@ pub mod tests {
|
||||
|
||||
cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap())));
|
||||
|
||||
let buffer = cx
|
||||
.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
|
||||
buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
|
||||
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let buffer = cx.new_model(|cx| {
|
||||
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
||||
});
|
||||
cx.condition(&buffer, |buf, _| !buf.is_parsing()).await;
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
|
||||
let font_cache = cx.font_cache();
|
||||
let family_id = font_cache
|
||||
.load_family(&["Helvetica"], &Default::default())
|
||||
.unwrap();
|
||||
let font_id = font_cache
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
let font_size = 14.0;
|
||||
let font_size = px(14.0);
|
||||
|
||||
let map = cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
|
||||
let map = cx
|
||||
.new_model(|cx| DisplayMap::new(buffer, font("Helvetica"), font_size, None, 1, 1, cx));
|
||||
assert_eq!(
|
||||
cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
|
||||
vec![
|
||||
("fn ".to_string(), None),
|
||||
("outer".to_string(), Some(Color::blue())),
|
||||
("outer".to_string(), Some(Hsla::blue())),
|
||||
("() {}\n\nmod module ".to_string(), None),
|
||||
("{\n fn ".to_string(), Some(Color::red())),
|
||||
("inner".to_string(), Some(Color::blue())),
|
||||
("() {}\n}".to_string(), Some(Color::red())),
|
||||
("{\n fn ".to_string(), Some(Hsla::red())),
|
||||
("inner".to_string(), Some(Hsla::blue())),
|
||||
("() {}\n}".to_string(), Some(Hsla::red())),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)),
|
||||
vec![
|
||||
(" fn ".to_string(), Some(Color::red())),
|
||||
("inner".to_string(), Some(Color::blue())),
|
||||
("() {}\n}".to_string(), Some(Color::red())),
|
||||
(" fn ".to_string(), Some(Hsla::red())),
|
||||
("inner".to_string(), Some(Hsla::blue())),
|
||||
("() {}\n}".to_string(), Some(Hsla::red())),
|
||||
]
|
||||
);
|
||||
|
||||
@ -1537,11 +1501,11 @@ pub mod tests {
|
||||
cx.update(|cx| syntax_chunks(0..2, &map, &theme, cx)),
|
||||
vec![
|
||||
("fn ".to_string(), None),
|
||||
("out".to_string(), Some(Color::blue())),
|
||||
("out".to_string(), Some(Hsla::blue())),
|
||||
("⋯".to_string(), None),
|
||||
(" fn ".to_string(), Some(Color::red())),
|
||||
("inner".to_string(), Some(Color::blue())),
|
||||
("() {}\n}".to_string(), Some(Color::red())),
|
||||
(" fn ".to_string(), Some(Hsla::red())),
|
||||
("inner".to_string(), Some(Hsla::blue())),
|
||||
("() {}\n}".to_string(), Some(Hsla::red())),
|
||||
]
|
||||
);
|
||||
}
|
||||
@ -1550,7 +1514,8 @@ pub mod tests {
|
||||
async fn test_chunks_with_soft_wrapping(cx: &mut gpui::TestAppContext) {
|
||||
use unindent::Unindent as _;
|
||||
|
||||
cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
|
||||
cx.background_executor
|
||||
.set_block_on_ticks(usize::MAX..=usize::MAX);
|
||||
|
||||
let text = r#"
|
||||
fn outer() {}
|
||||
@ -1560,9 +1525,9 @@ pub mod tests {
|
||||
}"#
|
||||
.unindent();
|
||||
|
||||
let theme = SyntaxTheme::new(vec![
|
||||
("mod.body".to_string(), Color::red().into()),
|
||||
("fn.name".to_string(), Color::blue().into()),
|
||||
let theme = SyntaxTheme::new_test(vec![
|
||||
("mod.body", Hsla::red().into()),
|
||||
("fn.name", Hsla::blue().into()),
|
||||
]);
|
||||
let language = Arc::new(
|
||||
Language::new(
|
||||
@ -1585,28 +1550,22 @@ pub mod tests {
|
||||
|
||||
cx.update(|cx| init_test(cx, |_| {}));
|
||||
|
||||
let buffer = cx
|
||||
.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
|
||||
buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
|
||||
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let buffer = cx.new_model(|cx| {
|
||||
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
||||
});
|
||||
cx.condition(&buffer, |buf, _| !buf.is_parsing()).await;
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
|
||||
let font_cache = cx.font_cache();
|
||||
let font_size = px(16.0);
|
||||
|
||||
let family_id = font_cache
|
||||
.load_family(&["Courier"], &Default::default())
|
||||
.unwrap();
|
||||
let font_id = font_cache
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
let font_size = 16.0;
|
||||
|
||||
let map =
|
||||
cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, Some(40.0), 1, 1, cx));
|
||||
let map = cx.new_model(|cx| {
|
||||
DisplayMap::new(buffer, font("Courier"), font_size, Some(px(40.0)), 1, 1, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
|
||||
[
|
||||
("fn \n".to_string(), None),
|
||||
("oute\nr".to_string(), Some(Color::blue())),
|
||||
("oute\nr".to_string(), Some(Hsla::blue())),
|
||||
("() \n{}\n\n".to_string(), None),
|
||||
]
|
||||
);
|
||||
@ -1621,10 +1580,10 @@ pub mod tests {
|
||||
assert_eq!(
|
||||
cx.update(|cx| syntax_chunks(1..4, &map, &theme, cx)),
|
||||
[
|
||||
("out".to_string(), Some(Color::blue())),
|
||||
("out".to_string(), Some(Hsla::blue())),
|
||||
("⋯\n".to_string(), None),
|
||||
(" \nfn ".to_string(), Some(Color::red())),
|
||||
("i\n".to_string(), Some(Color::blue()))
|
||||
(" \nfn ".to_string(), Some(Hsla::red())),
|
||||
("i\n".to_string(), Some(Hsla::blue()))
|
||||
]
|
||||
);
|
||||
}
|
||||
@ -1633,9 +1592,9 @@ pub mod tests {
|
||||
async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) {
|
||||
cx.update(|cx| init_test(cx, |_| {}));
|
||||
|
||||
let theme = SyntaxTheme::new(vec![
|
||||
("operator".to_string(), Color::red().into()),
|
||||
("string".to_string(), Color::green().into()),
|
||||
let theme = SyntaxTheme::new_test(vec![
|
||||
("operator", Hsla::red().into()),
|
||||
("string", Hsla::green().into()),
|
||||
]);
|
||||
let language = Arc::new(
|
||||
Language::new(
|
||||
@ -1658,27 +1617,22 @@ pub mod tests {
|
||||
|
||||
let (text, highlighted_ranges) = marked_text_ranges(r#"constˇ «a»: B = "c «d»""#, false);
|
||||
|
||||
let buffer = cx
|
||||
.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
|
||||
buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
|
||||
let buffer = cx.new_model(|cx| {
|
||||
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
||||
});
|
||||
cx.condition(&buffer, |buf, _| !buf.is_parsing()).await;
|
||||
|
||||
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
|
||||
|
||||
let font_cache = cx.font_cache();
|
||||
let family_id = font_cache
|
||||
.load_family(&["Courier"], &Default::default())
|
||||
.unwrap();
|
||||
let font_id = font_cache
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
let font_size = 16.0;
|
||||
let map = cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
|
||||
let font_size = px(16.0);
|
||||
let map =
|
||||
cx.new_model(|cx| DisplayMap::new(buffer, font("Courier"), font_size, None, 1, 1, cx));
|
||||
|
||||
enum MyType {}
|
||||
|
||||
let style = HighlightStyle {
|
||||
color: Some(Color::blue()),
|
||||
color: Some(Hsla::blue()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@ -1700,12 +1654,12 @@ pub mod tests {
|
||||
cx.update(|cx| chunks(0..10, &map, &theme, cx)),
|
||||
[
|
||||
("const ".to_string(), None, None),
|
||||
("a".to_string(), None, Some(Color::blue())),
|
||||
(":".to_string(), Some(Color::red()), None),
|
||||
("a".to_string(), None, Some(Hsla::blue())),
|
||||
(":".to_string(), Some(Hsla::red()), None),
|
||||
(" B = ".to_string(), None, None),
|
||||
("\"c ".to_string(), Some(Color::green()), None),
|
||||
("d".to_string(), Some(Color::green()), Some(Color::blue())),
|
||||
("\"".to_string(), Some(Color::green()), None),
|
||||
("\"c ".to_string(), Some(Hsla::green()), None),
|
||||
("d".to_string(), Some(Hsla::green()), Some(Hsla::blue())),
|
||||
("\"".to_string(), Some(Hsla::green()), None),
|
||||
]
|
||||
);
|
||||
}
|
||||
@ -1785,17 +1739,11 @@ pub mod tests {
|
||||
|
||||
let text = "✅\t\tα\nβ\t\n🏀β\t\tγ";
|
||||
let buffer = MultiBuffer::build_simple(text, cx);
|
||||
let font_cache = cx.font_cache();
|
||||
let family_id = font_cache
|
||||
.load_family(&["Helvetica"], &Default::default())
|
||||
.unwrap();
|
||||
let font_id = font_cache
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
let font_size = 14.0;
|
||||
let font_size = px(14.0);
|
||||
|
||||
let map =
|
||||
cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
|
||||
let map = cx.new_model(|cx| {
|
||||
DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx)
|
||||
});
|
||||
let map = map.update(cx, |map, cx| map.snapshot(cx));
|
||||
assert_eq!(map.text(), "✅ α\nβ \n🏀β γ");
|
||||
assert_eq!(
|
||||
@ -1846,16 +1794,10 @@ pub mod tests {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
|
||||
let font_cache = cx.font_cache();
|
||||
let family_id = font_cache
|
||||
.load_family(&["Helvetica"], &Default::default())
|
||||
.unwrap();
|
||||
let font_id = font_cache
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
let font_size = 14.0;
|
||||
let map =
|
||||
cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
|
||||
let font_size = px(14.0);
|
||||
let map = cx.new_model(|cx| {
|
||||
DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
map.update(cx, |map, cx| map.snapshot(cx)).max_point(),
|
||||
DisplayPoint::new(1, 11)
|
||||
@ -1864,10 +1806,10 @@ pub mod tests {
|
||||
|
||||
fn syntax_chunks<'a>(
|
||||
rows: Range<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);
|
||||
fn test_blocks_on_wrapped_lines(cx: &mut gpui::TestAppContext) {
|
||||
cx.update(|cx| init_test(cx));
|
||||
|
||||
let family_id = cx
|
||||
.font_cache()
|
||||
.load_family(&["Helvetica"], &Default::default())
|
||||
.unwrap();
|
||||
let font_id = cx
|
||||
.font_cache()
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
let _font_id = cx.text_system().font_id(&font("Helvetica")).unwrap();
|
||||
|
||||
let text = "one two three\nfour five six\nseven eight";
|
||||
|
||||
let buffer = MultiBuffer::build_simple(text, cx);
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
|
||||
let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
|
||||
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
|
||||
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
|
||||
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
|
||||
let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, Some(60.), cx);
|
||||
let (_, wraps_snapshot) = cx.update(|cx| {
|
||||
WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), Some(px(60.)), cx)
|
||||
});
|
||||
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
|
||||
|
||||
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
|
||||
@ -1218,14 +1205,14 @@ mod tests {
|
||||
style: BlockStyle::Fixed,
|
||||
position: buffer_snapshot.anchor_after(Point::new(1, 12)),
|
||||
disposition: BlockDisposition::Above,
|
||||
render: Arc::new(|_| Empty::new().into_any_named("block 1")),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
height: 1,
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
position: buffer_snapshot.anchor_after(Point::new(1, 1)),
|
||||
disposition: BlockDisposition::Below,
|
||||
render: Arc::new(|_| Empty::new().into_any_named("block 2")),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
height: 1,
|
||||
},
|
||||
]);
|
||||
@ -1240,8 +1227,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_blocks(cx: &mut gpui::AppContext, mut rng: StdRng) {
|
||||
init_test(cx);
|
||||
fn test_random_blocks(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
|
||||
cx.update(|cx| init_test(cx));
|
||||
|
||||
let operations = env::var("OPERATIONS")
|
||||
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||||
@ -1250,18 +1237,10 @@ mod tests {
|
||||
let wrap_width = if rng.gen_bool(0.2) {
|
||||
None
|
||||
} else {
|
||||
Some(rng.gen_range(0.0..=100.0))
|
||||
Some(px(rng.gen_range(0.0..=100.0)))
|
||||
};
|
||||
let tab_size = 1.try_into().unwrap();
|
||||
let family_id = cx
|
||||
.font_cache()
|
||||
.load_family(&["Helvetica"], &Default::default())
|
||||
.unwrap();
|
||||
let font_id = cx
|
||||
.font_cache()
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
let font_size = 14.0;
|
||||
let font_size = px(14.0);
|
||||
let buffer_start_header_height = rng.gen_range(1..=5);
|
||||
let excerpt_header_height = rng.gen_range(1..=5);
|
||||
|
||||
@ -1272,17 +1251,17 @@ mod tests {
|
||||
let len = rng.gen_range(0..10);
|
||||
let text = RandomCharIter::new(&mut rng).take(len).collect::<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 Default for Fold {
|
||||
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())
|
||||
}
|
||||
@ -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 {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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 diagnostic_colors = match self.local_diagnostic.diagnostic.severity {
|
||||
DiagnosticSeverity::ERROR => DiagnosticColors {
|
||||
text: style.status.error,
|
||||
background: style.status.error_background,
|
||||
border: style.status.error_border,
|
||||
},
|
||||
DiagnosticSeverity::WARNING => DiagnosticColors {
|
||||
text: style.status.warning,
|
||||
background: style.status.warning_background,
|
||||
border: style.status.warning_border,
|
||||
},
|
||||
DiagnosticSeverity::INFORMATION => DiagnosticColors {
|
||||
text: style.status.info,
|
||||
background: style.status.info_background,
|
||||
border: style.status.info_border,
|
||||
},
|
||||
DiagnosticSeverity::HINT => DiagnosticColors {
|
||||
text: style.status.hint,
|
||||
background: style.status.hint_background,
|
||||
border: style.status.hint_border,
|
||||
},
|
||||
_ => DiagnosticColors {
|
||||
text: style.status.ignored,
|
||||
background: style.status.ignored_background,
|
||||
border: style.status.ignored_border,
|
||||
},
|
||||
};
|
||||
|
||||
let tooltip_style = theme::current(cx).tooltip.clone();
|
||||
|
||||
MouseEventHandler::new::<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![HoverBlock {
|
||||
text: "one [two](https://the-url) three".to_string(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "one «two» three".to_string(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
underline: Some(Underline {
|
||||
thickness: 1.0.into(),
|
||||
let rows = &[
|
||||
// Strong emphasis
|
||||
Row {
|
||||
blocks: vec![HoverBlock {
|
||||
text: "one **two** three".to_string(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "one «two» three".to_string(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
font_weight: Some(FontWeight::BOLD),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
// Lists
|
||||
Row {
|
||||
blocks: vec![HoverBlock {
|
||||
text: "
|
||||
}],
|
||||
},
|
||||
// Links
|
||||
Row {
|
||||
blocks: vec![HoverBlock {
|
||||
text: "one [two](https://the-url) three".to_string(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "one «two» three".to_string(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
underline: Some(UnderlineStyle {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
// Lists
|
||||
Row {
|
||||
blocks: vec![HoverBlock {
|
||||
text: "
|
||||
lists:
|
||||
* one
|
||||
- a
|
||||
@ -934,10 +951,10 @@ mod tests {
|
||||
* two
|
||||
- [c](https://the-url)
|
||||
- d"
|
||||
.unindent(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "
|
||||
.unindent(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "
|
||||
lists:
|
||||
- one
|
||||
- a
|
||||
@ -945,19 +962,19 @@ mod tests {
|
||||
- two
|
||||
- «c»
|
||||
- d"
|
||||
.unindent(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
underline: Some(Underline {
|
||||
thickness: 1.0.into(),
|
||||
.unindent(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
underline: Some(UnderlineStyle {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
// Multi-paragraph list items
|
||||
Row {
|
||||
blocks: vec![HoverBlock {
|
||||
text: "
|
||||
}],
|
||||
},
|
||||
// Multi-paragraph list items
|
||||
Row {
|
||||
blocks: vec![HoverBlock {
|
||||
text: "
|
||||
* one two
|
||||
three
|
||||
|
||||
@ -968,10 +985,10 @@ mod tests {
|
||||
nine
|
||||
* ten
|
||||
* six"
|
||||
.unindent(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "
|
||||
.unindent(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "
|
||||
- one two three
|
||||
- four five
|
||||
- six seven eight
|
||||
@ -979,52 +996,51 @@ mod tests {
|
||||
nine
|
||||
- ten
|
||||
- six"
|
||||
.unindent(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
underline: Some(Underline {
|
||||
thickness: 1.0.into(),
|
||||
.unindent(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
underline: Some(UnderlineStyle {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
];
|
||||
}],
|
||||
},
|
||||
];
|
||||
|
||||
for Row {
|
||||
blocks,
|
||||
expected_marked_text,
|
||||
expected_styles,
|
||||
} in &rows[0..]
|
||||
{
|
||||
let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
|
||||
for Row {
|
||||
blocks,
|
||||
expected_marked_text,
|
||||
expected_styles,
|
||||
} in &rows[0..]
|
||||
{
|
||||
let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
|
||||
|
||||
let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
|
||||
let expected_highlights = ranges
|
||||
.into_iter()
|
||||
.zip(expected_styles.iter().cloned())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
rendered.text, expected_text,
|
||||
"wrong text for input {blocks:?}"
|
||||
);
|
||||
let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
|
||||
let expected_highlights = ranges
|
||||
.into_iter()
|
||||
.zip(expected_styles.iter().cloned())
|
||||
.collect::<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();
|
||||
let rendered_highlights: Vec<_> = rendered
|
||||
.highlights
|
||||
.iter()
|
||||
.filter_map(|(range, highlight)| {
|
||||
let highlight = highlight.to_highlight_style(&style.syntax)?;
|
||||
Some((range.clone(), highlight))
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
rendered_highlights, expected_highlights,
|
||||
"wrong highlights for input {blocks:?}"
|
||||
);
|
||||
}
|
||||
|
||||
editor
|
||||
});
|
||||
assert_eq!(
|
||||
rendered_highlights, expected_highlights,
|
||||
"wrong highlights for input {blocks:?}"
|
||||
);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@ -1127,7 +1143,7 @@ mod tests {
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
cx.foreground().run_until_parked();
|
||||
cx.background_executor.run_until_parked();
|
||||
cx.update_editor(|editor, cx| {
|
||||
let expected_layers = vec![entire_hint_label.to_string()];
|
||||
assert_eq!(expected_layers, cached_hint_labels(editor));
|
||||
@ -1236,7 +1252,7 @@ mod tests {
|
||||
)
|
||||
.next()
|
||||
.await;
|
||||
cx.foreground().run_until_parked();
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
update_inlay_link_and_hover_points(
|
||||
@ -1248,9 +1264,9 @@ mod tests {
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.foreground()
|
||||
cx.background_executor
|
||||
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
|
||||
cx.foreground().run_until_parked();
|
||||
cx.background_executor.run_until_parked();
|
||||
cx.update_editor(|editor, cx| {
|
||||
let hover_state = &editor.hover_state;
|
||||
assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
|
||||
@ -1301,9 +1317,9 @@ mod tests {
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.foreground()
|
||||
cx.background_executor
|
||||
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
|
||||
cx.foreground().run_until_parked();
|
||||
cx.background_executor.run_until_parked();
|
||||
cx.update_editor(|editor, cx| {
|
||||
let hover_state = &editor.hover_state;
|
||||
assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,16 +1,15 @@
|
||||
use crate::{
|
||||
editor_settings::SeedQuerySetting, link_go_to_definition::hide_link_definition,
|
||||
persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, EditorSettings, Event,
|
||||
persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, EditorEvent, EditorSettings,
|
||||
ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::HashSet;
|
||||
use futures::future::try_join_all;
|
||||
use gpui::{
|
||||
elements::*,
|
||||
geometry::vector::{vec2f, Vector2F},
|
||||
AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View, ViewContext,
|
||||
ViewHandle, WeakViewHandle,
|
||||
div, point, AnyElement, AppContext, AsyncWindowContext, Context, Entity, EntityId,
|
||||
EventEmitter, IntoElement, Model, ParentElement, Pixels, Render, SharedString, Styled,
|
||||
Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
|
||||
};
|
||||
use language::{
|
||||
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt,
|
||||
@ -18,27 +17,29 @@ use language::{
|
||||
};
|
||||
use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
|
||||
use rpc::proto::{self, update_view, PeerId};
|
||||
use smallvec::SmallVec;
|
||||
use settings::Settings;
|
||||
|
||||
use std::fmt::Write;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp::{self, Ordering},
|
||||
fmt::Write,
|
||||
iter,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use text::Selection;
|
||||
use util::{
|
||||
paths::{PathExt, FILE_ROW_COLUMN_DELIMITER},
|
||||
ResultExt, TryFutureExt,
|
||||
};
|
||||
use workspace::item::{BreadcrumbText, FollowableItemHandle, ItemHandle};
|
||||
use theme::{ActiveTheme, Theme};
|
||||
use ui::{h_stack, prelude::*, Label};
|
||||
use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
item::{FollowableItem, Item, ItemEvent, ProjectItem},
|
||||
item::{BreadcrumbText, FollowEvent, FollowableItemHandle},
|
||||
StatusItemView,
|
||||
};
|
||||
use workspace::{
|
||||
item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
|
||||
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
|
||||
ItemId, ItemNavHistory, Pane, StatusItemView, ToolbarItemLocation, ViewId, Workspace,
|
||||
WorkspaceId,
|
||||
ItemId, ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
|
||||
};
|
||||
|
||||
pub const MAX_TAB_TITLE_LEN: usize = 24;
|
||||
@ -49,12 +50,12 @@ impl FollowableItem for Editor {
|
||||
}
|
||||
|
||||
fn from_state_proto(
|
||||
pane: ViewHandle<workspace::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
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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})"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
}
|
||||
"#});
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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" }
|
||||
|
Loading…
Reference in New Issue
Block a user