Remove 2 suffix for editor

Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-01-03 10:58:57 -08:00
parent bcad3a5847
commit 588976d27a
85 changed files with 6483 additions and 41017 deletions

108
Cargo.lock generated
View File

@ -8,7 +8,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"auto_update", "auto_update",
"editor2", "editor",
"futures 0.3.28", "futures 0.3.28",
"gpui2", "gpui2",
"language2", "language2",
@ -375,7 +375,7 @@ dependencies = [
"client2", "client2",
"collections", "collections",
"ctor", "ctor",
"editor2", "editor",
"env_logger", "env_logger",
"fs2", "fs2",
"futures 0.3.28", "futures 0.3.28",
@ -1089,7 +1089,7 @@ name = "breadcrumbs"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"collections", "collections",
"editor2", "editor",
"gpui2", "gpui2",
"itertools 0.10.5", "itertools 0.10.5",
"language2", "language2",
@ -1717,7 +1717,7 @@ dependencies = [
"collections", "collections",
"ctor", "ctor",
"dashmap", "dashmap",
"editor2", "editor",
"env_logger", "env_logger",
"envy", "envy",
"fs2", "fs2",
@ -1782,7 +1782,7 @@ dependencies = [
"clock", "clock",
"collections", "collections",
"db2", "db2",
"editor2", "editor",
"feature_flags2", "feature_flags2",
"feedback", "feedback",
"futures 0.3.28", "futures 0.3.28",
@ -1852,7 +1852,7 @@ dependencies = [
"anyhow", "anyhow",
"collections", "collections",
"ctor", "ctor",
"editor2", "editor",
"env_logger", "env_logger",
"fuzzy2", "fuzzy2",
"go_to_line", "go_to_line",
@ -2004,7 +2004,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"copilot2", "copilot2",
"editor2", "editor",
"fs2", "fs2",
"futures 0.3.28", "futures 0.3.28",
"gpui2", "gpui2",
@ -2529,7 +2529,7 @@ dependencies = [
"anyhow", "anyhow",
"client2", "client2",
"collections", "collections",
"editor2", "editor",
"futures 0.3.28", "futures 0.3.28",
"gpui2", "gpui2",
"language2", "language2",
@ -2685,60 +2685,6 @@ checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd"
[[package]] [[package]]
name = "editor" name = "editor"
version = "0.1.0" 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 = [ dependencies = [
"aho-corasick", "aho-corasick",
"anyhow", "anyhow",
@ -2981,7 +2927,7 @@ dependencies = [
"bitflags 2.4.1", "bitflags 2.4.1",
"client2", "client2",
"db2", "db2",
"editor2", "editor",
"futures 0.3.28", "futures 0.3.28",
"gpui2", "gpui2",
"human_bytes", "human_bytes",
@ -3014,7 +2960,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"collections", "collections",
"ctor", "ctor",
"editor2", "editor",
"env_logger", "env_logger",
"fuzzy2", "fuzzy2",
"gpui2", "gpui2",
@ -3568,7 +3514,7 @@ dependencies = [
name = "go_to_line" name = "go_to_line"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"editor2", "editor",
"gpui2", "gpui2",
"menu2", "menu2",
"postage", "postage",
@ -4296,7 +4242,7 @@ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
"dirs 4.0.0", "dirs 4.0.0",
"editor2", "editor",
"gpui2", "gpui2",
"log", "log",
"schemars", "schemars",
@ -4488,7 +4434,7 @@ name = "language_selector"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"editor2", "editor",
"fuzzy2", "fuzzy2",
"gpui2", "gpui2",
"language2", "language2",
@ -4508,7 +4454,7 @@ dependencies = [
"anyhow", "anyhow",
"client2", "client2",
"collections", "collections",
"editor2", "editor",
"env_logger", "env_logger",
"futures 0.3.28", "futures 0.3.28",
"gpui2", "gpui2",
@ -5815,7 +5761,7 @@ dependencies = [
name = "outline2" name = "outline2"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"editor2", "editor",
"fuzzy2", "fuzzy2",
"gpui2", "gpui2",
"language2", "language2",
@ -6039,7 +5985,7 @@ name = "picker"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"ctor", "ctor",
"editor2", "editor",
"env_logger", "env_logger",
"gpui2", "gpui2",
"menu2", "menu2",
@ -6459,7 +6405,7 @@ dependencies = [
"client2", "client2",
"collections", "collections",
"db2", "db2",
"editor2", "editor",
"futures 0.3.28", "futures 0.3.28",
"gpui2", "gpui2",
"language2", "language2",
@ -6486,7 +6432,7 @@ name = "project_symbols"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"editor2", "editor",
"futures 0.3.28", "futures 0.3.28",
"fuzzy2", "fuzzy2",
"gpui2", "gpui2",
@ -6665,7 +6611,7 @@ name = "quick_action_bar"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"assistant2", "assistant2",
"editor2", "editor",
"gpui2", "gpui2",
"search", "search",
"ui2", "ui2",
@ -6837,7 +6783,7 @@ dependencies = [
name = "recent_projects" name = "recent_projects"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"editor2", "editor",
"futures 0.3.28", "futures 0.3.28",
"fuzzy2", "fuzzy2",
"gpui2", "gpui2",
@ -7679,7 +7625,7 @@ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"client2", "client2",
"collections", "collections",
"editor2", "editor",
"futures 0.3.28", "futures 0.3.28",
"gpui2", "gpui2",
"language2", "language2",
@ -8605,7 +8551,7 @@ dependencies = [
"chrono", "chrono",
"clap 4.4.4", "clap 4.4.4",
"dialoguer", "dialoguer",
"editor2", "editor",
"fuzzy2", "fuzzy2",
"gpui2", "gpui2",
"indoc", "indoc",
@ -8968,7 +8914,7 @@ dependencies = [
"client2", "client2",
"db2", "db2",
"dirs 4.0.0", "dirs 4.0.0",
"editor2", "editor",
"futures 0.3.28", "futures 0.3.28",
"gpui2", "gpui2",
"itertools 0.10.5", "itertools 0.10.5",
@ -9114,7 +9060,7 @@ name = "theme_selector"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"client2", "client2",
"editor2", "editor",
"feature_flags2", "feature_flags2",
"fs2", "fs2",
"fuzzy2", "fuzzy2",
@ -10211,7 +10157,7 @@ dependencies = [
"collections", "collections",
"command_palette", "command_palette",
"diagnostics", "diagnostics",
"editor2", "editor",
"futures 0.3.28", "futures 0.3.28",
"gpui2", "gpui2",
"indoc", "indoc",
@ -10629,7 +10575,7 @@ dependencies = [
"anyhow", "anyhow",
"client2", "client2",
"db2", "db2",
"editor2", "editor",
"fs2", "fs2",
"fuzzy2", "fuzzy2",
"gpui2", "gpui2",
@ -11072,7 +11018,7 @@ dependencies = [
"ctor", "ctor",
"db2", "db2",
"diagnostics", "diagnostics",
"editor2", "editor",
"env_logger", "env_logger",
"feature_flags2", "feature_flags2",
"feedback", "feedback",

View File

@ -10,7 +10,7 @@ doctest = false
[dependencies] [dependencies]
auto_update = { path = "../auto_update" } auto_update = { path = "../auto_update" }
editor = { path = "../editor2", package = "editor2" } editor = { path = "../editor" }
language = { path = "../language2", package = "language2" } language = { path = "../language2", package = "language2" }
gpui = { path = "../gpui2", package = "gpui2" } gpui = { path = "../gpui2", package = "gpui2" }
project = { path = "../project2", package = "project2" } project = { path = "../project2", package = "project2" }
@ -25,4 +25,4 @@ futures.workspace = true
smallvec.workspace = true smallvec.workspace = true
[dev-dependencies] [dev-dependencies]
editor = { path = "../editor2", package = "editor2", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] }

View File

@ -12,7 +12,7 @@ doctest = false
ai = { package = "ai2", path = "../ai2" } ai = { package = "ai2", path = "../ai2" }
client = { package = "client2", path = "../client2" } client = { package = "client2", path = "../client2" }
collections = { path = "../collections"} collections = { path = "../collections"}
editor = { package = "editor2", path = "../editor2" } editor = { path = "../editor" }
fs = { package = "fs2", path = "../fs2" } fs = { package = "fs2", path = "../fs2" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" } language = { package = "language2", path = "../language2" }
@ -45,7 +45,7 @@ tiktoken-rs.workspace = true
[dev-dependencies] [dev-dependencies]
ai = { package = "ai2", path = "../ai2", features = ["test-support"]} 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"] } project = { package = "project2", path = "../project2", features = ["test-support"] }
ctor.workspace = true ctor.workspace = true

View File

@ -10,7 +10,7 @@ doctest = false
[dependencies] [dependencies]
collections = { path = "../collections" } collections = { path = "../collections" }
editor = { package = "editor2", path = "../editor2" } editor = { path = "../editor" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
ui = { package = "ui2", path = "../ui2" } ui = { package = "ui2", path = "../ui2" }
language = { package = "language2", path = "../language2" } language = { package = "language2", path = "../language2" }
@ -23,6 +23,6 @@ outline = { package = "outline2", path = "../outline2" }
itertools = "0.10" itertools = "0.10"
[dev-dependencies] [dev-dependencies]
editor = { package = "editor2", path = "../editor2", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }

View File

@ -66,7 +66,7 @@ gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
call = { package = "call2", path = "../call2", features = ["test-support"] } call = { package = "call2", path = "../call2", features = ["test-support"] }
client = { package = "client2", path = "../client2", features = ["test-support"] } client = { package = "client2", path = "../client2", features = ["test-support"] }
channel = { package = "channel2", path = "../channel2" } 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"] } language = { package = "language2", path = "../language2", features = ["test-support"] }
fs = { package = "fs2", path = "../fs2", features = ["test-support"] } fs = { package = "fs2", path = "../fs2", features = ["test-support"] }
git = { package = "git3", path = "../git3", features = ["test-support"] } git = { package = "git3", path = "../git3", features = ["test-support"] }

View File

@ -31,7 +31,7 @@ clock = { path = "../clock" }
collections = { path = "../collections" } collections = { path = "../collections" }
# context_menu = { path = "../context_menu" } # context_menu = { path = "../context_menu" }
# drag_and_drop = { path = "../drag_and_drop" } # drag_and_drop = { path = "../drag_and_drop" }
editor = { package="editor2", path = "../editor2" } editor = { path = "../editor" }
feedback = { path = "../feedback" } feedback = { path = "../feedback" }
fuzzy = { package = "fuzzy2", path = "../fuzzy2" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
@ -68,7 +68,7 @@ smallvec.workspace = true
call = { package = "call2", path = "../call2", features = ["test-support"] } call = { package = "call2", path = "../call2", features = ["test-support"] }
client = { package = "client2", path = "../client2", features = ["test-support"] } client = { package = "client2", path = "../client2", features = ["test-support"] }
collections = { path = "../collections", 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"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
notifications = { package = "notifications2", path = "../notifications2", features = ["test-support"] } notifications = { package = "notifications2", path = "../notifications2", features = ["test-support"] }
project = { package = "project2", path = "../project2", features = ["test-support"] } project = { package = "project2", path = "../project2", features = ["test-support"] }

View File

@ -10,7 +10,7 @@ doctest = false
[dependencies] [dependencies]
collections = { path = "../collections" } collections = { path = "../collections" }
editor = { package = "editor2", path = "../editor2" } editor = { path = "../editor" }
fuzzy = { package = "fuzzy2", path = "../fuzzy2" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
picker = { path = "../picker" } picker = { path = "../picker" }
@ -26,7 +26,7 @@ serde.workspace = true
[dev-dependencies] [dev-dependencies]
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } 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"] } language = { package="language2", path = "../language2", features = ["test-support"] }
project = { package="project2", path = "../project2", features = ["test-support"] } project = { package="project2", path = "../project2", features = ["test-support"] }
menu = { package = "menu2", path = "../menu2" } menu = { package = "menu2", path = "../menu2" }

View File

@ -10,7 +10,7 @@ doctest = false
[dependencies] [dependencies]
collections = { path = "../collections" } collections = { path = "../collections" }
editor = { package = "editor2", path = "../editor2" } editor = { path = "../editor" }
fuzzy = { package = "fuzzy2", path = "../fuzzy2" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
picker = { path = "../picker" } picker = { path = "../picker" }
@ -25,7 +25,7 @@ anyhow.workspace = true
serde.workspace = true serde.workspace = true
[dev-dependencies] [dev-dependencies]
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } 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"] } language = { package="language2", path = "../language2", features = ["test-support"] }
project = { package="project2", path = "../project2", features = ["test-support"] } project = { package="project2", path = "../project2", features = ["test-support"] }
menu = { package = "menu2", path = "../menu2" } menu = { package = "menu2", path = "../menu2" }

View File

@ -10,7 +10,7 @@ doctest = false
[dependencies] [dependencies]
copilot = { package = "copilot2", path = "../copilot2" } copilot = { package = "copilot2", path = "../copilot2" }
editor = { package = "editor2", path = "../editor2" } editor = { path = "../editor" }
fs = { package = "fs2", path = "../fs2" } fs = { package = "fs2", path = "../fs2" }
zed-actions = { package="zed_actions2", path = "../zed_actions2"} zed-actions = { package="zed_actions2", path = "../zed_actions2"}
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
@ -24,4 +24,4 @@ smol.workspace = true
futures.workspace = true futures.workspace = true
[dev-dependencies] [dev-dependencies]
editor = { package = "editor2", path = "../editor2", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] }

View File

@ -10,7 +10,7 @@ doctest = false
[dependencies] [dependencies]
collections = { path = "../collections" } collections = { path = "../collections" }
editor = { package = "editor2", path = "../editor2" } editor = { path = "../editor" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
ui = { package = "ui2", path = "../ui2" } ui = { package = "ui2", path = "../ui2" }
language = { package = "language2", path = "../language2" } language = { package = "language2", path = "../language2" }
@ -32,7 +32,7 @@ postage.workspace = true
[dev-dependencies] [dev-dependencies]
client = { package = "client2", path = "../client2", features = ["test-support"] } 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"] } language = { package = "language2", path = "../language2", features = ["test-support"] }
lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] } lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }

View File

@ -23,30 +23,30 @@ test-support = [
] ]
[dependencies] [dependencies]
client = { path = "../client" } client = { package = "client2", path = "../client2" }
clock = { path = "../clock" } clock = { path = "../clock" }
copilot = { path = "../copilot" } copilot = { package="copilot2", path = "../copilot2" }
db = { path = "../db" } db = { package="db2", path = "../db2" }
drag_and_drop = { path = "../drag_and_drop" }
collections = { path = "../collections" } collections = { path = "../collections" }
context_menu = { path = "../context_menu" } # context_menu = { path = "../context_menu" }
fuzzy = { path = "../fuzzy" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
git = { path = "../git" } git = { package = "git3", path = "../git3" }
gpui = { path = "../gpui" } gpui = { package = "gpui2", path = "../gpui2" }
language = { path = "../language" } language = { package = "language2", path = "../language2" }
lsp = { path = "../lsp" } lsp = { package = "lsp2", path = "../lsp2" }
multi_buffer = { path = "../multi_buffer" } multi_buffer = { package = "multi_buffer2", path = "../multi_buffer2" }
project = { path = "../project" } project = { package = "project2", path = "../project2" }
rpc = { path = "../rpc" } rpc = { package = "rpc2", path = "../rpc2" }
rich_text = { path = "../rich_text" } rich_text = { package = "rich_text2", path = "../rich_text2" }
settings = { path = "../settings" } settings = { package="settings2", path = "../settings2" }
snippet = { path = "../snippet" } snippet = { path = "../snippet" }
sum_tree = { path = "../sum_tree" } sum_tree = { path = "../sum_tree" }
text = { path = "../text" } text = { package="text2", path = "../text2" }
theme = { path = "../theme" } theme = { package="theme2", path = "../theme2" }
ui = { package = "ui2", path = "../ui2" }
util = { path = "../util" } util = { path = "../util" }
sqlez = { path = "../sqlez" } sqlez = { path = "../sqlez" }
workspace = { path = "../workspace" } workspace = { package = "workspace2", path = "../workspace2" }
aho-corasick = "1.1" aho-corasick = "1.1"
anyhow.workspace = true anyhow.workspace = true
@ -62,6 +62,7 @@ postage.workspace = true
rand.workspace = true rand.workspace = true
schemars.workspace = true schemars.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true
serde_derive.workspace = true serde_derive.workspace = true
smallvec.workspace = true smallvec.workspace = true
smol.workspace = true smol.workspace = true
@ -71,16 +72,16 @@ tree-sitter-html = { workspace = true, optional = true }
tree-sitter-typescript = { workspace = true, optional = true } tree-sitter-typescript = { workspace = true, optional = true }
[dev-dependencies] [dev-dependencies]
copilot = { path = "../copilot", features = ["test-support"] } copilot = { package="copilot2", path = "../copilot2", features = ["test-support"] }
text = { path = "../text", features = ["test-support"] } text = { package="text2", path = "../text2", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] } language = { package="language2", path = "../language2", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] } lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] } util = { path = "../util", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] } project = { package = "project2", path = "../project2", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] } settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] } workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
multi_buffer = { path = "../multi_buffer", features = ["test-support"] } multi_buffer = { package = "multi_buffer2", path = "../multi_buffer2", features = ["test-support"] }
ctor.workspace = true ctor.workspace = true
env_logger.workspace = true env_logger.workspace = true

View File

@ -1,5 +1,6 @@
use crate::EditorSettings; use crate::EditorSettings;
use gpui::{Entity, ModelContext}; use gpui::ModelContext;
use settings::Settings;
use settings::SettingsStore; use settings::SettingsStore;
use smol::Timer; use smol::Timer;
use std::time::Duration; use std::time::Duration;
@ -16,7 +17,7 @@ pub struct BlinkManager {
impl BlinkManager { impl BlinkManager {
pub fn new(blink_interval: Duration, cx: &mut ModelContext<Self>) -> Self { pub fn new(blink_interval: Duration, cx: &mut ModelContext<Self>) -> Self {
// Make sure we blink the cursors if the setting is re-enabled // 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) this.blink_cursors(this.blink_epoch, cx)
}) })
.detach(); .detach();
@ -41,14 +42,9 @@ impl BlinkManager {
let epoch = self.next_blink_epoch(); let epoch = self.next_blink_epoch();
let interval = self.blink_interval; let interval = self.blink_interval;
cx.spawn(|this, mut cx| { cx.spawn(|this, mut cx| async move {
let this = this.downgrade(); Timer::after(interval).await;
async move { this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
Timer::after(interval).await;
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
}
}
}) })
.detach(); .detach();
} }
@ -61,20 +57,18 @@ impl BlinkManager {
} }
fn blink_cursors(&mut self, epoch: usize, cx: &mut ModelContext<Self>) { 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 { if epoch == self.blink_epoch && self.enabled && !self.blinking_paused {
self.visible = !self.visible; self.visible = !self.visible;
cx.notify(); cx.notify();
let epoch = self.next_blink_epoch(); let epoch = self.next_blink_epoch();
let interval = self.blink_interval; let interval = self.blink_interval;
cx.spawn(|this, mut cx| { cx.spawn(|this, mut cx| async move {
let this = this.downgrade(); Timer::after(interval).await;
async move { if let Some(this) = this.upgrade() {
Timer::after(interval).await; this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx))
if let Some(this) = this.upgrade(&cx) { .ok();
this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx));
}
} }
}) })
.detach(); .detach();
@ -92,6 +86,10 @@ impl BlinkManager {
} }
pub fn enable(&mut self, cx: &mut ModelContext<Self>) { pub fn enable(&mut self, cx: &mut ModelContext<Self>) {
if self.enabled {
return;
}
self.enabled = true; self.enabled = true;
// Set cursors as invisible and start blinking: this causes cursors // Set cursors as invisible and start blinking: this causes cursors
// to be visible during the next render. // to be visible during the next render.
@ -107,7 +105,3 @@ impl BlinkManager {
self.visible self.visible
} }
} }
impl Entity for BlinkManager {
type Event = ();
}

View File

@ -4,19 +4,15 @@ mod inlay_map;
mod tab_map; mod tab_map;
mod wrap_map; mod wrap_map;
use crate::EditorStyle;
use crate::{ use crate::{
link_go_to_definition::InlayHighlight, movement::TextLayoutDetails, Anchor, AnchorRangeExt, 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}; pub use block_map::{BlockMap, BlockPoint};
use collections::{BTreeMap, HashMap, HashSet}; use collections::{BTreeMap, HashMap, HashSet};
use fold_map::FoldMap; use fold_map::FoldMap;
use gpui::{ use gpui::{Font, HighlightStyle, Hsla, LineLayout, Model, ModelContext, Pixels, UnderlineStyle};
color::Color,
fonts::{FontId, HighlightStyle, Underline},
text_layout::{Line, RunStyle},
Entity, ModelContext, ModelHandle,
};
use inlay_map::InlayMap; use inlay_map::InlayMap;
use language::{ use language::{
language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription, 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 std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
use sum_tree::{Bias, TreeMap}; use sum_tree::{Bias, TreeMap};
use tab_map::TabMap; use tab_map::TabMap;
use wrap_map::WrapMap; use wrap_map::WrapMap;
pub use block_map::{ pub use block_map::{
@ -32,7 +29,7 @@ pub use block_map::{
BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock, 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}; pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint};
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
@ -41,6 +38,8 @@ pub enum FoldStatus {
Foldable, Foldable,
} }
const UNNECESSARY_CODE_FADE: f32 = 0.3;
pub trait ToDisplayPoint { pub trait ToDisplayPoint {
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint; 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)>>; type InlayHighlights = BTreeMap<TypeId, HashMap<InlayId, (HighlightStyle, InlayHighlight)>>;
pub struct DisplayMap { pub struct DisplayMap {
buffer: ModelHandle<MultiBuffer>, buffer: Model<MultiBuffer>,
buffer_subscription: BufferSubscription, buffer_subscription: BufferSubscription,
fold_map: FoldMap, fold_map: FoldMap,
inlay_map: InlayMap, inlay_map: InlayMap,
tab_map: TabMap, tab_map: TabMap,
wrap_map: ModelHandle<WrapMap>, wrap_map: Model<WrapMap>,
block_map: BlockMap, block_map: BlockMap,
text_highlights: TextHighlights, text_highlights: TextHighlights,
inlay_highlights: InlayHighlights, inlay_highlights: InlayHighlights,
pub clip_at_line_ends: bool, pub clip_at_line_ends: bool,
} }
impl Entity for DisplayMap {
type Event = ();
}
impl DisplayMap { impl DisplayMap {
pub fn new( pub fn new(
buffer: ModelHandle<MultiBuffer>, buffer: Model<MultiBuffer>,
font_id: FontId, font: Font,
font_size: f32, font_size: Pixels,
wrap_width: Option<f32>, wrap_width: Option<Pixels>,
buffer_header_height: u8, buffer_header_height: u8,
excerpt_header_height: u8, excerpt_header_height: u8,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
@ -81,7 +76,7 @@ impl DisplayMap {
let (inlay_map, snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx)); let (inlay_map, snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
let (fold_map, snapshot) = FoldMap::new(snapshot); let (fold_map, snapshot) = FoldMap::new(snapshot);
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size); 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); let block_map = BlockMap::new(snapshot, buffer_header_height, excerpt_header_height);
cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach(); cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
DisplayMap { DisplayMap {
@ -127,7 +122,7 @@ impl DisplayMap {
self.fold( self.fold(
other other
.folds_in_range(0..other.buffer_snapshot.len()) .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, cx,
); );
} }
@ -249,16 +244,16 @@ impl DisplayMap {
cleared 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 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) 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 self.wrap_map
.update(cx, |map, cx| map.set_wrap_width(width, cx)) .update(cx, |map, cx| map.set_wrap_width(width, cx))
} }
@ -296,7 +291,7 @@ impl DisplayMap {
self.block_map.read(snapshot, edits); 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 let language = buffer
.read(cx) .read(cx)
.as_singleton() .as_singleton()
@ -510,18 +505,18 @@ impl DisplaySnapshot {
&'a self, &'a self,
display_rows: Range<u32>, display_rows: Range<u32>,
language_aware: bool, language_aware: bool,
style: &'a EditorStyle, editor_style: &'a EditorStyle,
) -> impl Iterator<Item = HighlightedChunk<'a>> { ) -> impl Iterator<Item = HighlightedChunk<'a>> {
self.chunks( self.chunks(
display_rows, display_rows,
language_aware, language_aware,
Some(style.theme.hint), Some(editor_style.inlays_style),
Some(style.theme.suggestion), Some(editor_style.suggestions_style),
) )
.map(|chunk| { .map(|chunk| {
let mut highlight_style = chunk let mut highlight_style = chunk
.syntax_highlight_id .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(chunk_highlight) = chunk.highlight_style {
if let Some(highlight_style) = highlight_style.as_mut() { if let Some(highlight_style) = highlight_style.as_mut() {
@ -534,17 +529,18 @@ impl DisplaySnapshot {
let mut diagnostic_highlight = HighlightStyle::default(); let mut diagnostic_highlight = HighlightStyle::default();
if chunk.is_unnecessary { 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 { if let Some(severity) = chunk.diagnostic_severity {
// Omit underlines for HINT/INFO diagnostics on 'unnecessary' code. // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code.
if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary { if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary {
let diagnostic_style = super::diagnostic_style(severity, true, style); let diagnostic_color =
diagnostic_highlight.underline = Some(Underline { super::diagnostic_style(severity, true, &editor_style.status);
color: Some(diagnostic_style.message.text.color), diagnostic_highlight.underline = Some(UnderlineStyle {
color: Some(diagnostic_color),
thickness: 1.0.into(), 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, &self,
display_row: u32, display_row: u32,
TextLayoutDetails { TextLayoutDetails {
font_cache, text_system,
text_layout_cache,
editor_style, editor_style,
rem_size,
}: &TextLayoutDetails, }: &TextLayoutDetails,
) -> Line { ) -> Arc<LineLayout> {
let mut styles = Vec::new(); let mut runs = Vec::new();
let mut line = String::new(); let mut line = String::new();
let mut ended_in_newline = false;
let range = display_row..display_row + 1; 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); line.push_str(chunk.chunk);
let text_style = if let Some(style) = chunk.style { let text_style = if let Some(style) = chunk.style {
editor_style Cow::Owned(editor_style.text.clone().highlight(style))
.text
.clone()
.highlight(style, font_cache)
.map(Cow::Owned)
.unwrap_or_else(|_| Cow::Borrowed(&editor_style.text))
} else { } else {
Cow::Borrowed(&editor_style.text) Cow::Borrowed(&editor_style.text)
}; };
ended_in_newline = chunk.chunk.ends_with("\n");
styles.push(( runs.push(text_style.to_run(chunk.chunk.len()))
chunk.chunk.len(),
RunStyle {
font_id: text_style.font_id,
color: text_style.color,
underline: text_style.underline,
},
));
} }
// our pixel positioning logic assumes each line ends in \n, if line.ends_with('\n') {
// this is almost always true except for the last line which line.pop();
// may have no trailing newline. if let Some(last_run) = runs.last_mut() {
if !ended_in_newline && display_row == self.max_point().row() { last_run.len -= 1;
line.push_str("\n"); if last_run.len == 0 {
runs.pop();
styles.push(( }
"\n".len(), }
RunStyle {
font_id: editor_style.text.font_id,
color: editor_style.text_color,
underline: editor_style.text.underline,
},
));
} }
text_layout_cache.layout_str(&line, editor_style.text.font_size, &styles) 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, &self,
display_point: DisplayPoint, display_point: DisplayPoint,
text_layout_details: &TextLayoutDetails, text_layout_details: &TextLayoutDetails,
) -> f32 { ) -> Pixels {
let layout_line = self.lay_out_line_for_row(display_point.row(), text_layout_details); let line = self.layout_row(display_point.row(), text_layout_details);
layout_line.x_for_index(display_point.column() as usize) line.x_for_index(display_point.column() as usize)
} }
pub fn column_for_x( pub fn display_column_for_x(
&self, &self,
display_row: u32, display_row: u32,
x_coordinate: f32, x: Pixels,
text_layout_details: &TextLayoutDetails, details: &TextLayoutDetails,
) -> u32 { ) -> u32 {
let layout_line = self.lay_out_line_for_row(display_row, text_layout_details); let layout_line = self.layout_row(display_row, details);
layout_line.closest_index_for_x(x_coordinate) as u32 layout_line.closest_index_for_x(x) as u32
} }
pub fn chars_at( pub fn chars_at(
@ -740,7 +719,7 @@ impl DisplaySnapshot {
DisplayPoint(point) 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 where
T: ToOffset, T: ToOffset,
{ {
@ -1015,7 +994,7 @@ pub mod tests {
movement, movement,
test::{editor_test_context::EditorTestContext, marked_display_snapshot}, 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::{ use language::{
language_settings::{AllLanguageSettings, AllLanguageSettingsContent}, language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
Buffer, Language, LanguageConfig, SelectionGoal, Buffer, Language, LanguageConfig, SelectionGoal,
@ -1025,34 +1004,27 @@ pub mod tests {
use settings::SettingsStore; use settings::SettingsStore;
use smol::stream::StreamExt; use smol::stream::StreamExt;
use std::{env, sync::Arc}; use std::{env, sync::Arc};
use theme::SyntaxTheme; use theme::{LoadThemes, SyntaxTheme};
use util::test::{marked_text_ranges, sample_text}; use util::test::{marked_text_ranges, sample_text};
use Bias::*; use Bias::*;
#[gpui::test(iterations = 100)] #[gpui::test(iterations = 100)]
async fn test_random_display_map(cx: &mut gpui::TestAppContext, mut rng: StdRng) { async fn test_random_display_map(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
cx.foreground().set_block_on_ticks(0..=50); cx.background_executor.set_block_on_ticks(0..=50);
cx.foreground().forbid_parking();
let operations = env::var("OPERATIONS") let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10); .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 mut tab_size = rng.gen_range(1..=4);
let buffer_start_excerpt_header_height = rng.gen_range(1..=5); let buffer_start_excerpt_header_height = rng.gen_range(1..=5);
let excerpt_header_height = rng.gen_range(1..=5); let excerpt_header_height = rng.gen_range(1..=5);
let family_id = font_cache let font_size = px(14.0);
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let max_wrap_width = 300.0; let max_wrap_width = 300.0;
let mut wrap_width = if rng.gen_bool(0.1) { let mut wrap_width = if rng.gen_bool(0.1) {
None None
} else { } 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); 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( DisplayMap::new(
buffer.clone(), buffer.clone(),
font_id, font("Helvetica"),
font_size, font_size,
wrap_width, wrap_width,
buffer_start_excerpt_header_height, buffer_start_excerpt_header_height,
@ -1103,7 +1075,7 @@ pub mod tests {
wrap_width = if rng.gen_bool(0.2) { wrap_width = if rng.gen_bool(0.2) {
None None
} else { } 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); log::info!("setting wrap width to {:?}", wrap_width);
map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); 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(); tab_size = *tab_sizes.choose(&mut rng).unwrap();
log::info!("setting tab size to {:?}", tab_size); log::info!("setting tab size to {:?}", tab_size);
cx.update(|cx| { cx.update(|cx| {
cx.update_global::<SettingsStore, _, _>(|store, cx| { cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |s| { store.update_user_settings::<AllLanguageSettings>(cx, |s| {
s.defaults.tab_size = NonZeroU32::new(tab_size); s.defaults.tab_size = NonZeroU32::new(tab_size);
}); });
@ -1150,7 +1122,7 @@ pub mod tests {
position, position,
height, height,
disposition, disposition,
render: Arc::new(|_| Empty::new().into_any()), render: Arc::new(|_| div().into_any()),
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -1295,7 +1267,8 @@ pub mod tests {
#[gpui::test(retries = 5)] #[gpui::test(retries = 5)]
async fn test_soft_wraps(cx: &mut gpui::TestAppContext) { 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| { cx.update(|cx| {
init_test(cx, |_| {}); init_test(cx, |_| {});
}); });
@ -1304,25 +1277,25 @@ pub mod tests {
let editor = cx.editor.clone(); let editor = cx.editor.clone();
let window = cx.window.clone(); let window = cx.window.clone();
cx.update_window(window, |cx| { _ = cx.update_window(window, |_, cx| {
let text_layout_details = 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 font_size = px(12.0);
let wrap_width = Some(px(64.));
let family_id = font_cache
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 12.0;
let wrap_width = Some(64.);
let text = "one two three four five\nsix seven eight"; let text = "one two three four five\nsix seven eight";
let buffer = MultiBuffer::build_simple(text, cx); let buffer = MultiBuffer::build_simple(text, cx);
let map = cx.add_model(|cx| { let map = cx.new_model(|cx| {
DisplayMap::new(buffer.clone(), font_id, font_size, wrap_width, 1, 1, cx) DisplayMap::new(
buffer.clone(),
font("Helvetica"),
font_size,
wrap_width,
1,
1,
cx,
)
}); });
let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
@ -1347,7 +1320,7 @@ pub mod tests {
DisplayPoint::new(0, 7) 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!( assert_eq!(
movement::up( movement::up(
&snapshot, &snapshot,
@ -1358,33 +1331,33 @@ pub mod tests {
), ),
( (
DisplayPoint::new(0, 7), DisplayPoint::new(0, 7),
SelectionGoal::HorizontalPosition(x) SelectionGoal::HorizontalPosition(x.0)
) )
); );
assert_eq!( assert_eq!(
movement::down( movement::down(
&snapshot, &snapshot,
DisplayPoint::new(0, 7), DisplayPoint::new(0, 7),
SelectionGoal::HorizontalPosition(x), SelectionGoal::HorizontalPosition(x.0),
false, false,
&text_layout_details &text_layout_details
), ),
( (
DisplayPoint::new(1, 10), DisplayPoint::new(1, 10),
SelectionGoal::HorizontalPosition(x) SelectionGoal::HorizontalPosition(x.0)
) )
); );
assert_eq!( assert_eq!(
movement::down( movement::down(
&snapshot, &snapshot,
DisplayPoint::new(1, 10), DisplayPoint::new(1, 10),
SelectionGoal::HorizontalPosition(x), SelectionGoal::HorizontalPosition(x.0),
false, false,
&text_layout_details &text_layout_details
), ),
( (
DisplayPoint::new(2, 4), DisplayPoint::new(2, 4),
SelectionGoal::HorizontalPosition(x) SelectionGoal::HorizontalPosition(x.0)
) )
); );
@ -1400,7 +1373,9 @@ pub mod tests {
); );
// Re-wrap on font size changes // 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)); let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!( assert_eq!(
@ -1416,17 +1391,11 @@ pub mod tests {
let text = sample_text(6, 6, 'a'); let text = sample_text(6, 6, 'a');
let buffer = MultiBuffer::build_simple(&text, cx); let buffer = MultiBuffer::build_simple(&text, cx);
let family_id = cx
.font_cache() let font_size = px(14.0);
.load_family(&["Helvetica"], &Default::default()) let map = cx.new_model(|cx| {
.unwrap(); DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx)
let font_id = cx });
.font_cache()
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let map =
cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
buffer.update(cx, |buffer, cx| { buffer.update(cx, |buffer, cx| {
buffer.edit( buffer.edit(
@ -1470,9 +1439,9 @@ pub mod tests {
}"# }"#
.unindent(); .unindent();
let theme = SyntaxTheme::new(vec![ let theme = SyntaxTheme::new_test(vec![
("mod.body".to_string(), Color::red().into()), ("mod.body", Hsla::red().into()),
("fn.name".to_string(), Color::blue().into()), ("fn.name", Hsla::blue().into()),
]); ]);
let language = Arc::new( let language = Arc::new(
Language::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()))); cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap())));
let buffer = cx let buffer = cx.new_model(|cx| {
.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); Buffer::new(0, cx.entity_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)); 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(14.0);
let family_id = font_cache
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let map = cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx)); let map = cx
.new_model(|cx| DisplayMap::new(buffer, font("Helvetica"), font_size, None, 1, 1, cx));
assert_eq!( assert_eq!(
cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)), cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
vec![ vec![
("fn ".to_string(), None), ("fn ".to_string(), None),
("outer".to_string(), Some(Color::blue())), ("outer".to_string(), Some(Hsla::blue())),
("() {}\n\nmod module ".to_string(), None), ("() {}\n\nmod module ".to_string(), None),
("{\n fn ".to_string(), Some(Color::red())), ("{\n fn ".to_string(), Some(Hsla::red())),
("inner".to_string(), Some(Color::blue())), ("inner".to_string(), Some(Hsla::blue())),
("() {}\n}".to_string(), Some(Color::red())), ("() {}\n}".to_string(), Some(Hsla::red())),
] ]
); );
assert_eq!( assert_eq!(
cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)), cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)),
vec![ vec![
(" fn ".to_string(), Some(Color::red())), (" fn ".to_string(), Some(Hsla::red())),
("inner".to_string(), Some(Color::blue())), ("inner".to_string(), Some(Hsla::blue())),
("() {}\n}".to_string(), Some(Color::red())), ("() {}\n}".to_string(), Some(Hsla::red())),
] ]
); );
@ -1537,11 +1501,11 @@ pub mod tests {
cx.update(|cx| syntax_chunks(0..2, &map, &theme, cx)), cx.update(|cx| syntax_chunks(0..2, &map, &theme, cx)),
vec![ vec![
("fn ".to_string(), None), ("fn ".to_string(), None),
("out".to_string(), Some(Color::blue())), ("out".to_string(), Some(Hsla::blue())),
("".to_string(), None), ("".to_string(), None),
(" fn ".to_string(), Some(Color::red())), (" fn ".to_string(), Some(Hsla::red())),
("inner".to_string(), Some(Color::blue())), ("inner".to_string(), Some(Hsla::blue())),
("() {}\n}".to_string(), Some(Color::red())), ("() {}\n}".to_string(), Some(Hsla::red())),
] ]
); );
} }
@ -1550,7 +1514,8 @@ pub mod tests {
async fn test_chunks_with_soft_wrapping(cx: &mut gpui::TestAppContext) { async fn test_chunks_with_soft_wrapping(cx: &mut gpui::TestAppContext) {
use unindent::Unindent as _; 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#" let text = r#"
fn outer() {} fn outer() {}
@ -1560,9 +1525,9 @@ pub mod tests {
}"# }"#
.unindent(); .unindent();
let theme = SyntaxTheme::new(vec![ let theme = SyntaxTheme::new_test(vec![
("mod.body".to_string(), Color::red().into()), ("mod.body", Hsla::red().into()),
("fn.name".to_string(), Color::blue().into()), ("fn.name", Hsla::blue().into()),
]); ]);
let language = Arc::new( let language = Arc::new(
Language::new( Language::new(
@ -1585,28 +1550,22 @@ pub mod tests {
cx.update(|cx| init_test(cx, |_| {})); cx.update(|cx| init_test(cx, |_| {}));
let buffer = cx let buffer = cx.new_model(|cx| {
.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); Buffer::new(0, cx.entity_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)); 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 let map = cx.new_model(|cx| {
.load_family(&["Courier"], &Default::default()) DisplayMap::new(buffer, font("Courier"), font_size, Some(px(40.0)), 1, 1, cx)
.unwrap(); });
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 16.0;
let map =
cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, Some(40.0), 1, 1, cx));
assert_eq!( assert_eq!(
cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)), cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
[ [
("fn \n".to_string(), None), ("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), ("() \n{}\n\n".to_string(), None),
] ]
); );
@ -1621,10 +1580,10 @@ pub mod tests {
assert_eq!( assert_eq!(
cx.update(|cx| syntax_chunks(1..4, &map, &theme, cx)), 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), ("\n".to_string(), None),
(" \nfn ".to_string(), Some(Color::red())), (" \nfn ".to_string(), Some(Hsla::red())),
("i\n".to_string(), Some(Color::blue())) ("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) { async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) {
cx.update(|cx| init_test(cx, |_| {})); cx.update(|cx| init_test(cx, |_| {}));
let theme = SyntaxTheme::new(vec![ let theme = SyntaxTheme::new_test(vec![
("operator".to_string(), Color::red().into()), ("operator", Hsla::red().into()),
("string".to_string(), Color::green().into()), ("string", Hsla::green().into()),
]); ]);
let language = Arc::new( let language = Arc::new(
Language::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 (text, highlighted_ranges) = marked_text_ranges(r#"constˇ «a»: B = "c «d»""#, false);
let buffer = cx let buffer = cx.new_model(|cx| {
.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
buffer.condition(cx, |buf, _| !buf.is_parsing()).await; });
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 buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
let font_cache = cx.font_cache(); let font_size = px(16.0);
let family_id = font_cache let map =
.load_family(&["Courier"], &Default::default()) cx.new_model(|cx| DisplayMap::new(buffer, font("Courier"), font_size, None, 1, 1, cx));
.unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 16.0;
let map = cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
enum MyType {} enum MyType {}
let style = HighlightStyle { let style = HighlightStyle {
color: Some(Color::blue()), color: Some(Hsla::blue()),
..Default::default() ..Default::default()
}; };
@ -1700,12 +1654,12 @@ pub mod tests {
cx.update(|cx| chunks(0..10, &map, &theme, cx)), cx.update(|cx| chunks(0..10, &map, &theme, cx)),
[ [
("const ".to_string(), None, None), ("const ".to_string(), None, None),
("a".to_string(), None, Some(Color::blue())), ("a".to_string(), None, Some(Hsla::blue())),
(":".to_string(), Some(Color::red()), None), (":".to_string(), Some(Hsla::red()), None),
(" B = ".to_string(), None, None), (" B = ".to_string(), None, None),
("\"c ".to_string(), Some(Color::green()), None), ("\"c ".to_string(), Some(Hsla::green()), None),
("d".to_string(), Some(Color::green()), Some(Color::blue())), ("d".to_string(), Some(Hsla::green()), Some(Hsla::blue())),
("\"".to_string(), Some(Color::green()), None), ("\"".to_string(), Some(Hsla::green()), None),
] ]
); );
} }
@ -1785,17 +1739,11 @@ pub mod tests {
let text = "\t\tα\nβ\t\n🏀β\t\tγ"; let text = "\t\tα\nβ\t\n🏀β\t\tγ";
let buffer = MultiBuffer::build_simple(text, cx); let buffer = MultiBuffer::build_simple(text, cx);
let font_cache = cx.font_cache(); let font_size = px(14.0);
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 = let map = cx.new_model(|cx| {
cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx)); DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx)
});
let map = map.update(cx, |map, cx| map.snapshot(cx)); let map = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(map.text(), "α\nβ \n🏀β γ"); assert_eq!(map.text(), "α\nβ \n🏀β γ");
assert_eq!( assert_eq!(
@ -1846,16 +1794,10 @@ pub mod tests {
init_test(cx, |_| {}); init_test(cx, |_| {});
let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx); let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
let font_cache = cx.font_cache(); let font_size = px(14.0);
let family_id = font_cache let map = cx.new_model(|cx| {
.load_family(&["Helvetica"], &Default::default()) DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx)
.unwrap(); });
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let map =
cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
assert_eq!( assert_eq!(
map.update(cx, |map, cx| map.snapshot(cx)).max_point(), map.update(cx, |map, cx| map.snapshot(cx)).max_point(),
DisplayPoint::new(1, 11) DisplayPoint::new(1, 11)
@ -1864,10 +1806,10 @@ pub mod tests {
fn syntax_chunks<'a>( fn syntax_chunks<'a>(
rows: Range<u32>, rows: Range<u32>,
map: &ModelHandle<DisplayMap>, map: &Model<DisplayMap>,
theme: &'a SyntaxTheme, theme: &'a SyntaxTheme,
cx: &mut AppContext, cx: &mut AppContext,
) -> Vec<(String, Option<Color>)> { ) -> Vec<(String, Option<Hsla>)> {
chunks(rows, map, theme, cx) chunks(rows, map, theme, cx)
.into_iter() .into_iter()
.map(|(text, color, _)| (text, color)) .map(|(text, color, _)| (text, color))
@ -1876,12 +1818,12 @@ pub mod tests {
fn chunks<'a>( fn chunks<'a>(
rows: Range<u32>, rows: Range<u32>,
map: &ModelHandle<DisplayMap>, map: &Model<DisplayMap>,
theme: &'a SyntaxTheme, theme: &'a SyntaxTheme,
cx: &mut AppContext, 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 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) { for chunk in snapshot.chunks(rows, true, None, None) {
let syntax_color = chunk let syntax_color = chunk
.syntax_highlight_id .syntax_highlight_id
@ -1899,13 +1841,13 @@ pub mod tests {
} }
fn init_test(cx: &mut AppContext, f: impl Fn(&mut AllLanguageSettingsContent)) { fn init_test(cx: &mut AppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
cx.foreground().forbid_parking(); let settings = SettingsStore::test(cx);
cx.set_global(SettingsStore::test(cx)); cx.set_global(settings);
language::init(cx); language::init(cx);
crate::init(cx); crate::init(cx);
Project::init_settings(cx); Project::init_settings(cx);
theme::init((), cx); theme::init(LoadThemes::JustBase, cx);
cx.update_global::<SettingsStore, _, _>(|store, cx| { cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, f); store.update_user_settings::<AllLanguageSettings>(cx, f);
}); });
} }

View File

@ -2,9 +2,9 @@ use super::{
wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot}, wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot},
Highlights, Highlights,
}; };
use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _}; use crate::{Anchor, Editor, EditorStyle, ExcerptId, ExcerptRange, ToPoint as _};
use collections::{Bound, HashMap, HashSet}; use collections::{Bound, HashMap, HashSet};
use gpui::{AnyElement, ViewContext}; use gpui::{AnyElement, Pixels, ViewContext};
use language::{BufferSnapshot, Chunk, Patch, Point}; use language::{BufferSnapshot, Chunk, Patch, Point};
use parking_lot::Mutex; use parking_lot::Mutex;
use std::{ use std::{
@ -50,7 +50,7 @@ struct BlockRow(u32);
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
struct WrapRow(u32); 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 { pub struct Block {
id: BlockId, id: BlockId,
@ -69,7 +69,7 @@ where
pub position: P, pub position: P,
pub height: u8, pub height: u8,
pub style: BlockStyle, pub style: BlockStyle,
pub render: Arc<dyn Fn(&mut BlockContext) -> AnyElement<Editor>>, pub render: Arc<dyn Fn(&mut BlockContext) -> AnyElement>,
pub disposition: BlockDisposition, pub disposition: BlockDisposition,
} }
@ -80,15 +80,15 @@ pub enum BlockStyle {
Sticky, Sticky,
} }
pub struct BlockContext<'a, 'b, 'c> { pub struct BlockContext<'a, 'b> {
pub view_context: &'c mut ViewContext<'a, 'b, Editor>, pub view_context: &'b mut ViewContext<'a, Editor>,
pub anchor_x: f32, pub anchor_x: Pixels,
pub scroll_x: f32, pub gutter_width: Pixels,
pub gutter_width: f32, pub gutter_padding: Pixels,
pub gutter_padding: f32, pub em_width: Pixels,
pub em_width: f32, pub line_height: Pixels,
pub line_height: f32,
pub block_id: usize, pub block_id: usize,
pub editor_style: &'b EditorStyle,
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
@ -932,22 +932,22 @@ impl BlockDisposition {
} }
} }
impl<'a, 'b, 'c> Deref for BlockContext<'a, 'b, 'c> { impl<'a> Deref for BlockContext<'a, '_> {
type Target = ViewContext<'a, 'b, Editor>; type Target = ViewContext<'a, Editor>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
self.view_context self.view_context
} }
} }
impl DerefMut for BlockContext<'_, '_, '_> { impl DerefMut for BlockContext<'_, '_> {
fn deref_mut(&mut self) -> &mut Self::Target { fn deref_mut(&mut self) -> &mut Self::Target {
self.view_context self.view_context
} }
} }
impl Block { impl Block {
pub fn render(&self, cx: &mut BlockContext) -> AnyElement<Editor> { pub fn render(&self, cx: &mut BlockContext) -> AnyElement {
self.render.lock()(cx) self.render.lock()(cx)
} }
@ -993,7 +993,7 @@ mod tests {
use super::*; use super::*;
use crate::display_map::inlay_map::InlayMap; use crate::display_map::inlay_map::InlayMap;
use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap}; 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 multi_buffer::MultiBuffer;
use rand::prelude::*; use rand::prelude::*;
use settings::SettingsStore; use settings::SettingsStore;
@ -1015,27 +1015,19 @@ mod tests {
} }
#[gpui::test] #[gpui::test]
fn test_basic_blocks(cx: &mut gpui::AppContext) { fn test_basic_blocks(cx: &mut gpui::TestAppContext) {
init_test(cx); 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 text = "aaa\nbbb\nccc\nddd"; let text = "aaa\nbbb\nccc\nddd";
let buffer = MultiBuffer::build_simple(text, cx); let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
let buffer_snapshot = buffer.read(cx).snapshot(cx); let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot); 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 (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 block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default()); 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)), position: buffer_snapshot.anchor_after(Point::new(1, 0)),
height: 1, height: 1,
disposition: BlockDisposition::Above, disposition: BlockDisposition::Above,
render: Arc::new(|_| Empty::new().into_any_named("block 1")), render: Arc::new(|_| div().into_any()),
}, },
BlockProperties { BlockProperties {
style: BlockStyle::Fixed, style: BlockStyle::Fixed,
position: buffer_snapshot.anchor_after(Point::new(1, 2)), position: buffer_snapshot.anchor_after(Point::new(1, 2)),
height: 2, height: 2,
disposition: BlockDisposition::Above, disposition: BlockDisposition::Above,
render: Arc::new(|_| Empty::new().into_any_named("block 2")), render: Arc::new(|_| div().into_any()),
}, },
BlockProperties { BlockProperties {
style: BlockStyle::Fixed, style: BlockStyle::Fixed,
position: buffer_snapshot.anchor_after(Point::new(3, 3)), position: buffer_snapshot.anchor_after(Point::new(3, 3)),
height: 3, height: 3,
disposition: BlockDisposition::Below, 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] #[gpui::test]
fn test_blocks_on_wrapped_lines(cx: &mut gpui::AppContext) { fn test_blocks_on_wrapped_lines(cx: &mut gpui::TestAppContext) {
init_test(cx); cx.update(|cx| init_test(cx));
let family_id = cx let _font_id = cx.text_system().font_id(&font("Helvetica")).unwrap();
.font_cache()
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
.unwrap();
let text = "one two three\nfour five six\nseven eight"; let text = "one two three\nfour five six\nseven eight";
let buffer = MultiBuffer::build_simple(text, cx); let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
let buffer_snapshot = buffer.read(cx).snapshot(cx); let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); 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 block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default()); let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
@ -1218,14 +1205,14 @@ mod tests {
style: BlockStyle::Fixed, style: BlockStyle::Fixed,
position: buffer_snapshot.anchor_after(Point::new(1, 12)), position: buffer_snapshot.anchor_after(Point::new(1, 12)),
disposition: BlockDisposition::Above, disposition: BlockDisposition::Above,
render: Arc::new(|_| Empty::new().into_any_named("block 1")), render: Arc::new(|_| div().into_any()),
height: 1, height: 1,
}, },
BlockProperties { BlockProperties {
style: BlockStyle::Fixed, style: BlockStyle::Fixed,
position: buffer_snapshot.anchor_after(Point::new(1, 1)), position: buffer_snapshot.anchor_after(Point::new(1, 1)),
disposition: BlockDisposition::Below, disposition: BlockDisposition::Below,
render: Arc::new(|_| Empty::new().into_any_named("block 2")), render: Arc::new(|_| div().into_any()),
height: 1, height: 1,
}, },
]); ]);
@ -1240,8 +1227,8 @@ mod tests {
} }
#[gpui::test(iterations = 100)] #[gpui::test(iterations = 100)]
fn test_random_blocks(cx: &mut gpui::AppContext, mut rng: StdRng) { fn test_random_blocks(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
init_test(cx); cx.update(|cx| init_test(cx));
let operations = env::var("OPERATIONS") let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
@ -1250,18 +1237,10 @@ mod tests {
let wrap_width = if rng.gen_bool(0.2) { let wrap_width = if rng.gen_bool(0.2) {
None None
} else { } 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 tab_size = 1.try_into().unwrap();
let family_id = cx let font_size = px(14.0);
.font_cache()
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let buffer_start_header_height = rng.gen_range(1..=5); let buffer_start_header_height = rng.gen_range(1..=5);
let excerpt_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 len = rng.gen_range(0..10);
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>(); let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
log::info!("initial buffer text: {:?}", text); log::info!("initial buffer text: {:?}", text);
MultiBuffer::build_simple(&text, cx) cx.update(|cx| MultiBuffer::build_simple(&text, cx))
} else { } 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 inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot); 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 (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (wrap_map, wraps_snapshot) = let (wrap_map, wraps_snapshot) = cx
WrapMap::new(tab_snapshot, font_id, font_size, wrap_width, cx); .update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), font_size, wrap_width, cx));
let mut block_map = BlockMap::new( let mut block_map = BlockMap::new(
wraps_snapshot, wraps_snapshot,
buffer_start_header_height, buffer_start_header_height,
@ -1297,7 +1276,7 @@ mod tests {
let wrap_width = if rng.gen_bool(0.2) { let wrap_width = if rng.gen_bool(0.2) {
None None
} else { } 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); log::info!("Setting wrap width to {:?}", wrap_width);
wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); 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_count = rng.gen_range(1..=5);
let block_properties = (0..block_count) let block_properties = (0..block_count)
.map(|_| { .map(|_| {
let buffer = buffer.read(cx).read(cx); let buffer = cx.update(|cx| buffer.read(cx).read(cx).clone());
let position = buffer.anchor_after( let position = buffer.anchor_after(
buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Left), buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Left),
); );
@ -1328,7 +1307,7 @@ mod tests {
position, position,
height, height,
disposition, disposition,
render: Arc::new(|_| Empty::new().into_any()), render: Arc::new(|_| div().into_any()),
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -1646,8 +1625,9 @@ mod tests {
} }
fn init_test(cx: &mut gpui::AppContext) { fn init_test(cx: &mut gpui::AppContext) {
cx.set_global(SettingsStore::test(cx)); let settings = SettingsStore::test(cx);
theme::init((), cx); cx.set_global(settings);
theme::init(theme::LoadThemes::JustBase, cx);
} }
impl TransformBlock { impl TransformBlock {

View File

@ -3,15 +3,16 @@ use super::{
Highlights, Highlights,
}; };
use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset}; use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset};
use gpui::{color::Color, fonts::HighlightStyle}; use gpui::{ElementId, HighlightStyle, Hsla};
use language::{Chunk, Edit, Point, TextSummary}; use language::{Chunk, Edit, Point, TextSummary};
use std::{ use std::{
any::TypeId, any::TypeId,
cmp::{self, Ordering}, cmp::{self, Ordering},
iter, iter,
ops::{Add, AddAssign, Range, Sub}, ops::{Add, AddAssign, Deref, DerefMut, Range, Sub},
}; };
use sum_tree::{Bias, Cursor, FilterCursor, SumTree}; use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
use util::post_inc;
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct FoldPoint(pub Point); pub struct FoldPoint(pub Point);
@ -90,12 +91,16 @@ impl<'a> FoldMapWriter<'a> {
} }
// For now, ignore any ranges that span an excerpt boundary. // For now, ignore any ranges that span an excerpt boundary.
let fold = Fold(buffer.anchor_after(range.start)..buffer.anchor_before(range.end)); let fold_range =
if fold.0.start.excerpt_id != fold.0.end.excerpt_id { 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; continue;
} }
folds.push(fold); folds.push(Fold {
id: FoldId(post_inc(&mut self.0.next_fold_id.0)),
range: fold_range,
});
let inlay_range = let inlay_range =
snapshot.to_inlay_offset(range.start)..snapshot.to_inlay_offset(range.end); snapshot.to_inlay_offset(range.start)..snapshot.to_inlay_offset(range.end);
@ -106,13 +111,13 @@ impl<'a> FoldMapWriter<'a> {
} }
let buffer = &snapshot.buffer; 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 = { self.0.snapshot.folds = {
let mut new_tree = SumTree::new(); 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 { 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.push(fold, buffer);
} }
new_tree.append(cursor.suffix(buffer), buffer); new_tree.append(cursor.suffix(buffer), buffer);
@ -138,7 +143,8 @@ impl<'a> FoldMapWriter<'a> {
let mut folds_cursor = let mut folds_cursor =
intersecting_folds(&snapshot, &self.0.snapshot.folds, range, inclusive); intersecting_folds(&snapshot, &self.0.snapshot.folds, range, inclusive);
while let Some(fold) = folds_cursor.item() { 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 { if offset_range.end > offset_range.start {
let inlay_range = snapshot.to_inlay_offset(offset_range.start) let inlay_range = snapshot.to_inlay_offset(offset_range.start)
..snapshot.to_inlay_offset(offset_range.end); ..snapshot.to_inlay_offset(offset_range.end);
@ -174,7 +180,8 @@ impl<'a> FoldMapWriter<'a> {
pub struct FoldMap { pub struct FoldMap {
snapshot: FoldSnapshot, snapshot: FoldSnapshot,
ellipses_color: Option<Color>, ellipses_color: Option<Hsla>,
next_fold_id: FoldId,
} }
impl FoldMap { impl FoldMap {
@ -197,6 +204,7 @@ impl FoldMap {
ellipses_color: None, ellipses_color: None,
}, },
ellipses_color: None, ellipses_color: None,
next_fold_id: FoldId::default(),
}; };
let snapshot = this.snapshot.clone(); let snapshot = this.snapshot.clone();
(this, snapshot) (this, snapshot)
@ -221,7 +229,7 @@ impl FoldMap {
(FoldMapWriter(self), snapshot, edits) (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) { if self.ellipses_color != Some(color) {
self.ellipses_color = Some(color); self.ellipses_color = Some(color);
true true
@ -242,8 +250,8 @@ impl FoldMap {
while let Some(fold) = folds.next() { while let Some(fold) = folds.next() {
if let Some(next_fold) = folds.peek() { if let Some(next_fold) = folds.peek() {
let comparison = fold let comparison = fold
.0 .range
.cmp(&next_fold.0, &self.snapshot.inlay_snapshot.buffer); .cmp(&next_fold.range, &self.snapshot.inlay_snapshot.buffer);
assert!(comparison.is_le()); assert!(comparison.is_le());
} }
} }
@ -304,9 +312,9 @@ impl FoldMap {
let anchor = inlay_snapshot let anchor = inlay_snapshot
.buffer .buffer
.anchor_before(inlay_snapshot.to_buffer_offset(edit.new.start)); .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( folds_cursor.seek(
&Fold(anchor..Anchor::max()), &FoldRange(anchor..Anchor::max()),
Bias::Left, Bias::Left,
&inlay_snapshot.buffer, &inlay_snapshot.buffer,
); );
@ -315,8 +323,8 @@ impl FoldMap {
let inlay_snapshot = &inlay_snapshot; let inlay_snapshot = &inlay_snapshot;
move || { move || {
let item = folds_cursor.item().map(|f| { let item = folds_cursor.item().map(|f| {
let buffer_start = f.0.start.to_offset(&inlay_snapshot.buffer); let buffer_start = f.range.start.to_offset(&inlay_snapshot.buffer);
let buffer_end = f.0.end.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_start)
..inlay_snapshot.to_inlay_offset(buffer_end) ..inlay_snapshot.to_inlay_offset(buffer_end)
}); });
@ -469,7 +477,7 @@ pub struct FoldSnapshot {
folds: SumTree<Fold>, folds: SumTree<Fold>,
pub inlay_snapshot: InlaySnapshot, pub inlay_snapshot: InlaySnapshot,
pub version: usize, pub version: usize,
pub ellipses_color: Option<Color>, pub ellipses_color: Option<Hsla>,
} }
impl FoldSnapshot { impl FoldSnapshot {
@ -596,13 +604,13 @@ impl FoldSnapshot {
self.transforms.summary().output.longest_row 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 where
T: ToOffset, T: ToOffset,
{ {
let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false); let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false);
iter::from_fn(move || { iter::from_fn(move || {
let item = folds.item().map(|f| &f.0); let item = folds.item();
folds.next(&self.inlay_snapshot.buffer); folds.next(&self.inlay_snapshot.buffer);
item item
}) })
@ -830,10 +838,39 @@ impl sum_tree::Summary for TransformSummary {
} }
} }
#[derive(Clone, Debug)] #[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
struct Fold(Range<Anchor>); 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 { fn default() -> Self {
Self(Anchor::min()..Anchor::max()) Self(Anchor::min()..Anchor::max())
} }
@ -844,17 +881,17 @@ impl sum_tree::Item for Fold {
fn summary(&self) -> Self::Summary { fn summary(&self) -> Self::Summary {
FoldSummary { FoldSummary {
start: self.0.start.clone(), start: self.range.start.clone(),
end: self.0.end.clone(), end: self.range.end.clone(),
min_start: self.0.start.clone(), min_start: self.range.start.clone(),
max_end: self.0.end.clone(), max_end: self.range.end.clone(),
count: 1, count: 1,
} }
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct FoldSummary { pub struct FoldSummary {
start: Anchor, start: Anchor,
end: Anchor, end: Anchor,
min_start: 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) { fn add_summary(&mut self, summary: &'a FoldSummary, _: &MultiBufferSnapshot) {
self.0.start = summary.start.clone(); self.0.start = summary.start.clone();
self.0.end = summary.end.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 { fn cmp(&self, other: &Self, buffer: &MultiBufferSnapshot) -> Ordering {
self.0.cmp(&other.0, buffer) self.0.cmp(&other.0, buffer)
} }
@ -959,7 +996,7 @@ pub struct FoldChunks<'a> {
inlay_offset: InlayOffset, inlay_offset: InlayOffset,
output_offset: usize, output_offset: usize,
max_output_offset: usize, max_output_offset: usize,
ellipses_color: Option<Color>, ellipses_color: Option<Hsla>,
} }
impl<'a> Iterator for FoldChunks<'a> { impl<'a> Iterator for FoldChunks<'a> {
@ -1321,7 +1358,10 @@ mod tests {
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
let fold_ranges = snapshot let fold_ranges = snapshot
.folds_in_range(Point::new(1, 0)..Point::new(1, 3)) .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<_>>(); .collect::<Vec<_>>();
assert_eq!( assert_eq!(
fold_ranges, fold_ranges,
@ -1553,10 +1593,9 @@ mod tests {
.filter(|fold| { .filter(|fold| {
let start = buffer_snapshot.anchor_before(start); let start = buffer_snapshot.anchor_before(start);
let end = buffer_snapshot.anchor_after(end); let end = buffer_snapshot.anchor_after(end);
start.cmp(&fold.0.end, &buffer_snapshot) == Ordering::Less start.cmp(&fold.range.end, &buffer_snapshot) == Ordering::Less
&& end.cmp(&fold.0.start, &buffer_snapshot) == Ordering::Greater && end.cmp(&fold.range.start, &buffer_snapshot) == Ordering::Greater
}) })
.map(|fold| fold.0)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
assert_eq!( assert_eq!(
@ -1629,7 +1668,8 @@ mod tests {
} }
fn init_test(cx: &mut gpui::AppContext) { fn init_test(cx: &mut gpui::AppContext) {
cx.set_global(SettingsStore::test(cx)); let store = SettingsStore::test(cx);
cx.set_global(store);
} }
impl FoldMap { impl FoldMap {
@ -1638,10 +1678,10 @@ mod tests {
let buffer = &inlay_snapshot.buffer; let buffer = &inlay_snapshot.buffer;
let mut folds = self.snapshot.folds.items(buffer); let mut folds = self.snapshot.folds.items(buffer);
// Ensure sorting doesn't change how folds get merged and displayed. // 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 let mut fold_ranges = folds
.iter() .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(); .peekable();
let mut merged_ranges = Vec::new(); let mut merged_ranges = Vec::new();

View File

@ -1,6 +1,6 @@
use crate::{Anchor, InlayId, MultiBufferSnapshot, ToOffset}; use crate::{Anchor, InlayId, MultiBufferSnapshot, ToOffset};
use collections::{BTreeMap, BTreeSet}; use collections::{BTreeMap, BTreeSet};
use gpui::fonts::HighlightStyle; use gpui::HighlightStyle;
use language::{Chunk, Edit, Point, TextSummary}; use language::{Chunk, Edit, Point, TextSummary};
use multi_buffer::{MultiBufferChunks, MultiBufferRows}; use multi_buffer::{MultiBufferChunks, MultiBufferRows};
use std::{ use std::{
@ -1889,7 +1889,8 @@ mod tests {
} }
fn init_test(cx: &mut AppContext) { fn init_test(cx: &mut AppContext) {
cx.set_global(SettingsStore::test(cx)); let store = SettingsStore::test(cx);
theme::init((), cx); cx.set_global(store);
theme::init(theme::LoadThemes::JustBase, cx);
} }
} }

View File

@ -4,15 +4,14 @@ use super::{
Highlights, Highlights,
}; };
use crate::MultiBufferSnapshot; use crate::MultiBufferSnapshot;
use gpui::{ use gpui::{AppContext, Context, Font, LineWrapper, Model, ModelContext, Pixels, Task};
fonts::FontId, text_layout::LineWrapper, AppContext, Entity, ModelContext, ModelHandle, Task,
};
use language::{Chunk, Point}; use language::{Chunk, Point};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use smol::future::yield_now; use smol::future::yield_now;
use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration}; use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration};
use sum_tree::{Bias, Cursor, SumTree}; use sum_tree::{Bias, Cursor, SumTree};
use text::Patch; use text::Patch;
use util::ResultExt;
pub use super::tab_map::TextSummary; pub use super::tab_map::TextSummary;
pub type WrapEdit = text::Edit<u32>; pub type WrapEdit = text::Edit<u32>;
@ -22,13 +21,9 @@ pub struct WrapMap {
pending_edits: VecDeque<(TabSnapshot, Vec<TabEdit>)>, pending_edits: VecDeque<(TabSnapshot, Vec<TabEdit>)>,
interpolated_edits: Patch<u32>, interpolated_edits: Patch<u32>,
edits_since_sync: Patch<u32>, edits_since_sync: Patch<u32>,
wrap_width: Option<f32>, wrap_width: Option<Pixels>,
background_task: Option<Task<()>>, background_task: Option<Task<()>>,
font: (FontId, f32), font_with_size: (Font, Pixels),
}
impl Entity for WrapMap {
type Event = ();
} }
#[derive(Clone)] #[derive(Clone)]
@ -74,14 +69,14 @@ pub struct WrapBufferRows<'a> {
impl WrapMap { impl WrapMap {
pub fn new( pub fn new(
tab_snapshot: TabSnapshot, tab_snapshot: TabSnapshot,
font_id: FontId, font: Font,
font_size: f32, font_size: Pixels,
wrap_width: Option<f32>, wrap_width: Option<Pixels>,
cx: &mut AppContext, cx: &mut AppContext,
) -> (ModelHandle<Self>, WrapSnapshot) { ) -> (Model<Self>, WrapSnapshot) {
let handle = cx.add_model(|cx| { let handle = cx.new_model(|cx| {
let mut this = Self { let mut this = Self {
font: (font_id, font_size), font_with_size: (font, font_size),
wrap_width: None, wrap_width: None,
pending_edits: Default::default(), pending_edits: Default::default(),
interpolated_edits: Default::default(), interpolated_edits: Default::default(),
@ -121,14 +116,16 @@ impl WrapMap {
(self.snapshot.clone(), mem::take(&mut self.edits_since_sync)) (self.snapshot.clone(), mem::take(&mut self.edits_since_sync))
} }
pub fn set_font( pub fn set_font_with_size(
&mut self, &mut self,
font_id: FontId, font: Font,
font_size: f32, font_size: Pixels,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> bool { ) -> bool {
if (font_id, font_size) != self.font { let font_with_size = (font, font_size);
self.font = (font_id, font_size);
if font_with_size != self.font_with_size {
self.font_with_size = font_with_size;
self.rewrap(cx); self.rewrap(cx);
true true
} else { } 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 { if wrap_width == self.wrap_width {
return false; return false;
} }
@ -153,34 +154,36 @@ impl WrapMap {
if let Some(wrap_width) = self.wrap_width { if let Some(wrap_width) = self.wrap_width {
let mut new_snapshot = self.snapshot.clone(); let mut new_snapshot = self.snapshot.clone();
let font_cache = cx.font_cache().clone(); let mut edits = Patch::default();
let (font_id, font_size) = self.font; let text_system = cx.text_system().clone();
let task = cx.background().spawn(async move { let (font, font_size) = self.font_with_size.clone();
let mut line_wrapper = font_cache.line_wrapper(font_id, font_size); let task = cx.background_executor().spawn(async move {
let tab_snapshot = new_snapshot.tab_snapshot.clone(); if let Some(mut line_wrapper) = text_system.line_wrapper(font, font_size).log_err()
let range = TabPoint::zero()..tab_snapshot.max_point(); {
let edits = new_snapshot let tab_snapshot = new_snapshot.tab_snapshot.clone();
.update( let range = TabPoint::zero()..tab_snapshot.max_point();
tab_snapshot, edits = new_snapshot
&[TabEdit { .update(
old: range.clone(), tab_snapshot,
new: range.clone(), &[TabEdit {
}], old: range.clone(),
wrap_width, new: range.clone(),
&mut line_wrapper, }],
) wrap_width,
.await; &mut line_wrapper,
)
.await;
}
(new_snapshot, edits) (new_snapshot, edits)
}); });
match cx match cx
.background() .background_executor()
.block_with_timeout(Duration::from_millis(5), task) .block_with_timeout(Duration::from_millis(5), task)
{ {
Ok((snapshot, edits)) => { Ok((snapshot, edits)) => {
self.snapshot = snapshot; self.snapshot = snapshot;
self.edits_since_sync = self.edits_since_sync.compose(&edits); self.edits_since_sync = self.edits_since_sync.compose(&edits);
cx.notify();
} }
Err(wrap_task) => { Err(wrap_task) => {
self.background_task = Some(cx.spawn(|this, mut cx| async move { self.background_task = Some(cx.spawn(|this, mut cx| async move {
@ -194,7 +197,8 @@ impl WrapMap {
this.background_task = None; this.background_task = None;
this.flush_edits(cx); this.flush_edits(cx);
cx.notify(); cx.notify();
}); })
.ok();
})); }));
} }
} }
@ -237,23 +241,25 @@ impl WrapMap {
if self.background_task.is_none() { if self.background_task.is_none() {
let pending_edits = self.pending_edits.clone(); let pending_edits = self.pending_edits.clone();
let mut snapshot = self.snapshot.clone(); let mut snapshot = self.snapshot.clone();
let font_cache = cx.font_cache().clone(); let text_system = cx.text_system().clone();
let (font_id, font_size) = self.font; let (font, font_size) = self.font_with_size.clone();
let update_task = cx.background().spawn(async move { let update_task = cx.background_executor().spawn(async move {
let mut line_wrapper = font_cache.line_wrapper(font_id, font_size);
let mut edits = Patch::default(); let mut edits = Patch::default();
for (tab_snapshot, tab_edits) in pending_edits { if let Some(mut line_wrapper) =
let wrap_edits = snapshot text_system.line_wrapper(font, font_size).log_err()
.update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper) {
.await; for (tab_snapshot, tab_edits) in pending_edits {
edits = edits.compose(&wrap_edits); let wrap_edits = snapshot
.update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper)
.await;
edits = edits.compose(&wrap_edits);
}
} }
(snapshot, edits) (snapshot, edits)
}); });
match cx match cx
.background() .background_executor()
.block_with_timeout(Duration::from_millis(1), update_task) .block_with_timeout(Duration::from_millis(1), update_task)
{ {
Ok((snapshot, output_edits)) => { Ok((snapshot, output_edits)) => {
@ -272,7 +278,8 @@ impl WrapMap {
this.background_task = None; this.background_task = None;
this.flush_edits(cx); this.flush_edits(cx);
cx.notify(); cx.notify();
}); })
.ok();
})); }));
} }
} }
@ -385,7 +392,7 @@ impl WrapSnapshot {
&mut self, &mut self,
new_tab_snapshot: TabSnapshot, new_tab_snapshot: TabSnapshot,
tab_edits: &[TabEdit], tab_edits: &[TabEdit],
wrap_width: f32, wrap_width: Pixels,
line_wrapper: &mut LineWrapper, line_wrapper: &mut LineWrapper,
) -> Patch<u32> { ) -> Patch<u32> {
#[derive(Debug)] #[derive(Debug)]
@ -1026,37 +1033,34 @@ mod tests {
display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap}, display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
MultiBuffer, MultiBuffer,
}; };
use gpui::test::observe; use gpui::{font, px, test::observe};
use rand::prelude::*; use rand::prelude::*;
use settings::SettingsStore; use settings::SettingsStore;
use smol::stream::StreamExt; use smol::stream::StreamExt;
use std::{cmp, env, num::NonZeroU32}; use std::{cmp, env, num::NonZeroU32};
use text::Rope; use text::Rope;
use theme::LoadThemes;
#[gpui::test(iterations = 100)] #[gpui::test(iterations = 100)]
async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) { async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
// todo!() this test is flaky
init_test(cx); 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") let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10); .unwrap_or(10);
let font_cache = cx.font_cache().clone(); let text_system = cx.read(|cx| cx.text_system().clone());
let font_system = cx.platform().fonts();
let mut wrap_width = if rng.gen_bool(0.1) { let mut wrap_width = if rng.gen_bool(0.1) {
None None
} else { } 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 tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
let family_id = font_cache let font = font("Helvetica");
.load_family(&["Helvetica"], &Default::default()) let _font_id = text_system.font_id(&font).unwrap();
.unwrap(); let font_size = px(14.0);
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
log::info!("Tab size: {}", tab_size); log::info!("Tab size: {}", tab_size);
log::info!("Wrap width: {:?}", wrap_width); log::info!("Wrap width: {:?}", wrap_width);
@ -1082,12 +1086,12 @@ mod tests {
let tabs_snapshot = tab_map.set_max_expansion_column(32); let tabs_snapshot = tab_map.set_max_expansion_column(32);
log::info!("TabMap text: {:?}", tabs_snapshot.text()); 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 unwrapped_text = tabs_snapshot.text();
let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
let (wrap_map, _) = 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); let mut notifications = observe(&wrap_map, cx);
if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
@ -1118,7 +1122,7 @@ mod tests {
wrap_width = if rng.gen_bool(0.2) { wrap_width = if rng.gen_bool(0.2) {
None None
} else { } 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); log::info!("Setting wrap width to {:?}", wrap_width);
wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); 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) { fn init_test(cx: &mut gpui::TestAppContext) {
cx.foreground().forbid_parking();
cx.update(|cx| { cx.update(|cx| {
cx.set_global(SettingsStore::test(cx)); let settings = SettingsStore::test(cx);
theme::init((), cx); cx.set_global(settings);
theme::init(LoadThemes::JustBase, cx);
}); });
} }
fn wrap_text( fn wrap_text(
unwrapped_text: &str, unwrapped_text: &str,
wrap_width: Option<f32>, wrap_width: Option<Pixels>,
line_wrapper: &mut LineWrapper, line_wrapper: &mut LineWrapper,
) -> String { ) -> String {
if let Some(wrap_width) = wrap_width { if let Some(wrap_width) = wrap_width {

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,8 @@
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::Setting; use settings::Settings;
#[derive(Clone, Deserialize)] #[derive(Deserialize)]
pub struct EditorSettings { pub struct EditorSettings {
pub cursor_blink: bool, pub cursor_blink: bool,
pub hover_popover_enabled: bool, pub hover_popover_enabled: bool,
@ -57,7 +57,7 @@ pub struct ScrollbarContent {
pub selections: Option<bool>, pub selections: Option<bool>,
} }
impl Setting for EditorSettings { impl Settings for EditorSettings {
const KEY: Option<&'static str> = None; const KEY: Option<&'static str> = None;
type FileContent = EditorSettingsContent; type FileContent = EditorSettingsContent;
@ -65,7 +65,7 @@ impl Setting for EditorSettings {
fn load( fn load(
default_value: &Self::FileContent, default_value: &Self::FileContent,
user_values: &[&Self::FileContent], user_values: &[&Self::FileContent],
_: &gpui::AppContext, _: &mut gpui::AppContext,
) -> anyhow::Result<Self> { ) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values) 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

View File

@ -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_end = Point::new(hunk.buffer_range.end + 2, 0);
let folds_range = folds_start..folds_end; let folds_range = folds_start..folds_end;
let containing_fold = snapshot.folds_in_range(folds_range).find(|fold_range| { 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.range.to_point(&snapshot.buffer_snapshot);
let fold_point_range = fold_point_range.start..=fold_point_range.end; let fold_point_range = fold_point_range.start..=fold_point_range.end;
let folded_start = fold_point_range.contains(&hunk_start_point); 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 { 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 } DisplayDiffHunk::Folded { display_row: row }
} else { } else {
let start = hunk_start_point.to_display_point(snapshot).row(); 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 { mod tests {
use crate::editor_tests::init_test; use crate::editor_tests::init_test;
use crate::Point; use crate::Point;
use gpui::TestAppContext; use gpui::{Context, TestAppContext};
use multi_buffer::{ExcerptRange, MultiBuffer}; use multi_buffer::{ExcerptRange, MultiBuffer};
use project::{FakeFs, Project}; use project::{FakeFs, Project};
use unindent::Unindent; use unindent::Unindent;
@ -101,7 +101,7 @@ mod tests {
use git::diff::DiffHunkStatus; use git::diff::DiffHunkStatus;
init_test(cx, |_| {}); init_test(cx, |_| {});
let fs = FakeFs::new(cx.background()); let fs = FakeFs::new(cx.background_executor.clone());
let project = Project::test(fs, [], cx).await; let project = Project::test(fs, [], cx).await;
// buffer has two modified hunks with two rows each // 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); let mut multibuffer = MultiBuffer::new(0);
multibuffer.push_excerpts( multibuffer.push_excerpts(
buffer_1.clone(), buffer_1.clone(),

View File

@ -24,7 +24,7 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
opening_range.to_anchors(&snapshot.buffer_snapshot), opening_range.to_anchors(&snapshot.buffer_snapshot),
closing_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, cx,
) )
} }

View File

@ -6,16 +6,17 @@ use crate::{
}; };
use futures::FutureExt; use futures::FutureExt;
use gpui::{ use gpui::{
actions, actions, div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, Model,
elements::{Flex, MouseEventHandler, Padding, ParentElement, Text}, MouseButton, ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled,
platform::{CursorStyle, MouseButton}, Task, ViewContext, WeakView,
AnyElement, AppContext, Element, ModelHandle, Task, ViewContext, WeakViewHandle,
};
use language::{
markdown, Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, ParsedMarkdown,
}; };
use language::{markdown, Bias, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
use lsp::DiagnosticSeverity;
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
use settings::Settings;
use std::{ops::Range, sync::Arc, time::Duration}; use std::{ops::Range, sync::Arc, time::Duration};
use ui::{StyledExt, Tooltip};
use util::TryFutureExt; use util::TryFutureExt;
use workspace::Workspace; use workspace::Workspace;
@ -23,15 +24,11 @@ pub const HOVER_DELAY_MILLIS: u64 = 350;
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200; pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.; pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.; pub const MIN_POPOVER_LINE_HEIGHT: Pixels = px(4.);
pub const HOVER_POPOVER_GAP: f32 = 10.; pub const HOVER_POPOVER_GAP: Pixels = px(10.);
actions!(editor, [Hover]); 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 /// Bindable action which uses the most recent selection head to trigger a hover
pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) { pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
let head = editor.selections.newest_display(cx).head(); 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` /// The internal hover action dispatches between `show_hover` or `hide_hover`
/// depending on whether a point to hover over is provided. /// depending on whether a point to hover over is provided.
pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewContext<Editor>) { 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 { if let Some(point) = point {
show_hover(editor, point, false, cx); show_hover(editor, point, false, cx);
} else { } 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>) { 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() { if editor.pending_rename.is_some() {
return; 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| { let task = cx.spawn(|this, mut cx| {
async move { async move {
cx.background() cx.background_executor()
.timer(Duration::from_millis(HOVER_DELAY_MILLIS)) .timer(Duration::from_millis(HOVER_DELAY_MILLIS))
.await; .await;
this.update(&mut cx, |this, _| { this.update(&mut cx, |this, _| {
this.hover_state.diagnostic_popover = None; 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 blocks = vec![inlay_hover.tooltip];
let parsed_content = parse_blocks(&blocks, &language_registry, None).await; 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 // Highlight the selected symbol using a background highlight
this.highlight_inlay_background::<HoverState>( this.highlight_inlay_background::<HoverState>(
vec![inlay_hover.range], vec![inlay_hover.range],
|theme| theme.editor.hover_popover.highlight, |theme| theme.element_hover, // todo!("use a proper background here")
cx, cx,
); );
this.hover_state.info_popover = Some(hover_popover); this.hover_state.info_popover = Some(hover_popover);
@ -239,11 +236,11 @@ fn show_hover(
let delay = if !ignore_timeout { let delay = if !ignore_timeout {
// Construct delay task to wait for later // Construct delay task to wait for later
let total_delay = Some( let total_delay = Some(
cx.background() cx.background_executor()
.timer(Duration::from_millis(HOVER_DELAY_MILLIS)), .timer(Duration::from_millis(HOVER_DELAY_MILLIS)),
); );
cx.background() cx.background_executor()
.timer(Duration::from_millis(HOVER_REQUEST_DELAY_MILLIS)) .timer(Duration::from_millis(HOVER_REQUEST_DELAY_MILLIS))
.await; .await;
total_delay total_delay
@ -252,11 +249,11 @@ fn show_hover(
}; };
// query the LSP for hover info // query the LSP for hover info
let hover_request = cx.update(|cx| { let hover_request = cx.update(|_, cx| {
project.update(cx, |project, cx| { project.update(cx, |project, cx| {
project.hover(&buffer, buffer_position, cx) project.hover(&buffer, buffer_position, cx)
}) })
}); })?;
if let Some(delay) = delay { if let Some(delay) = delay {
delay.await; delay.await;
@ -310,7 +307,8 @@ fn show_hover(
anchor..anchor 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 blocks = hover_result.contents;
let language = hover_result.language; let language = hover_result.language;
let parsed_content = parse_blocks(&blocks, &language_registry, language).await; 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 // Highlight the selected symbol using a background highlight
this.highlight_background::<HoverState>( this.highlight_background::<HoverState>(
vec![symbol_range], vec![symbol_range],
|theme| theme.editor.hover_popover.highlight, |theme| theme.element_hover, // todo! update theme
cx, cx,
); );
} else { } else {
@ -423,9 +421,10 @@ impl HoverState {
snapshot: &EditorSnapshot, snapshot: &EditorSnapshot,
style: &EditorStyle, style: &EditorStyle,
visible_rows: Range<u32>, visible_rows: Range<u32>,
workspace: Option<WeakViewHandle<Workspace>>, max_size: Size<Pixels>,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) -> Option<(DisplayPoint, Vec<AnyElement<Editor>>)> { ) -> Option<(DisplayPoint, Vec<AnyElement>)> {
// If there is a diagnostic, position the popovers based on that. // If there is a diagnostic, position the popovers based on that.
// Otherwise use the start of the hover range // Otherwise use the start of the hover range
let anchor = self let anchor = self
@ -450,10 +449,10 @@ impl HoverState {
let mut elements = Vec::new(); let mut elements = Vec::new();
if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() { 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() { 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)) Some((point, elements))
@ -462,7 +461,7 @@ impl HoverState {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct InfoPopover { pub struct InfoPopover {
pub project: ModelHandle<Project>, pub project: Model<Project>,
symbol_range: RangeInEditor, symbol_range: RangeInEditor,
pub blocks: Vec<HoverBlock>, pub blocks: Vec<HoverBlock>,
parsed_content: ParsedMarkdown, parsed_content: ParsedMarkdown,
@ -472,29 +471,28 @@ impl InfoPopover {
pub fn render( pub fn render(
&mut self, &mut self,
style: &EditorStyle, style: &EditorStyle,
workspace: Option<WeakViewHandle<Workspace>>, max_size: Size<Pixels>,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) -> AnyElement<Editor> { ) -> AnyElement {
MouseEventHandler::new::<InfoPopover, _>(0, cx, |_, cx| { div()
Flex::column() .id("info_popover")
.scrollable::<HoverBlock>(0, None, cx) .elevation_2(cx)
.with_child(crate::render_parsed_markdown::<HoverBlock>( .p_2()
&self.parsed_content, .overflow_y_scroll()
style, .max_w(max_size.width)
workspace, .max_h(max_size.height)
cx, // Prevent a mouse move on the popover from being propagated to the editor,
)) // because that would dismiss the popover.
.contained() .on_mouse_move(|_, cx| cx.stop_propagation())
.with_style(style.hover_popover.container) .child(crate::render_parsed_markdown(
}) "content",
.on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath. &self.parsed_content,
.with_cursor_style(CursorStyle::Arrow) style,
.with_padding(Padding { workspace,
bottom: HOVER_POPOVER_GAP, cx,
top: HOVER_POPOVER_GAP, ))
..Default::default() .into_any_element()
})
.into_any()
} }
} }
@ -505,56 +503,74 @@ pub struct DiagnosticPopover {
} }
impl DiagnosticPopover { impl DiagnosticPopover {
pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement<Editor> { pub fn render(
enum PrimaryDiagnostic {} &self,
style: &EditorStyle,
let mut text_style = style.hover_popover.prose.clone(); max_size: Size<Pixels>,
text_style.font_size = style.text.font_size; cx: &mut ViewContext<Editor>,
let diagnostic_source_style = style.hover_popover.diagnostic_source_highlight.clone(); ) -> AnyElement {
let text = match &self.local_diagnostic.diagnostic.source { let text = match &self.local_diagnostic.diagnostic.source {
Some(source) => Text::new( Some(source) => format!("{source}: {}", self.local_diagnostic.diagnostic.message),
format!("{source}: {}", self.local_diagnostic.diagnostic.message), None => self.local_diagnostic.diagnostic.message.clone(),
text_style,
)
.with_highlights(vec![(0..source.len(), diagnostic_source_style)]),
None => Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style),
}; };
let container_style = match self.local_diagnostic.diagnostic.severity { struct DiagnosticColors {
DiagnosticSeverity::HINT => style.hover_popover.info_container, pub text: Hsla,
DiagnosticSeverity::INFORMATION => style.hover_popover.info_container, pub background: Hsla,
DiagnosticSeverity::WARNING => style.hover_popover.warning_container, pub border: Hsla,
DiagnosticSeverity::ERROR => style.hover_popover.error_container, }
_ => style.hover_popover.container,
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(); div()
.id("diagnostic")
MouseEventHandler::new::<DiagnosticPopover, _>(0, cx, |_, _| { .overflow_y_scroll()
text.with_soft_wrap(true) .px_2()
.contained() .py_1()
.with_style(container_style) .bg(diagnostic_colors.background)
}) .text_color(diagnostic_colors.text)
.with_padding(Padding { .border_1()
top: HOVER_POPOVER_GAP, .border_color(diagnostic_colors.border)
bottom: HOVER_POPOVER_GAP, .rounded_md()
..Default::default() .max_w(max_size.width)
}) .max_h(max_size.height)
.on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath. .cursor(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, this, cx| { .tooltip(move |cx| Tooltip::for_action("Go To Diagnostic", &crate::GoToDiagnostic, cx))
this.go_to_diagnostic(&Default::default(), cx) // Prevent a mouse move on the popover from being propagated to the editor,
}) // because that would dismiss the popover.
.with_cursor_style(CursorStyle::PointingHand) .on_mouse_move(|_, cx| cx.stop_propagation())
.with_tooltip::<PrimaryDiagnostic>( // Prevent a mouse down on the popover from being propagated to the editor,
0, // because that would move the cursor.
"Go To Diagnostic".to_string(), .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
Some(Box::new(crate::GoToDiagnostic)), .on_click(cx.listener(|editor, _, cx| editor.go_to_diagnostic(&Default::default(), cx)))
tooltip_style, .child(SharedString::from(text))
cx, .into_any_element()
)
.into_any()
} }
pub fn activation_info(&self) -> (usize, Anchor) { pub fn activation_info(&self) -> (usize, Anchor) {
@ -579,7 +595,7 @@ mod tests {
InlayId, InlayId,
}; };
use collections::BTreeSet; use collections::BTreeSet;
use gpui::fonts::{HighlightStyle, Underline, Weight}; use gpui::{FontWeight, HighlightStyle, UnderlineStyle};
use indoc::indoc; use indoc::indoc;
use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
use lsp::LanguageServerId; use lsp::LanguageServerId;
@ -626,7 +642,7 @@ mod tests {
range: Some(symbol_range), range: Some(symbol_range),
})) }))
}); });
cx.foreground() cx.background_executor
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
requests.next().await; requests.next().await;
@ -649,7 +665,7 @@ mod tests {
.lsp .lsp
.handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) }); .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx)); 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)); .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
request.next().await; request.next().await;
cx.editor(|editor, _| { cx.editor(|editor, _| {
@ -853,7 +869,7 @@ mod tests {
// Hover pops diagnostic immediately // Hover pops diagnostic immediately
cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); 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, .. }, _| { cx.editor(|Editor { hover_state, .. }, _| {
assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none()) assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none())
@ -872,10 +888,10 @@ mod tests {
range: Some(range), range: Some(range),
})) }))
}); });
cx.foreground() cx.background_executor
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); .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, .. }, _| { cx.editor(|Editor { hover_state, .. }, _| {
hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some() 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) { fn test_render_blocks(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
cx.add_window(|cx| { let editor = cx.add_window(|cx| Editor::single_line(cx));
let editor = Editor::single_line(None, cx); editor
let style = editor.style(cx); .update(cx, |editor, _cx| {
let style = editor.style.clone().unwrap();
struct Row { struct Row {
blocks: Vec<HoverBlock>, blocks: Vec<HoverBlock>,
expected_marked_text: String, expected_marked_text: String,
expected_styles: Vec<HighlightStyle>, expected_styles: Vec<HighlightStyle>,
} }
let rows = &[ let rows = &[
// Strong emphasis // Strong emphasis
Row { Row {
blocks: vec![HoverBlock { blocks: vec![HoverBlock {
text: "one **two** three".to_string(), text: "one **two** three".to_string(),
kind: HoverBlockKind::Markdown, kind: HoverBlockKind::Markdown,
}], }],
expected_marked_text: "one «two» three".to_string(), expected_marked_text: "one «two» three".to_string(),
expected_styles: vec![HighlightStyle { expected_styles: vec![HighlightStyle {
weight: Some(Weight::BOLD), font_weight: Some(FontWeight::BOLD),
..Default::default()
}],
},
// Links
Row {
blocks: vec![HoverBlock {
text: "one [two](https://the-url) three".to_string(),
kind: HoverBlockKind::Markdown,
}],
expected_marked_text: "one «two» three".to_string(),
expected_styles: vec![HighlightStyle {
underline: Some(Underline {
thickness: 1.0.into(),
..Default::default() ..Default::default()
}), }],
..Default::default() },
}], // Links
}, Row {
// Lists blocks: vec![HoverBlock {
Row { text: "one [two](https://the-url) three".to_string(),
blocks: vec![HoverBlock { kind: HoverBlockKind::Markdown,
text: " }],
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: lists:
* one * one
- a - a
@ -934,10 +951,10 @@ mod tests {
* two * two
- [c](https://the-url) - [c](https://the-url)
- d" - d"
.unindent(), .unindent(),
kind: HoverBlockKind::Markdown, kind: HoverBlockKind::Markdown,
}], }],
expected_marked_text: " expected_marked_text: "
lists: lists:
- one - one
- a - a
@ -945,19 +962,19 @@ mod tests {
- two - two
- «c» - «c»
- d" - d"
.unindent(), .unindent(),
expected_styles: vec![HighlightStyle { expected_styles: vec![HighlightStyle {
underline: Some(Underline { underline: Some(UnderlineStyle {
thickness: 1.0.into(), thickness: 1.0.into(),
..Default::default()
}),
..Default::default() ..Default::default()
}), }],
..Default::default() },
}], // Multi-paragraph list items
}, Row {
// Multi-paragraph list items blocks: vec![HoverBlock {
Row { text: "
blocks: vec![HoverBlock {
text: "
* one two * one two
three three
@ -968,10 +985,10 @@ mod tests {
nine nine
* ten * ten
* six" * six"
.unindent(), .unindent(),
kind: HoverBlockKind::Markdown, kind: HoverBlockKind::Markdown,
}], }],
expected_marked_text: " expected_marked_text: "
- one two three - one two three
- four five - four five
- six seven eight - six seven eight
@ -979,52 +996,51 @@ mod tests {
nine nine
- ten - ten
- six" - six"
.unindent(), .unindent(),
expected_styles: vec![HighlightStyle { expected_styles: vec![HighlightStyle {
underline: Some(Underline { underline: Some(UnderlineStyle {
thickness: 1.0.into(), thickness: 1.0.into(),
..Default::default()
}),
..Default::default() ..Default::default()
}), }],
..Default::default() },
}], ];
},
];
for Row { for Row {
blocks, blocks,
expected_marked_text, expected_marked_text,
expected_styles, expected_styles,
} in &rows[0..] } in &rows[0..]
{ {
let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
let expected_highlights = ranges let expected_highlights = ranges
.into_iter() .into_iter()
.zip(expected_styles.iter().cloned()) .zip(expected_styles.iter().cloned())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
assert_eq!( assert_eq!(
rendered.text, expected_text, rendered.text, expected_text,
"wrong text for input {blocks:?}" "wrong text for input {blocks:?}"
); );
let rendered_highlights: Vec<_> = rendered let rendered_highlights: Vec<_> = rendered
.highlights .highlights
.iter() .iter()
.filter_map(|(range, highlight)| { .filter_map(|(range, highlight)| {
let highlight = highlight.to_highlight_style(&style.syntax)?; let highlight = highlight.to_highlight_style(&style.syntax)?;
Some((range.clone(), highlight)) Some((range.clone(), highlight))
}) })
.collect(); .collect();
assert_eq!( assert_eq!(
rendered_highlights, expected_highlights, rendered_highlights, expected_highlights,
"wrong highlights for input {blocks:?}" "wrong highlights for input {blocks:?}"
); );
} }
})
editor .unwrap();
});
} }
#[gpui::test] #[gpui::test]
@ -1127,7 +1143,7 @@ mod tests {
}) })
.next() .next()
.await; .await;
cx.foreground().run_until_parked(); cx.background_executor.run_until_parked();
cx.update_editor(|editor, cx| { cx.update_editor(|editor, cx| {
let expected_layers = vec![entire_hint_label.to_string()]; let expected_layers = vec![entire_hint_label.to_string()];
assert_eq!(expected_layers, cached_hint_labels(editor)); assert_eq!(expected_layers, cached_hint_labels(editor));
@ -1236,7 +1252,7 @@ mod tests {
) )
.next() .next()
.await; .await;
cx.foreground().run_until_parked(); cx.background_executor.run_until_parked();
cx.update_editor(|editor, cx| { cx.update_editor(|editor, cx| {
update_inlay_link_and_hover_points( update_inlay_link_and_hover_points(
@ -1248,9 +1264,9 @@ mod tests {
cx, cx,
); );
}); });
cx.foreground() cx.background_executor
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); .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| { cx.update_editor(|editor, cx| {
let hover_state = &editor.hover_state; let hover_state = &editor.hover_state;
assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
@ -1301,9 +1317,9 @@ mod tests {
cx, cx,
); );
}); });
cx.foreground() cx.background_executor
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); .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| { cx.update_editor(|editor, cx| {
let hover_state = &editor.hover_state; let hover_state = &editor.hover_state;
assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,15 @@
use crate::{ use crate::{
editor_settings::SeedQuerySetting, link_go_to_definition::hide_link_definition, 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 _, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
}; };
use anyhow::{Context, Result}; use anyhow::{anyhow, Context as _, Result};
use collections::HashSet; use collections::HashSet;
use futures::future::try_join_all; use futures::future::try_join_all;
use gpui::{ use gpui::{
elements::*, div, point, AnyElement, AppContext, AsyncWindowContext, Context, Entity, EntityId,
geometry::vector::{vec2f, Vector2F}, EventEmitter, IntoElement, Model, ParentElement, Pixels, Render, SharedString, Styled,
AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View, ViewContext, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
ViewHandle, WeakViewHandle,
}; };
use language::{ use language::{
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt, 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 project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
use rpc::proto::{self, update_view, PeerId}; use rpc::proto::{self, update_view, PeerId};
use smallvec::SmallVec; use settings::Settings;
use std::fmt::Write;
use std::{ use std::{
borrow::Cow, borrow::Cow,
cmp::{self, Ordering}, cmp::{self, Ordering},
fmt::Write,
iter, iter,
ops::Range, ops::Range,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
}; };
use text::Selection; use text::Selection;
use util::{ use theme::{ActiveTheme, Theme};
paths::{PathExt, FILE_ROW_COLUMN_DELIMITER}, use ui::{h_stack, prelude::*, Label};
ResultExt, TryFutureExt, use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt};
};
use workspace::item::{BreadcrumbText, FollowableItemHandle, ItemHandle};
use workspace::{ 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}, searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
ItemId, ItemNavHistory, Pane, StatusItemView, ToolbarItemLocation, ViewId, Workspace, ItemId, ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
WorkspaceId,
}; };
pub const MAX_TAB_TITLE_LEN: usize = 24; pub const MAX_TAB_TITLE_LEN: usize = 24;
@ -49,12 +50,12 @@ impl FollowableItem for Editor {
} }
fn from_state_proto( fn from_state_proto(
pane: ViewHandle<workspace::Pane>, pane: View<workspace::Pane>,
workspace: ViewHandle<Workspace>, workspace: View<Workspace>,
remote_id: ViewId, remote_id: ViewId,
state: &mut Option<proto::view::Variant>, state: &mut Option<proto::view::Variant>,
cx: &mut AppContext, cx: &mut WindowContext,
) -> Option<Task<Result<ViewHandle<Self>>>> { ) -> Option<Task<Result<View<Self>>>> {
let project = workspace.read(cx).project().to_owned(); let project = workspace.read(cx).project().to_owned();
let Some(proto::view::Variant::Editor(_)) = state else { let Some(proto::view::Variant::Editor(_)) = state else {
return None; return None;
@ -80,7 +81,7 @@ impl FollowableItem for Editor {
let pane = pane.downgrade(); let pane = pane.downgrade();
Some(cx.spawn(|mut cx| async move { Some(cx.spawn(|mut cx| async move {
let mut buffers = futures::future::try_join_all(buffers).await?; 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>(); let mut editors = pane.items_of_type::<Self>();
editors.find(|editor| { editors.find(|editor| {
let ids_match = editor.remote_id(&client, cx) == Some(remote_id); let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
@ -95,7 +96,7 @@ impl FollowableItem for Editor {
editor editor
} else { } else {
pane.update(&mut cx, |_, cx| { pane.update(&mut cx, |_, cx| {
let multibuffer = cx.add_model(|cx| { let multibuffer = cx.new_model(|cx| {
let mut multibuffer; let mut multibuffer;
if state.singleton && buffers.len() == 1 { if state.singleton && buffers.len() == 1 {
multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx) multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
@ -128,7 +129,7 @@ impl FollowableItem for Editor {
multibuffer multibuffer
}); });
cx.add_view(|cx| { cx.new_view(|cx| {
let mut editor = let mut editor =
Editor::for_multibuffer(multibuffer, Some(project.clone()), cx); Editor::for_multibuffer(multibuffer, Some(project.clone()), cx);
editor.remote_id = Some(remote_id); editor.remote_id = Some(remote_id);
@ -162,22 +163,20 @@ impl FollowableItem for Editor {
self.buffer.update(cx, |buffer, cx| { self.buffer.update(cx, |buffer, cx| {
buffer.remove_active_selections(cx); buffer.remove_active_selections(cx);
}); });
} else { } else if self.focus_handle.is_focused(cx) {
self.buffer.update(cx, |buffer, cx| { self.buffer.update(cx, |buffer, cx| {
if self.focused { buffer.set_active_selections(
buffer.set_active_selections( &self.selections.disjoint_anchors(),
&self.selections.disjoint_anchors(), self.selections.line_mode,
self.selections.line_mode, self.cursor_shape,
self.cursor_shape, cx,
cx, );
);
}
}); });
} }
cx.notify(); 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 buffer = self.buffer.read(cx);
let scroll_anchor = self.scroll_manager.anchor(); let scroll_anchor = self.scroll_manager.anchor();
let excerpts = buffer let excerpts = buffer
@ -204,8 +203,8 @@ impl FollowableItem for Editor {
title: (!buffer.is_singleton()).then(|| buffer.title(cx).into()), title: (!buffer.is_singleton()).then(|| buffer.title(cx).into()),
excerpts, excerpts,
scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.anchor)), scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.anchor)),
scroll_x: scroll_anchor.offset.x(), scroll_x: scroll_anchor.offset.x,
scroll_y: scroll_anchor.offset.y(), scroll_y: scroll_anchor.offset.y,
selections: self selections: self
.selections .selections
.disjoint_anchors() .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( fn add_event_to_update_proto(
&self, &self,
event: &Self::Event, event: &EditorEvent,
update: &mut Option<proto::update_view::Variant>, update: &mut Option<proto::update_view::Variant>,
cx: &AppContext, cx: &WindowContext,
) -> bool { ) -> bool {
let update = let update =
update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default())); update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default()));
match update { match update {
proto::update_view::Variant::Editor(update) => match event { proto::update_view::Variant::Editor(update) => match event {
Event::ExcerptsAdded { EditorEvent::ExcerptsAdded {
buffer, buffer,
predecessor, predecessor,
excerpts, excerpts,
@ -252,20 +266,20 @@ impl FollowableItem for Editor {
} }
true true
} }
Event::ExcerptsRemoved { ids } => { EditorEvent::ExcerptsRemoved { ids } => {
update update
.deleted_excerpts .deleted_excerpts
.extend(ids.iter().map(ExcerptId::to_proto)); .extend(ids.iter().map(ExcerptId::to_proto));
true true
} }
Event::ScrollPositionChanged { .. } => { EditorEvent::ScrollPositionChanged { .. } => {
let scroll_anchor = self.scroll_manager.anchor(); let scroll_anchor = self.scroll_manager.anchor();
update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.anchor)); update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.anchor));
update.scroll_x = scroll_anchor.offset.x(); update.scroll_x = scroll_anchor.offset.x;
update.scroll_y = scroll_anchor.offset.y(); update.scroll_y = scroll_anchor.offset.y;
true true
} }
Event::SelectionsChanged { .. } => { EditorEvent::SelectionsChanged { .. } => {
update.selections = self update.selections = self
.selections .selections
.disjoint_anchors() .disjoint_anchors()
@ -286,7 +300,7 @@ impl FollowableItem for Editor {
fn apply_update_proto( fn apply_update_proto(
&mut self, &mut self,
project: &ModelHandle<Project>, project: &Model<Project>,
message: update_view::Variant, message: update_view::Variant,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
@ -297,25 +311,16 @@ impl FollowableItem for Editor {
}) })
} }
fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool { fn is_project_item(&self, _cx: &WindowContext) -> bool {
match event {
Event::Edited => true,
Event::SelectionsChanged { local } => *local,
Event::ScrollPositionChanged { local, .. } => *local,
_ => false,
}
}
fn is_project_item(&self, _cx: &AppContext) -> bool {
true true
} }
} }
async fn update_editor_from_message( async fn update_editor_from_message(
this: WeakViewHandle<Editor>, this: WeakView<Editor>,
project: ModelHandle<Project>, project: Model<Project>,
message: proto::update_view::Editor, message: proto::update_view::Editor,
cx: &mut AsyncAppContext, cx: &mut AsyncWindowContext,
) -> Result<()> { ) -> Result<()> {
// Open all of the buffers of which excerpts were added to the editor. // Open all of the buffers of which excerpts were added to the editor.
let inserted_excerpt_buffer_ids = message let inserted_excerpt_buffer_ids = message
@ -328,7 +333,7 @@ async fn update_editor_from_message(
.into_iter() .into_iter()
.map(|id| project.open_buffer_by_id(id, cx)) .map(|id| project.open_buffer_by_id(id, cx))
.collect::<Vec<_>>() .collect::<Vec<_>>()
}); })?;
let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?; let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?;
// Update the editor's excerpts. // Update the editor's excerpts.
@ -353,7 +358,7 @@ async fn update_editor_from_message(
continue; continue;
}; };
let buffer_id = excerpt.buffer_id; 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; continue;
}; };
@ -430,7 +435,7 @@ async fn update_editor_from_message(
editor.set_scroll_anchor_remote( editor.set_scroll_anchor_remote(
ScrollAnchor { ScrollAnchor {
anchor: scroll_top_anchor, anchor: scroll_top_anchor,
offset: vec2f(message.scroll_x, message.scroll_y), offset: point(message.scroll_x, message.scroll_y),
}, },
cx, cx,
); );
@ -516,6 +521,8 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor)
} }
impl Item for Editor { impl Item for Editor {
type Event = EditorEvent;
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool { fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
if let Ok(data) = data.downcast::<NavigationData>() { if let Ok(data) = data.downcast::<NavigationData>() {
let newest_selection = self.selections.newest::<Point>(cx); 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 let file_path = self
.buffer() .buffer()
.read(cx) .read(cx)
@ -566,53 +573,66 @@ impl Item for Editor {
Some(file_path.into()) Some(file_path.into())
} }
fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<str>> { fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<SharedString> {
match path_for_buffer(&self.buffer, detail, true, cx)? { let path = path_for_buffer(&self.buffer, detail, true, cx)?;
Cow::Borrowed(path) => Some(path.to_string_lossy()), Some(path.to_string_lossy().to_string().into())
Cow::Owned(path) => Some(path.to_string_lossy().to_string().into()),
}
} }
fn tab_content<T: 'static>( fn tab_content(&self, detail: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement {
&self, let _theme = cx.theme();
detail: Option<usize>,
style: &theme::Tab, let description = detail.and_then(|detail| {
cx: &AppContext, let path = path_for_buffer(&self.buffer, detail, false, cx)?;
) -> AnyElement<T> { let description = path.to_string_lossy();
Flex::row() let description = description.trim();
.with_child(Label::new(self.title(cx).to_string(), style.label.clone()).into_any())
.with_children(detail.and_then(|detail| { if description.is_empty() {
let path = path_for_buffer(&self.buffer, detail, false, cx)?; return None;
let description = path.to_string_lossy(); }
Some(
Label::new( Some(util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN))
util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN), });
style.description.text.clone(),
) h_stack()
.contained() .gap_2()
.with_style(style.description.container) .child(Label::new(self.title(cx).to_string()).color(if selected {
.aligned(), Color::Default
) } else {
Color::Muted
})) }))
.align_children_center() .when_some(description, |this, description| {
.into_any() 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 self.buffer
.read(cx) .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 { fn is_singleton(&self, cx: &AppContext) -> bool {
self.buffer.read(cx).is_singleton() 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 where
Self: Sized, 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>) { fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
@ -646,11 +666,7 @@ impl Item for Editor {
} }
} }
fn save( fn save(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
&mut self,
project: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
self.report_editor_event("save", None, cx); self.report_editor_event("save", None, cx);
let format = self.perform_format(project.clone(), FormatTrigger::Save, cx); let format = self.perform_format(project.clone(), FormatTrigger::Save, cx);
let buffers = self.buffer().clone().read(cx).all_buffers(); let buffers = self.buffer().clone().read(cx).all_buffers();
@ -659,28 +675,34 @@ impl Item for Editor {
if buffers.len() == 1 { if buffers.len() == 1 {
project project
.update(&mut cx, |project, cx| project.save_buffers(buffers, cx)) .update(&mut cx, |project, cx| project.save_buffers(buffers, cx))?
.await?; .await?;
} else { } else {
// For multi-buffers, only save those ones that contain changes. For clean buffers // 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 // we simulate saving by calling `Buffer::did_save`, so that language servers or
// other downstream listeners of save events get notified. // other downstream listeners of save events get notified.
let (dirty_buffers, clean_buffers) = buffers.into_iter().partition(|buffer| { 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 project
.update(&mut cx, |project, cx| { .update(&mut cx, |project, cx| {
project.save_buffers(dirty_buffers, cx) project.save_buffers(dirty_buffers, cx)
}) })?
.await?; .await?;
for buffer in clean_buffers { for buffer in clean_buffers {
buffer.update(&mut cx, |buffer, cx| { buffer
let version = buffer.saved_version().clone(); .update(&mut cx, |buffer, cx| {
let fingerprint = buffer.saved_version_fingerprint(); let version = buffer.saved_version().clone();
let mtime = buffer.saved_mtime(); let fingerprint = buffer.saved_version_fingerprint();
buffer.did_save(version, fingerprint, mtime, cx); let mtime = buffer.saved_mtime();
}); buffer.did_save(version, fingerprint, mtime, cx);
})
.ok();
} }
} }
@ -690,7 +712,7 @@ impl Item for Editor {
fn save_as( fn save_as(
&mut self, &mut self,
project: ModelHandle<Project>, project: Model<Project>,
abs_path: PathBuf, abs_path: PathBuf,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
@ -710,11 +732,7 @@ impl Item for Editor {
}) })
} }
fn reload( fn reload(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
&mut self,
project: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
let buffer = self.buffer().clone(); let buffer = self.buffer().clone();
let buffers = self.buffer.read(cx).all_buffers(); let buffers = self.buffer.read(cx).all_buffers();
let reload_buffers = let reload_buffers =
@ -724,60 +742,36 @@ impl Item for Editor {
this.update(&mut cx, |editor, cx| { this.update(&mut cx, |editor, cx| {
editor.request_autoscroll(Autoscroll::fit(), cx) editor.request_autoscroll(Autoscroll::fit(), cx)
})?; })?;
buffer.update(&mut cx, |buffer, cx| { buffer
if let Some(transaction) = transaction { .update(&mut cx, |buffer, cx| {
if !buffer.is_singleton() { if let Some(transaction) = transaction {
buffer.push_transaction(&transaction.0, cx); if !buffer.is_singleton() {
buffer.push_transaction(&transaction.0, cx);
}
} }
} })
}); .ok();
Ok(()) Ok(())
}) })
} }
fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
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>> {
Some(Box::new(handle.clone())) 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 self.pixel_position_of_newest_cursor
} }
fn breadcrumb_location(&self) -> ToolbarItemLocation { 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 cursor = self.selections.newest_anchor().head();
let multibuffer = &self.buffer().read(cx); let multibuffer = &self.buffer().read(cx);
let (buffer_id, symbols) = 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 = multibuffer.buffer(buffer_id)?;
let buffer = buffer.read(cx); 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>) { fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
let workspace_id = workspace.database_id(); 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())); self.workspace = Some((workspace.weak_handle(), workspace.database_id()));
fn serialize( fn serialize(
buffer: ModelHandle<Buffer>, buffer: Model<Buffer>,
workspace_id: WorkspaceId, workspace_id: WorkspaceId,
item_id: ItemId, item_id: ItemId,
cx: &mut AppContext, 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()) { if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
let path = file.abs_path(cx); let path = file.abs_path(cx);
cx.background() cx.background_executor()
.spawn(async move { .spawn(async move {
DB.save_path(item_id, workspace_id, path.clone()) DB.save_path(item_id, workspace_id, path.clone())
.await .await
@ -834,7 +828,12 @@ impl Item for Editor {
cx.subscribe(&buffer, |this, buffer, event, cx| { cx.subscribe(&buffer, |this, buffer, event, cx| {
if let Some((_, workspace_id)) = this.workspace.as_ref() { if let Some((_, workspace_id)) = this.workspace.as_ref() {
if let language::Event::FileHandleChanged = event { 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") 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( fn deserialize(
project: ModelHandle<Project>, project: Model<Project>,
_workspace: WeakViewHandle<Workspace>, _workspace: WeakView<Workspace>,
workspace_id: workspace::WorkspaceId, workspace_id: workspace::WorkspaceId,
item_id: ItemId, item_id: ItemId,
cx: &mut ViewContext<Pane>, cx: &mut ViewContext<Pane>,
) -> Task<Result<ViewHandle<Self>>> { ) -> Task<Result<View<Self>>> {
let project_item: Result<_> = project.update(cx, |project, cx| { let project_item: Result<_> = project.update(cx, |project, cx| {
// Look up the path with this key associated, create a self with that path // Look up the path with this key associated, create a self with that path
let path = DB let path = DB
@ -876,10 +909,11 @@ impl Item for Editor {
let (_, project_item) = project_item.await?; let (_, project_item) = project_item.await?;
let buffer = project_item let buffer = project_item
.downcast::<Buffer>() .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| { Ok(pane.update(&mut cx, |_, cx| {
cx.add_view(|cx| { cx.new_view(|cx| {
let mut editor = Editor::for_buffer(buffer, Some(project), cx); let mut editor = Editor::for_buffer(buffer, Some(project), cx);
editor.read_scroll_position_from_db(item_id, workspace_id, cx); editor.read_scroll_position_from_db(item_id, workspace_id, cx);
editor editor
}) })
@ -894,36 +928,20 @@ impl ProjectItem for Editor {
type Item = Buffer; type Item = Buffer;
fn for_project_item( fn for_project_item(
project: ModelHandle<Project>, project: Model<Project>,
buffer: ModelHandle<Buffer>, buffer: Model<Buffer>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Self { ) -> Self {
Self::for_buffer(buffer, Some(project), cx) Self::for_buffer(buffer, Some(project), cx)
} }
} }
impl EventEmitter<SearchEvent> for Editor {}
pub(crate) enum BufferSearchHighlights {} pub(crate) enum BufferSearchHighlights {}
impl SearchableItem for Editor { impl SearchableItem for Editor {
type Match = Range<Anchor>; 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>) { fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
self.clear_background_highlights::<BufferSearchHighlights>(cx); 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>) { fn update_matches(&mut self, matches: Vec<Range<Anchor>>, cx: &mut ViewContext<Self>) {
self.highlight_background::<BufferSearchHighlights>( self.highlight_background::<BufferSearchHighlights>(
matches, matches,
|theme| theme.search.match_background, |theme| theme.search_match_background,
cx, cx,
); );
} }
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String { 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 snapshot = &self.snapshot(cx).buffer_snapshot;
let selection = self.selections.newest::<usize>(cx); let selection = self.selections.newest::<usize>(cx);
@ -1060,7 +1078,7 @@ impl SearchableItem for Editor {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<Vec<Range<Anchor>>> { ) -> Task<Vec<Range<Anchor>>> {
let buffer = self.buffer().read(cx).snapshot(cx); let buffer = self.buffer().read(cx).snapshot(cx);
cx.background().spawn(async move { cx.background_executor().spawn(async move {
let mut ranges = Vec::new(); let mut ranges = Vec::new();
if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() { if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
ranges.extend( 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 editor = editor.read(cx);
let buffer = editor.buffer().read(cx).snapshot(cx); let buffer = editor.buffer().read(cx).snapshot(cx);
@ -1174,18 +1192,9 @@ impl CursorPosition {
} }
} }
impl Entity for CursorPosition { impl Render for CursorPosition {
type Event = (); fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
} div().when_some(self.position, |el, position| {
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;
let mut text = format!( let mut text = format!(
"{}{FILE_ROW_COLUMN_DELIMITER}{}", "{}{FILE_ROW_COLUMN_DELIMITER}{}",
position.row + 1, position.row + 1,
@ -1194,10 +1203,9 @@ impl View for CursorPosition {
if self.selected_count > 0 { if self.selected_count > 0 {
write!(text, " ({} selected)", self.selected_count).unwrap(); write!(text, " ({} selected)", self.selected_count).unwrap();
} }
Label::new(text, theme.cursor_position.clone()).into_any()
} else { el.child(Label::new(text).size(LabelSize::Small))
Empty::new().into_any() })
}
} }
} }
@ -1220,7 +1228,7 @@ impl StatusItemView for CursorPosition {
} }
fn path_for_buffer<'a>( fn path_for_buffer<'a>(
buffer: &ModelHandle<MultiBuffer>, buffer: &Model<MultiBuffer>,
height: usize, height: usize,
include_filename: bool, include_filename: bool,
cx: &'a AppContext, cx: &'a AppContext,

View File

@ -2,9 +2,10 @@ use crate::{
display_map::DisplaySnapshot, display_map::DisplaySnapshot,
element::PointForPosition, element::PointForPosition,
hover_popover::{self, InlayHover}, 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 language::{Bias, ToOffset};
use lsp::LanguageServerId; use lsp::LanguageServerId;
use project::{ use project::{
@ -12,6 +13,7 @@ use project::{
ResolveState, ResolveState,
}; };
use std::ops::Range; use std::ops::Range;
use theme::ActiveTheme as _;
use util::TryFutureExt; use util::TryFutureExt;
#[derive(Debug, Default)] #[derive(Debug, Default)]
@ -168,7 +170,7 @@ pub fn update_inlay_link_and_hover_points(
editor: &mut Editor, editor: &mut Editor,
cmd_held: bool, cmd_held: bool,
shift_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 { 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)) 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 { let result = match &trigger_point {
TriggerPoint::Text(_) => { TriggerPoint::Text(_) => {
// query the LSP for definition info // query the LSP for definition info
cx.update(|cx| { project
project.update(cx, |project, cx| match definition_kind { .update(&mut cx, |project, cx| match definition_kind {
LinkDefinitionKind::Symbol => { LinkDefinitionKind::Symbol => {
project.definition(&buffer, buffer_position, cx) project.definition(&buffer, buffer_position, cx)
} }
@ -405,29 +407,30 @@ pub fn show_link_definition(
LinkDefinitionKind::Type => { LinkDefinitionKind::Type => {
project.type_definition(&buffer, buffer_position, cx) 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(( TriggerPoint::InlayHint(highlight, lsp_location, server_id) => Some((
Some(RangeInEditor::Inlay(highlight.clone())), Some(RangeInEditor::Inlay(highlight.clone())),
@ -483,8 +486,14 @@ pub fn show_link_definition(
}); });
if any_definition_does_not_contain_current_location { if any_definition_does_not_contain_current_location {
// Highlight symbol using theme link definition highlight style let style = gpui::HighlightStyle {
let style = theme::current(cx).editor.link_definition; underline: Some(gpui::UnderlineStyle {
thickness: px(1.),
..Default::default()
}),
color: Some(cx.theme().colors().link_text_hover),
..Default::default()
};
let highlight_range = let highlight_range =
symbol_range.unwrap_or_else(|| match &trigger_point { symbol_range.unwrap_or_else(|| match &trigger_point {
TriggerPoint::Text(trigger_anchor) => { TriggerPoint::Text(trigger_anchor) => {
@ -575,8 +584,8 @@ fn go_to_fetched_definition_of_kind(
let is_correct_kind = cached_definitions_kind == Some(kind); let is_correct_kind = cached_definitions_kind == Some(kind);
if !cached_definitions.is_empty() && is_correct_kind { if !cached_definitions.is_empty() && is_correct_kind {
if !editor.focused { if !editor.focus_handle.is_focused(cx) {
cx.focus_self(); cx.focus(&editor.focus_handle);
} }
editor.navigate_to_definitions(cached_definitions, split, cx); 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() { if point.as_valid().is_some() {
match kind { match kind {
LinkDefinitionKind::Symbol => editor.go_to_definition(&Default::default(), cx), LinkDefinitionKind::Symbol => editor.go_to_definition(&GoToDefinition, cx),
LinkDefinitionKind::Type => editor.go_to_type_definition(&Default::default(), cx), LinkDefinitionKind::Type => editor.go_to_type_definition(&GoToTypeDefinition, cx),
} }
} }
} }
@ -609,10 +618,7 @@ mod tests {
test::editor_lsp_test_context::EditorLspTestContext, test::editor_lsp_test_context::EditorLspTestContext,
}; };
use futures::StreamExt; use futures::StreamExt;
use gpui::{ use gpui::{Modifiers, ModifiersChangedEvent};
platform::{self, Modifiers, ModifiersChangedEvent},
View,
};
use indoc::indoc; use indoc::indoc;
use language::language_settings::InlayHintSettings; use language::language_settings::InlayHintSettings;
use lsp::request::{GotoDefinition, GotoTypeDefinition}; use lsp::request::{GotoDefinition, GotoTypeDefinition};
@ -674,7 +680,7 @@ mod tests {
); );
}); });
requests.next().await; requests.next().await;
cx.foreground().run_until_parked(); cx.background_executor.run_until_parked();
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {" cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
struct A; struct A;
let «variable» = A; let «variable» = A;
@ -682,10 +688,11 @@ mod tests {
// Unpress shift causes highlight to go away (normal goto-definition is not valid here) // Unpress shift causes highlight to go away (normal goto-definition is not valid here)
cx.update_editor(|editor, cx| { cx.update_editor(|editor, cx| {
editor.modifiers_changed( crate::element::EditorElement::modifiers_changed(
&platform::ModifiersChangedEvent { editor,
&ModifiersChangedEvent {
modifiers: Modifiers { modifiers: Modifiers {
cmd: true, command: true,
..Default::default() ..Default::default()
}, },
..Default::default() ..Default::default()
@ -725,7 +732,7 @@ mod tests {
go_to_fetched_type_definition(editor, PointForPosition::valid(hover_point), false, cx); go_to_fetched_type_definition(editor, PointForPosition::valid(hover_point), false, cx);
}); });
requests.next().await; requests.next().await;
cx.foreground().run_until_parked(); cx.background_executor.run_until_parked();
cx.assert_editor_state(indoc! {" cx.assert_editor_state(indoc! {"
struct «»; struct «»;
@ -747,23 +754,23 @@ mod tests {
.await; .await;
cx.set_state(indoc! {" cx.set_state(indoc! {"
fn ˇtest() { do_work(); } fn ˇtest() { do_work(); }
fn do_work() { test(); } fn do_work() { test(); }
"}); "});
// Basic hold cmd, expect highlight in region if response contains definition // Basic hold cmd, expect highlight in region if response contains definition
let hover_point = cx.display_point(indoc! {" let hover_point = cx.display_point(indoc! {"
fn test() { do_wˇork(); } fn test() { do_wˇork(); }
fn do_work() { test(); } fn do_work() { test(); }
"}); "});
let symbol_range = cx.lsp_range(indoc! {" let symbol_range = cx.lsp_range(indoc! {"
fn test() { «do_work»(); } fn test() { «do_work»(); }
fn do_work() { test(); } fn do_work() { test(); }
"}); "});
let target_range = cx.lsp_range(indoc! {" let target_range = cx.lsp_range(indoc! {"
fn test() { do_work(); } fn test() { do_work(); }
fn «do_work»() { test(); } fn «do_work»() { test(); }
"}); "});
let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move { let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
@ -786,22 +793,22 @@ mod tests {
); );
}); });
requests.next().await; requests.next().await;
cx.foreground().run_until_parked(); cx.background_executor.run_until_parked();
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {" cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
fn test() { «do_work»(); } fn test() { «do_work»(); }
fn do_work() { test(); } fn do_work() { test(); }
"}); "});
// Unpress cmd causes highlight to go away // Unpress cmd causes highlight to go away
cx.update_editor(|editor, cx| { cx.update_editor(|editor, cx| {
editor.modifiers_changed(&Default::default(), cx); crate::element::EditorElement::modifiers_changed(editor, &Default::default(), cx);
}); });
// Assert no link highlights // Assert no link highlights
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {" cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
fn test() { do_work(); } fn test() { do_work(); }
fn do_work() { test(); } fn do_work() { test(); }
"}); "});
// Response without source range still highlights word // Response without source range still highlights word
cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_trigger_point = None); cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_trigger_point = None);
@ -826,18 +833,18 @@ mod tests {
); );
}); });
requests.next().await; requests.next().await;
cx.foreground().run_until_parked(); cx.background_executor.run_until_parked();
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {" cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
fn test() { «do_work»(); } fn test() { «do_work»(); }
fn do_work() { test(); } fn do_work() { test(); }
"}); "});
// Moving mouse to location with no response dismisses highlight // Moving mouse to location with no response dismisses highlight
let hover_point = cx.display_point(indoc! {" let hover_point = cx.display_point(indoc! {"
fˇn test() { do_work(); } fˇn test() { do_work(); }
fn do_work() { test(); } fn do_work() { test(); }
"}); "});
let mut requests = cx let mut requests = cx
.lsp .lsp
.handle_request::<GotoDefinition, _, _>(move |_, _| async move { .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
@ -854,19 +861,19 @@ mod tests {
); );
}); });
requests.next().await; requests.next().await;
cx.foreground().run_until_parked(); cx.background_executor.run_until_parked();
// Assert no link highlights // Assert no link highlights
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {" cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
fn test() { do_work(); } fn test() { do_work(); }
fn do_work() { test(); } fn do_work() { test(); }
"}); "});
// Move mouse without cmd and then pressing cmd triggers highlight // Move mouse without cmd and then pressing cmd triggers highlight
let hover_point = cx.display_point(indoc! {" let hover_point = cx.display_point(indoc! {"
fn test() { do_work(); } fn test() { do_work(); }
fn do_work() { teˇst(); } fn do_work() { teˇst(); }
"}); "});
cx.update_editor(|editor, cx| { cx.update_editor(|editor, cx| {
update_go_to_definition_link( update_go_to_definition_link(
editor, editor,
@ -876,22 +883,22 @@ mod tests {
cx, cx,
); );
}); });
cx.foreground().run_until_parked(); cx.background_executor.run_until_parked();
// Assert no link highlights // Assert no link highlights
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {" cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
fn test() { do_work(); } fn test() { do_work(); }
fn do_work() { test(); } fn do_work() { test(); }
"}); "});
let symbol_range = cx.lsp_range(indoc! {" let symbol_range = cx.lsp_range(indoc! {"
fn test() { do_work(); } fn test() { do_work(); }
fn do_work() { «test»(); } fn do_work() { «test»(); }
"}); "});
let target_range = cx.lsp_range(indoc! {" let target_range = cx.lsp_range(indoc! {"
fn «test»() { do_work(); } fn «test»() { do_work(); }
fn do_work() { test(); } fn do_work() { test(); }
"}); "});
let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move { let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
@ -904,10 +911,11 @@ mod tests {
]))) ])))
}); });
cx.update_editor(|editor, cx| { cx.update_editor(|editor, cx| {
editor.modifiers_changed( crate::element::EditorElement::modifiers_changed(
editor,
&ModifiersChangedEvent { &ModifiersChangedEvent {
modifiers: Modifiers { modifiers: Modifiers {
cmd: true, command: true,
..Default::default() ..Default::default()
}, },
}, },
@ -915,21 +923,21 @@ mod tests {
); );
}); });
requests.next().await; requests.next().await;
cx.foreground().run_until_parked(); cx.background_executor.run_until_parked();
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {" cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
fn test() { do_work(); } fn test() { do_work(); }
fn do_work() { «test»(); } fn do_work() { «test»(); }
"}); "});
// Deactivating the window dismisses the highlight // Deactivating the window dismisses the highlight
cx.update_workspace(|workspace, cx| { 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! {" cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
fn test() { do_work(); } fn test() { do_work(); }
fn do_work() { test(); } fn do_work() { test(); }
"}); "});
// Moving the mouse restores the highlights. // Moving the mouse restores the highlights.
cx.update_editor(|editor, cx| { cx.update_editor(|editor, cx| {
@ -941,17 +949,17 @@ mod tests {
cx, cx,
); );
}); });
cx.foreground().run_until_parked(); cx.background_executor.run_until_parked();
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {" cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
fn test() { do_work(); } fn test() { do_work(); }
fn do_work() { «test»(); } fn do_work() { «test»(); }
"}); "});
// Moving again within the same symbol range doesn't re-request // Moving again within the same symbol range doesn't re-request
let hover_point = cx.display_point(indoc! {" let hover_point = cx.display_point(indoc! {"
fn test() { do_work(); } fn test() { do_work(); }
fn do_work() { tesˇt(); } fn do_work() { tesˇt(); }
"}); "});
cx.update_editor(|editor, cx| { cx.update_editor(|editor, cx| {
update_go_to_definition_link( update_go_to_definition_link(
editor, editor,
@ -961,11 +969,11 @@ mod tests {
cx, cx,
); );
}); });
cx.foreground().run_until_parked(); cx.background_executor.run_until_parked();
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {" cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
fn test() { do_work(); } fn test() { do_work(); }
fn do_work() { «test»(); } fn do_work() { «test»(); }
"}); "});
// Cmd click with existing definition doesn't re-request and dismisses highlight // Cmd click with existing definition doesn't re-request and dismisses highlight
cx.update_editor(|editor, cx| { cx.update_editor(|editor, cx| {
@ -978,27 +986,27 @@ mod tests {
// the cached location instead // the cached location instead
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![]))) Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
}); });
cx.foreground().run_until_parked(); cx.background_executor.run_until_parked();
cx.assert_editor_state(indoc! {" cx.assert_editor_state(indoc! {"
fn «testˇ»() { do_work(); } fn «testˇ»() { do_work(); }
fn do_work() { test(); } fn do_work() { test(); }
"}); "});
// Assert no link highlights after jump // Assert no link highlights after jump
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {" cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
fn test() { do_work(); } fn test() { do_work(); }
fn do_work() { test(); } fn do_work() { test(); }
"}); "});
// Cmd click without existing definition requests and jumps // Cmd click without existing definition requests and jumps
let hover_point = cx.display_point(indoc! {" let hover_point = cx.display_point(indoc! {"
fn test() { do_wˇork(); } fn test() { do_wˇork(); }
fn do_work() { test(); } fn do_work() { test(); }
"}); "});
let target_range = cx.lsp_range(indoc! {" let target_range = cx.lsp_range(indoc! {"
fn test() { do_work(); } fn test() { do_work(); }
fn «do_work»() { test(); } fn «do_work»() { test(); }
"}); "});
let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move { let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
@ -1014,22 +1022,22 @@ mod tests {
go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx); go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx);
}); });
requests.next().await; requests.next().await;
cx.foreground().run_until_parked(); cx.background_executor.run_until_parked();
cx.assert_editor_state(indoc! {" cx.assert_editor_state(indoc! {"
fn test() { do_work(); } fn test() { do_work(); }
fn «do_workˇ»() { test(); } 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 // 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 // 2. Selection is completed, hovering
let hover_point = cx.display_point(indoc! {" let hover_point = cx.display_point(indoc! {"
fn test() { do_wˇork(); } fn test() { do_wˇork(); }
fn do_work() { test(); } fn do_work() { test(); }
"}); "});
let target_range = cx.lsp_range(indoc! {" let target_range = cx.lsp_range(indoc! {"
fn test() { do_work(); } fn test() { do_work(); }
fn «do_work»() { test(); } fn «do_work»() { test(); }
"}); "});
let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move { let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
lsp::LocationLink { lsp::LocationLink {
@ -1043,9 +1051,9 @@ mod tests {
// create a pending selection // create a pending selection
let selection_range = cx.ranges(indoc! {" let selection_range = cx.ranges(indoc! {"
fn «test() { do_w»ork(); } fn «test() { do_w»ork(); }
fn do_work() { test(); } fn do_work() { test(); }
"})[0] "})[0]
.clone(); .clone();
cx.update_editor(|editor, cx| { cx.update_editor(|editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx); let snapshot = editor.buffer().read(cx).snapshot(cx);
@ -1064,13 +1072,13 @@ mod tests {
cx, cx,
); );
}); });
cx.foreground().run_until_parked(); cx.background_executor.run_until_parked();
assert!(requests.try_next().is_err()); assert!(requests.try_next().is_err());
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {" cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
fn test() { do_work(); } fn test() { do_work(); }
fn do_work() { test(); } fn do_work() { test(); }
"}); "});
cx.foreground().run_until_parked(); cx.background_executor.run_until_parked();
} }
#[gpui::test] #[gpui::test]
@ -1093,28 +1101,28 @@ mod tests {
) )
.await; .await;
cx.set_state(indoc! {" cx.set_state(indoc! {"
struct TestStruct; struct TestStruct;
fn main() { fn main() {
let variableˇ = TestStruct; let variableˇ = TestStruct;
} }
"}); "});
let hint_start_offset = cx.ranges(indoc! {" let hint_start_offset = cx.ranges(indoc! {"
struct TestStruct; struct TestStruct;
fn main() { fn main() {
let variableˇ = TestStruct; let variableˇ = TestStruct;
} }
"})[0] "})[0]
.start; .start;
let hint_position = cx.to_lsp(hint_start_offset); let hint_position = cx.to_lsp(hint_start_offset);
let target_range = cx.lsp_range(indoc! {" let target_range = cx.lsp_range(indoc! {"
struct «TestStruct»; struct «TestStruct»;
fn main() { fn main() {
let variable = TestStruct; let variable = TestStruct;
} }
"}); "});
let expected_uri = cx.buffer_lsp_url.clone(); let expected_uri = cx.buffer_lsp_url.clone();
let hint_label = ": TestStruct"; let hint_label = ": TestStruct";
@ -1144,7 +1152,7 @@ mod tests {
}) })
.next() .next()
.await; .await;
cx.foreground().run_until_parked(); cx.background_executor.run_until_parked();
cx.update_editor(|editor, cx| { cx.update_editor(|editor, cx| {
let expected_layers = vec![hint_label.to_string()]; let expected_layers = vec![hint_label.to_string()];
assert_eq!(expected_layers, cached_hint_labels(editor)); assert_eq!(expected_layers, cached_hint_labels(editor));
@ -1153,12 +1161,12 @@ mod tests {
let inlay_range = cx let inlay_range = cx
.ranges(indoc! {" .ranges(indoc! {"
struct TestStruct; struct TestStruct;
fn main() { fn main() {
let variable« »= TestStruct; let variable« »= TestStruct;
} }
"}) "})
.get(0) .get(0)
.cloned() .cloned()
.unwrap(); .unwrap();
@ -1190,7 +1198,7 @@ mod tests {
cx, cx,
); );
}); });
cx.foreground().run_until_parked(); cx.background_executor.run_until_parked();
cx.update_editor(|editor, cx| { cx.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx); let snapshot = editor.snapshot(cx);
let actual_highlights = snapshot let actual_highlights = snapshot
@ -1210,10 +1218,11 @@ mod tests {
// Unpress cmd causes highlight to go away // Unpress cmd causes highlight to go away
cx.update_editor(|editor, cx| { cx.update_editor(|editor, cx| {
editor.modifiers_changed( crate::element::EditorElement::modifiers_changed(
&platform::ModifiersChangedEvent { editor,
&ModifiersChangedEvent {
modifiers: Modifiers { modifiers: Modifiers {
cmd: false, command: false,
..Default::default() ..Default::default()
}, },
..Default::default() ..Default::default()
@ -1223,21 +1232,22 @@ mod tests {
}); });
// Assert no link highlights // Assert no link highlights
cx.update_editor(|editor, cx| { cx.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx); let snapshot = editor.snapshot(cx);
let actual_ranges = snapshot let actual_ranges = snapshot
.text_highlight_ranges::<LinkGoToDefinitionState>() .text_highlight_ranges::<LinkGoToDefinitionState>()
.map(|ranges| ranges.as_ref().clone().1) .map(|ranges| ranges.as_ref().clone().1)
.unwrap_or_default(); .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 // Cmd+click without existing definition requests and jumps
cx.update_editor(|editor, cx| { cx.update_editor(|editor, cx| {
editor.modifiers_changed( crate::element::EditorElement::modifiers_changed(
&platform::ModifiersChangedEvent { editor,
&ModifiersChangedEvent {
modifiers: Modifiers { modifiers: Modifiers {
cmd: true, command: true,
..Default::default() ..Default::default()
}, },
..Default::default() ..Default::default()
@ -1253,17 +1263,17 @@ mod tests {
cx, cx,
); );
}); });
cx.foreground().run_until_parked(); cx.background_executor.run_until_parked();
cx.update_editor(|editor, cx| { cx.update_editor(|editor, cx| {
go_to_fetched_type_definition(editor, hint_hover_position, false, 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! {" cx.assert_editor_state(indoc! {"
struct «TestStructˇ»; struct «TestStructˇ»;
fn main() { fn main() {
let variable = TestStruct; let variable = TestStruct;
} }
"}); "});
} }
} }

View File

@ -2,17 +2,22 @@ use crate::{
DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition, DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition,
Rename, RevealInFinder, SelectMode, ToggleCodeActions, Rename, RevealInFinder, SelectMode, ToggleCodeActions,
}; };
use context_menu::ContextMenuItem; use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext};
use gpui::{elements::AnchorCorner, geometry::vector::Vector2F, ViewContext};
pub struct MouseContextMenu {
pub(crate) position: Point<Pixels>,
pub(crate) context_menu: View<ui::ContextMenu>,
_subscription: Subscription,
}
pub fn deploy_context_menu( pub fn deploy_context_menu(
editor: &mut Editor, editor: &mut Editor,
position: Vector2F, position: Point<Pixels>,
point: DisplayPoint, point: DisplayPoint,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) { ) {
if !editor.focused { if !editor.is_focused(cx) {
cx.focus_self(); editor.focus(cx);
} }
// Don't show context menu for inline editors // 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); s.set_pending_display_range(point..point, SelectMode::Character);
}); });
editor.mouse_context_menu.update(cx, |menu, cx| { let context_menu = ui::ContextMenu::build(cx, |menu, _cx| {
menu.show( menu.action("Rename Symbol", Box::new(Rename))
position, .action("Go to Definition", Box::new(GoToDefinition))
AnchorCorner::TopLeft, .action("Go to Type Definition", Box::new(GoToTypeDefinition))
vec![ .action("Find All References", Box::new(FindAllReferences))
ContextMenuItem::action("Rename Symbol", Rename), .action(
ContextMenuItem::action("Go to Definition", GoToDefinition), "Code Actions",
ContextMenuItem::action("Go to Type Definition", GoToTypeDefinition), Box::new(ToggleCodeActions {
ContextMenuItem::action("Find All References", FindAllReferences), deployed_from_indicator: false,
ContextMenuItem::action( }),
"Code Actions", )
ToggleCodeActions { .separator()
deployed_from_indicator: false, .action("Reveal in Finder", Box::new(RevealInFinder))
}, });
), let context_menu_focus = context_menu.focus_handle(cx);
ContextMenuItem::Separator, cx.focus(&context_menu_focus);
ContextMenuItem::action("Reveal in Finder", RevealInFinder),
], let _subscription = cx.subscribe(&context_menu, move |this, _, _event: &DismissEvent, cx| {
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(); cx.notify();
} }
@ -84,6 +97,7 @@ mod tests {
do_wˇork(); 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.update_editor(|editor, cx| deploy_context_menu(editor, Default::default(), point, cx));
cx.assert_editor_state(indoc! {" cx.assert_editor_state(indoc! {"
@ -91,6 +105,6 @@ mod tests {
do_wˇork(); 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()));
} }
} }

View File

@ -1,7 +1,8 @@
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint}; use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint};
use gpui::{FontCache, TextLayoutCache}; use gpui::{px, Pixels, TextSystem};
use language::Point; use language::Point;
use std::{ops::Range, sync::Arc}; use std::{ops::Range, sync::Arc};
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
@ -13,9 +14,9 @@ pub enum FindRange {
/// TextLayoutDetails encompasses everything we need to move vertically /// TextLayoutDetails encompasses everything we need to move vertically
/// taking into account variable width characters. /// taking into account variable width characters.
pub struct TextLayoutDetails { pub struct TextLayoutDetails {
pub font_cache: Arc<FontCache>, pub text_system: Arc<TextSystem>,
pub text_layout_cache: Arc<TextLayoutCache>,
pub editor_style: EditorStyle, pub editor_style: EditorStyle,
pub rem_size: Pixels,
} }
pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
@ -94,10 +95,10 @@ pub fn up_by_rows(
text_layout_details: &TextLayoutDetails, text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) { ) -> (DisplayPoint, SelectionGoal) {
let mut goal_x = match goal { let mut goal_x = match goal {
SelectionGoal::HorizontalPosition(x) => x, 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, SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
SelectionGoal::HorizontalRange { end, .. } => end, SelectionGoal::HorizontalRange { end, .. } => end.into(),
_ => map.x_for_point(start, text_layout_details), _ => map.x_for_display_point(start, text_layout_details),
}; };
let prev_row = start.row().saturating_sub(row_count); let prev_row = start.row().saturating_sub(row_count);
@ -106,19 +107,22 @@ pub fn up_by_rows(
Bias::Left, Bias::Left,
); );
if point.row() < start.row() { 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 { } else if preserve_column_at_start {
return (start, goal); return (start, goal);
} else { } else {
point = DisplayPoint::new(0, 0); point = DisplayPoint::new(0, 0);
goal_x = 0.0; goal_x = px(0.);
} }
let mut clipped_point = map.clip_point(point, Bias::Left); let mut clipped_point = map.clip_point(point, Bias::Left);
if clipped_point.row() < point.row() { if clipped_point.row() < point.row() {
clipped_point = map.clip_point(point, Bias::Right); 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( pub fn down_by_rows(
@ -130,28 +134,31 @@ pub fn down_by_rows(
text_layout_details: &TextLayoutDetails, text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) { ) -> (DisplayPoint, SelectionGoal) {
let mut goal_x = match goal { let mut goal_x = match goal {
SelectionGoal::HorizontalPosition(x) => x, SelectionGoal::HorizontalPosition(x) => x.into(),
SelectionGoal::WrappedHorizontalPosition((_, x)) => x, SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
SelectionGoal::HorizontalRange { end, .. } => end, SelectionGoal::HorizontalRange { end, .. } => end.into(),
_ => map.x_for_point(start, text_layout_details), _ => map.x_for_display_point(start, text_layout_details),
}; };
let new_row = start.row() + row_count; let new_row = start.row() + row_count;
let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right); let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
if point.row() > start.row() { 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 { } else if preserve_column_at_end {
return (start, goal); return (start, goal);
} else { } else {
point = map.max_point(); 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); let mut clipped_point = map.clip_point(point, Bias::Right);
if clipped_point.row() > point.row() { if clipped_point.row() > point.row() {
clipped_point = map.clip_point(point, Bias::Left); 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( pub fn line_beginning(
@ -453,6 +460,7 @@ mod tests {
test::{editor_test_context::EditorTestContext, marked_display_snapshot}, test::{editor_test_context::EditorTestContext, marked_display_snapshot},
Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer, Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer,
}; };
use gpui::{font, Context as _};
use project::Project; use project::Project;
use settings::SettingsStore; use settings::SettingsStore;
use util::post_inc; use util::post_inc;
@ -563,19 +571,12 @@ mod tests {
init_test(cx); init_test(cx);
let input_text = "abcdefghijklmnopqrstuvwxys"; let input_text = "abcdefghijklmnopqrstuvwxys";
let family_id = cx let font = font("Helvetica");
.font_cache() let font_size = px(14.0);
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let buffer = MultiBuffer::build_simple(input_text, cx); let buffer = MultiBuffer::build_simple(input_text, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx); let buffer_snapshot = buffer.read(cx).snapshot(cx);
let display_map = 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 // 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 mut id = 0;
@ -756,22 +757,15 @@ mod tests {
let mut cx = EditorTestContext::new(cx).await; let mut cx = EditorTestContext::new(cx).await;
let editor = cx.editor.clone(); let editor = cx.editor.clone();
let window = cx.window.clone(); let window = cx.window.clone();
cx.update_window(window, |cx| { _ = cx.update_window(window, |_, cx| {
let text_layout_details = 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 let font = font("Helvetica");
.font_cache()
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
.unwrap();
let buffer = let buffer =
cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn")); cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abc\ndefg\nhijkl\nmn"));
let multibuffer = cx.add_model(|cx| { let multibuffer = cx.new_model(|cx| {
let mut multibuffer = MultiBuffer::new(0); let mut multibuffer = MultiBuffer::new(0);
multibuffer.push_excerpts( multibuffer.push_excerpts(
buffer.clone(), buffer.clone(),
@ -790,19 +784,20 @@ mod tests {
multibuffer multibuffer
}); });
let display_map = 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)); let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn"); 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 // Can't move up into the first excerpt's header
assert_eq!( assert_eq!(
up( up(
&snapshot, &snapshot,
DisplayPoint::new(2, 2), DisplayPoint::new(2, 2),
SelectionGoal::HorizontalPosition(col_2_x), SelectionGoal::HorizontalPosition(col_2_x.0),
false, false,
&text_layout_details &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 // Move up and down within first excerpt
assert_eq!( assert_eq!(
up( up(
&snapshot, &snapshot,
DisplayPoint::new(3, 4), DisplayPoint::new(3, 4),
SelectionGoal::HorizontalPosition(col_4_x), SelectionGoal::HorizontalPosition(col_4_x.0),
false, false,
&text_layout_details &text_layout_details
), ),
( (
DisplayPoint::new(2, 3), DisplayPoint::new(2, 3),
SelectionGoal::HorizontalPosition(col_4_x) SelectionGoal::HorizontalPosition(col_4_x.0)
), ),
); );
assert_eq!( assert_eq!(
down( down(
&snapshot, &snapshot,
DisplayPoint::new(2, 3), DisplayPoint::new(2, 3),
SelectionGoal::HorizontalPosition(col_4_x), SelectionGoal::HorizontalPosition(col_4_x.0),
false, false,
&text_layout_details &text_layout_details
), ),
( (
DisplayPoint::new(3, 4), 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 // Move up and down across second excerpt's header
assert_eq!( assert_eq!(
up( up(
&snapshot, &snapshot,
DisplayPoint::new(6, 5), DisplayPoint::new(6, 5),
SelectionGoal::HorizontalPosition(col_5_x), SelectionGoal::HorizontalPosition(col_5_x.0),
false, false,
&text_layout_details &text_layout_details
), ),
( (
DisplayPoint::new(3, 4), DisplayPoint::new(3, 4),
SelectionGoal::HorizontalPosition(col_5_x) SelectionGoal::HorizontalPosition(col_5_x.0)
), ),
); );
assert_eq!( assert_eq!(
down( down(
&snapshot, &snapshot,
DisplayPoint::new(3, 4), DisplayPoint::new(3, 4),
SelectionGoal::HorizontalPosition(col_5_x), SelectionGoal::HorizontalPosition(col_5_x.0),
false, false,
&text_layout_details &text_layout_details
), ),
( (
DisplayPoint::new(6, 5), 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 // Can't move down off the end
assert_eq!( assert_eq!(
@ -898,28 +896,29 @@ mod tests {
), ),
( (
DisplayPoint::new(7, 2), DisplayPoint::new(7, 2),
SelectionGoal::HorizontalPosition(max_point_x) SelectionGoal::HorizontalPosition(max_point_x.0)
), ),
); );
assert_eq!( assert_eq!(
down( down(
&snapshot, &snapshot,
DisplayPoint::new(7, 2), DisplayPoint::new(7, 2),
SelectionGoal::HorizontalPosition(max_point_x), SelectionGoal::HorizontalPosition(max_point_x.0),
false, false,
&text_layout_details &text_layout_details
), ),
( (
DisplayPoint::new(7, 2), DisplayPoint::new(7, 2),
SelectionGoal::HorizontalPosition(max_point_x) SelectionGoal::HorizontalPosition(max_point_x.0)
), ),
); );
}); });
} }
fn init_test(cx: &mut gpui::AppContext) { fn init_test(cx: &mut gpui::AppContext) {
cx.set_global(SettingsStore::test(cx)); let settings_store = SettingsStore::test(cx);
theme::init((), cx); cx.set_global(settings_store);
theme::init(theme::LoadThemes::JustBase, cx);
language::init(cx); language::init(cx);
crate::init(cx); crate::init(cx);
Project::init_settings(cx); Project::init_settings(cx);

View File

@ -1,31 +1,50 @@
use std::sync::Arc; use std::sync::Arc;
use anyhow::Context as _; use anyhow::Context as _;
use gpui::{AppContext, Task, ViewContext}; use gpui::{Context, View, ViewContext, VisualContext, WindowContext};
use language::Language; use language::Language;
use multi_buffer::MultiBuffer; use multi_buffer::MultiBuffer;
use project::lsp_ext_command::ExpandMacro; use project::lsp_ext_command::ExpandMacro;
use text::ToPointUtf16; use text::ToPointUtf16;
use crate::{Editor, ExpandMacroRecursively}; use crate::{element::register_action, Editor, ExpandMacroRecursively};
pub fn apply_related_actions(cx: &mut AppContext) { pub fn apply_related_actions(editor: &View<Editor>, cx: &mut WindowContext) {
cx.add_async_action(expand_macro_recursively); 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( pub fn expand_macro_recursively(
editor: &mut Editor, editor: &mut Editor,
_: &ExpandMacroRecursively, _: &ExpandMacroRecursively,
cx: &mut ViewContext<'_, '_, Editor>, cx: &mut ViewContext<'_, Editor>,
) -> Option<Task<anyhow::Result<()>>> { ) {
if editor.selections.count() == 0 { if editor.selections.count() == 0 {
return None; return;
} }
let project = editor.project.as_ref()?; let Some(project) = &editor.project else {
let workspace = editor.workspace(cx)?; return;
};
let Some(workspace) = editor.workspace() else {
return;
};
let multibuffer = editor.buffer().read(cx); 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 .selections
.disjoint_anchors() .disjoint_anchors()
.into_iter() .into_iter()
@ -56,7 +75,10 @@ pub fn expand_macro_recursively(
None None
} }
}) })
})?; })
else {
return;
};
let project = project.clone(); let project = project.clone();
let buffer_snapshot = buffer.read(cx).snapshot(); let buffer_snapshot = buffer.read(cx).snapshot();
@ -69,7 +91,7 @@ pub fn expand_macro_recursively(
cx, 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")?; let macro_expansion = expand_macro_task.await.context("expand macro")?;
if macro_expansion.is_empty() { if macro_expansion.is_empty() {
log::info!("Empty macro expansion for position {position:?}"); 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| { let buffer = project.update(&mut cx, |project, cx| {
project.create_buffer(&macro_expansion.expansion, Some(rust_language), cx) project.create_buffer(&macro_expansion.expansion, Some(rust_language), cx)
})?; })??;
workspace.update(&mut cx, |workspace, 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) MultiBuffer::singleton(buffer, cx).with_title(macro_expansion.name)
}); });
workspace.add_item( 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, cx,
); );
}); })
})
anyhow::Ok(()) .detach_and_log_err(cx);
}))
} }
fn is_rust_language(language: &Language) -> bool { fn is_rust_language(language: &Language) -> bool {

View File

@ -2,26 +2,21 @@ pub mod actions;
pub mod autoscroll; pub mod autoscroll;
pub mod scroll_amount; 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::{ use crate::{
display_map::{DisplaySnapshot, ToDisplayPoint}, display_map::{DisplaySnapshot, ToDisplayPoint},
hover_popover::hide_hover, hover_popover::hide_hover,
persistence::DB, persistence::DB,
Anchor, DisplayPoint, Editor, EditorMode, Event, InlayHintRefreshReason, MultiBufferSnapshot, Anchor, DisplayPoint, Editor, EditorEvent, EditorMode, InlayHintRefreshReason,
ToPoint, 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::{ use self::{
autoscroll::{Autoscroll, AutoscrollStrategy}, autoscroll::{Autoscroll, AutoscrollStrategy},
@ -37,25 +32,25 @@ pub struct ScrollbarAutoHide(pub bool);
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct ScrollAnchor { pub struct ScrollAnchor {
pub offset: Vector2F, pub offset: gpui::Point<f32>,
pub anchor: Anchor, pub anchor: Anchor,
} }
impl ScrollAnchor { impl ScrollAnchor {
fn new() -> Self { fn new() -> Self {
Self { Self {
offset: Vector2F::zero(), offset: gpui::Point::default(),
anchor: Anchor::min(), 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; let mut scroll_position = self.offset;
if self.anchor != Anchor::min() { if self.anchor != Anchor::min() {
let scroll_top = self.anchor.to_display_point(snapshot).row() as f32; 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 { } else {
scroll_position.set_y(0.); scroll_position.y = 0.;
} }
scroll_position scroll_position
} }
@ -65,6 +60,12 @@ impl ScrollAnchor {
} }
} }
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Axis {
Vertical,
Horizontal,
}
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct OngoingScroll { pub struct OngoingScroll {
last_event: Instant, 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_PERCENT: f32 = 1.9;
const UNLOCK_LOWER_BOUND: f32 = 6.; const UNLOCK_LOWER_BOUND: Pixels = px(6.);
let mut axis = self.axis; let mut axis = self.axis;
let x = delta.x().abs(); let x = delta.x.abs();
let y = delta.y().abs(); let y = delta.y.abs();
let duration = Instant::now().duration_since(self.last_event); let duration = Instant::now().duration_since(self.last_event);
if duration > SCROLL_EVENT_SEPARATION { if duration > SCROLL_EVENT_SEPARATION {
//New ongoing scroll will start, determine axis //New ongoing scroll will start, determine axis
@ -114,8 +115,12 @@ impl OngoingScroll {
} }
match axis { match axis {
Some(Axis::Vertical) => *delta = vec2f(0., delta.y()), Some(Axis::Vertical) => {
Some(Axis::Horizontal) => *delta = vec2f(delta.x(), 0.), *delta = point(px(0.), delta.y);
}
Some(Axis::Horizontal) => {
*delta = point(delta.x, px(0.));
}
None => {} None => {}
} }
@ -128,9 +133,10 @@ pub struct ScrollManager {
anchor: ScrollAnchor, anchor: ScrollAnchor,
ongoing: OngoingScroll, ongoing: OngoingScroll,
autoscroll_request: Option<(Autoscroll, bool)>, autoscroll_request: Option<(Autoscroll, bool)>,
last_autoscroll: Option<(Vector2F, f32, f32, AutoscrollStrategy)>, last_autoscroll: Option<(gpui::Point<f32>, f32, f32, AutoscrollStrategy)>,
show_scrollbars: bool, show_scrollbars: bool,
hide_scrollbar_task: Option<Task<()>>, hide_scrollbar_task: Option<Task<()>>,
dragging_scrollbar: bool,
visible_line_count: Option<f32>, visible_line_count: Option<f32>,
} }
@ -143,6 +149,7 @@ impl ScrollManager {
autoscroll_request: None, autoscroll_request: None,
show_scrollbars: true, show_scrollbars: true,
hide_scrollbar_task: None, hide_scrollbar_task: None,
dragging_scrollbar: false,
last_autoscroll: None, last_autoscroll: None,
visible_line_count: None, visible_line_count: None,
} }
@ -166,30 +173,30 @@ impl ScrollManager {
self.ongoing.axis = axis; 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) self.anchor.scroll_position(snapshot)
} }
fn set_scroll_position( fn set_scroll_position(
&mut self, &mut self,
scroll_position: Vector2F, scroll_position: gpui::Point<f32>,
map: &DisplaySnapshot, map: &DisplaySnapshot,
local: bool, local: bool,
autoscroll: bool, autoscroll: bool,
workspace_id: Option<i64>, workspace_id: Option<i64>,
cx: &mut ViewContext<Editor>, 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 { ScrollAnchor {
anchor: Anchor::min(), anchor: Anchor::min(),
offset: scroll_position.max(vec2f(0., 0.)), offset: scroll_position.max(&gpui::Point::default()),
}, },
0, 0,
) )
} else { } else {
let scroll_top_buffer_point = 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 let top_anchor = map
.buffer_snapshot .buffer_snapshot
.anchor_at(scroll_top_buffer_point, Bias::Right); .anchor_at(scroll_top_buffer_point, Bias::Right);
@ -197,9 +204,9 @@ impl ScrollManager {
( (
ScrollAnchor { ScrollAnchor {
anchor: top_anchor, anchor: top_anchor,
offset: vec2f( offset: point(
scroll_position.x(), scroll_position.x,
scroll_position.y() - top_anchor.to_display_point(&map).row() as f32, scroll_position.y - top_anchor.to_display_point(&map).row() as f32,
), ),
}, },
scroll_top_buffer_point.row, scroll_top_buffer_point.row,
@ -219,20 +226,20 @@ impl ScrollManager {
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) { ) {
self.anchor = anchor; self.anchor = anchor;
cx.emit(Event::ScrollPositionChanged { local, autoscroll }); cx.emit(EditorEvent::ScrollPositionChanged { local, autoscroll });
self.show_scrollbar(cx); self.show_scrollbar(cx);
self.autoscroll_request.take(); self.autoscroll_request.take();
if let Some(workspace_id) = workspace_id { 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 { .spawn(async move {
DB.save_scroll_position( DB.save_scroll_position(
item_id, item_id,
workspace_id, workspace_id,
top_row, top_row,
anchor.offset.x(), anchor.offset.x,
anchor.offset.y(), anchor.offset.y,
) )
.await .await
.log_err() .log_err()
@ -250,7 +257,9 @@ impl ScrollManager {
if cx.default_global::<ScrollbarAutoHide>().0 { if cx.default_global::<ScrollbarAutoHide>().0 {
self.hide_scrollbar_task = Some(cx.spawn(|editor, mut cx| async move { 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 editor
.update(&mut cx, |editor, cx| { .update(&mut cx, |editor, cx| {
editor.scroll_manager.show_scrollbars = false; editor.scroll_manager.show_scrollbars = false;
@ -271,9 +280,20 @@ impl ScrollManager {
self.autoscroll_request.is_some() 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 { pub fn clamp_scroll_left(&mut self, max: f32) -> bool {
if max < self.anchor.offset.x() { if max < self.anchor.offset.x {
self.anchor.offset.set_x(max); self.anchor.offset.x = max;
true true
} else { } else {
false 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); self.set_scroll_position_internal(scroll_position, true, false, cx);
} }
pub(crate) fn set_scroll_position_internal( pub(crate) fn set_scroll_position_internal(
&mut self, &mut self,
scroll_position: Vector2F, scroll_position: gpui::Point<f32>,
local: bool, local: bool,
autoscroll: bool, autoscroll: bool,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
@ -337,7 +361,7 @@ impl Editor {
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); 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)); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
self.scroll_manager.anchor.scroll_position(&display_map) 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>) { pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) {
if matches!(self.mode, EditorMode::SingleLine) { if matches!(self.mode, EditorMode::SingleLine) {
cx.propagate_action(); cx.propagate();
return; return;
} }
@ -379,7 +403,7 @@ impl Editor {
} }
let cur_position = self.scroll_position(cx); 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); self.set_scroll_position(new_pos, cx);
} }
@ -415,7 +439,7 @@ impl Editor {
pub fn read_scroll_position_from_db( pub fn read_scroll_position_from_db(
&mut self, &mut self,
item_id: usize, item_id: u64,
workspace_id: WorkspaceId, workspace_id: WorkspaceId,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) { ) {
@ -427,7 +451,7 @@ impl Editor {
.snapshot(cx) .snapshot(cx)
.anchor_at(Point::new(top_row as u32, 0), Bias::Left); .anchor_at(Point::new(top_row as u32, 0), Bias::Left);
let scroll_anchor = ScrollAnchor { let scroll_anchor = ScrollAnchor {
offset: Vector2F::new(x, y), offset: gpui::Point::new(x, y),
anchor: top_anchor, anchor: top_anchor,
}; };
self.set_scroll_anchor(scroll_anchor, cx); self.set_scroll_anchor(scroll_anchor, cx);

View File

@ -1,72 +1,31 @@
use gpui::{actions, geometry::vector::Vector2F, AppContext, Axis, ViewContext}; use super::Axis;
use language::Bias; use crate::{
Autoscroll, Bias, Editor, EditorMode, NextScreen, ScrollAnchor, ScrollCursorBottom,
use crate::{Editor, EditorMode}; ScrollCursorCenter, ScrollCursorTop,
};
use super::{autoscroll::Autoscroll, scroll_amount::ScrollAmount, ScrollAnchor}; use gpui::{Point, ViewContext};
actions!(
editor,
[
LineDown,
LineUp,
HalfPageDown,
HalfPageUp,
PageDown,
PageUp,
NextScreen,
ScrollCursorTop,
ScrollCursorCenter,
ScrollCursorBottom,
]
);
pub fn init(cx: &mut AppContext) {
cx.add_action(Editor::next_screen);
cx.add_action(Editor::scroll_cursor_top);
cx.add_action(Editor::scroll_cursor_center);
cx.add_action(Editor::scroll_cursor_bottom);
cx.add_action(|this: &mut Editor, _: &LineDown, cx| {
this.scroll_screen(&ScrollAmount::Line(1.), cx)
});
cx.add_action(|this: &mut Editor, _: &LineUp, cx| {
this.scroll_screen(&ScrollAmount::Line(-1.), cx)
});
cx.add_action(|this: &mut Editor, _: &HalfPageDown, cx| {
this.scroll_screen(&ScrollAmount::Page(0.5), cx)
});
cx.add_action(|this: &mut Editor, _: &HalfPageUp, cx| {
this.scroll_screen(&ScrollAmount::Page(-0.5), cx)
});
cx.add_action(|this: &mut Editor, _: &PageDown, cx| {
this.scroll_screen(&ScrollAmount::Page(1.), cx)
});
cx.add_action(|this: &mut Editor, _: &PageUp, cx| {
this.scroll_screen(&ScrollAmount::Page(-1.), cx)
});
}
impl Editor { 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() { if self.take_rename(true, cx).is_some() {
return None; return;
} }
if self.mouse_context_menu.read(cx).visible() { // todo!()
return None; // if self.mouse_context_menu.read(cx).visible() {
} // return None;
// }
if matches!(self.mode, EditorMode::SingleLine) { if matches!(self.mode, EditorMode::SingleLine) {
cx.propagate_action(); cx.propagate();
return None; return;
} }
self.request_autoscroll(Autoscroll::Next, cx); self.request_autoscroll(Autoscroll::Next, cx);
Some(())
} }
pub fn scroll( pub fn scroll(
&mut self, &mut self,
scroll_position: Vector2F, scroll_position: Point<f32>,
axis: Option<Axis>, axis: Option<Axis>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
@ -74,17 +33,17 @@ impl Editor {
self.set_scroll_position(scroll_position, cx); self.set_scroll_position(scroll_position, cx);
} }
fn scroll_cursor_top(editor: &mut Editor, _: &ScrollCursorTop, cx: &mut ViewContext<Editor>) { pub fn scroll_cursor_top(&mut self, _: &ScrollCursorTop, cx: &mut ViewContext<Editor>) {
let snapshot = editor.snapshot(cx).display_snapshot; let snapshot = self.snapshot(cx).display_snapshot;
let scroll_margin_rows = editor.vertical_scroll_margin() as u32; 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.row_mut() = new_screen_top.row().saturating_sub(scroll_margin_rows);
*new_screen_top.column_mut() = 0; *new_screen_top.column_mut() = 0;
let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left); let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left);
let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top); let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top);
editor.set_scroll_anchor( self.set_scroll_anchor(
ScrollAnchor { ScrollAnchor {
anchor: new_anchor, anchor: new_anchor,
offset: Default::default(), offset: Default::default(),
@ -93,25 +52,21 @@ impl Editor {
) )
} }
fn scroll_cursor_center( pub fn scroll_cursor_center(&mut self, _: &ScrollCursorCenter, cx: &mut ViewContext<Editor>) {
editor: &mut Editor, let snapshot = self.snapshot(cx).display_snapshot;
_: &ScrollCursorCenter, let visible_rows = if let Some(visible_rows) = self.visible_line_count() {
cx: &mut ViewContext<Editor>,
) {
let snapshot = editor.snapshot(cx).display_snapshot;
let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
visible_rows as u32 visible_rows as u32
} else { } else {
return; 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.row_mut() = new_screen_top.row().saturating_sub(visible_rows / 2);
*new_screen_top.column_mut() = 0; *new_screen_top.column_mut() = 0;
let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left); let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left);
let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top); let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top);
editor.set_scroll_anchor( self.set_scroll_anchor(
ScrollAnchor { ScrollAnchor {
anchor: new_anchor, anchor: new_anchor,
offset: Default::default(), offset: Default::default(),
@ -120,20 +75,16 @@ impl Editor {
) )
} }
fn scroll_cursor_bottom( pub fn scroll_cursor_bottom(&mut self, _: &ScrollCursorBottom, cx: &mut ViewContext<Editor>) {
editor: &mut Editor, let snapshot = self.snapshot(cx).display_snapshot;
_: &ScrollCursorBottom, let scroll_margin_rows = self.vertical_scroll_margin() as u32;
cx: &mut ViewContext<Editor>, let visible_rows = if let Some(visible_rows) = self.visible_line_count() {
) {
let snapshot = editor.snapshot(cx).display_snapshot;
let scroll_margin_rows = editor.vertical_scroll_margin() as u32;
let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
visible_rows as u32 visible_rows as u32
} else { } else {
return; 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 *new_screen_top.row_mut() = new_screen_top
.row() .row()
.saturating_sub(visible_rows.saturating_sub(scroll_margin_rows)); .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_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left);
let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top); let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top);
editor.set_scroll_anchor( self.set_scroll_anchor(
ScrollAnchor { ScrollAnchor {
anchor: new_anchor, anchor: new_anchor,
offset: Default::default(), offset: Default::default(),

View File

@ -1,6 +1,6 @@
use std::cmp; use std::{cmp, f32};
use gpui::ViewContext; use gpui::{px, Pixels, ViewContext};
use language::Point; use language::Point;
use crate::{display_map::ToDisplayPoint, Editor, EditorMode, LineWithInvisibles}; use crate::{display_map::ToDisplayPoint, Editor, EditorMode, LineWithInvisibles};
@ -48,11 +48,11 @@ impl AutoscrollStrategy {
impl Editor { impl Editor {
pub fn autoscroll_vertically( pub fn autoscroll_vertically(
&mut self, &mut self,
viewport_height: f32, viewport_height: Pixels,
line_height: f32, line_height: Pixels,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) -> bool { ) -> 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 display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let mut scroll_position = self.scroll_manager.scroll_position(&display_map); let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) { let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
@ -60,8 +60,8 @@ impl Editor {
} else { } else {
display_map.max_point().row() as f32 display_map.max_point().row() as f32
}; };
if scroll_position.y() > max_scroll_top { if scroll_position.y > max_scroll_top {
scroll_position.set_y(max_scroll_top); scroll_position.y = max_scroll_top;
self.set_scroll_position(scroll_position, cx); self.set_scroll_position(scroll_position, cx);
} }
@ -136,31 +136,31 @@ impl Editor {
let margin = margin.min(self.scroll_manager.vertical_scroll_margin); let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
let target_top = (target_top - margin).max(0.0); let target_top = (target_top - margin).max(0.0);
let target_bottom = target_bottom + margin; 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 end_row = start_row + visible_lines;
let needs_scroll_up = target_top < start_row; let needs_scroll_up = target_top < start_row;
let needs_scroll_down = target_bottom >= end_row; let needs_scroll_down = target_bottom >= end_row;
if needs_scroll_up && !needs_scroll_down { 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); self.set_scroll_position_internal(scroll_position, local, true, cx);
} }
if !needs_scroll_up && needs_scroll_down { 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); self.set_scroll_position_internal(scroll_position, local, true, cx);
} }
} }
AutoscrollStrategy::Center => { 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); self.set_scroll_position_internal(scroll_position, local, true, cx);
} }
AutoscrollStrategy::Top => { 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); self.set_scroll_position_internal(scroll_position, local, true, cx);
} }
AutoscrollStrategy::Bottom => { 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); self.set_scroll_position_internal(scroll_position, local, true, cx);
} }
} }
@ -178,9 +178,9 @@ impl Editor {
pub fn autoscroll_horizontally( pub fn autoscroll_horizontally(
&mut self, &mut self,
start_row: u32, start_row: u32,
viewport_width: f32, viewport_width: Pixels,
scroll_width: f32, scroll_width: Pixels,
max_glyph_width: f32, max_glyph_width: Pixels,
layouts: &[LineWithInvisibles], layouts: &[LineWithInvisibles],
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> bool { ) -> bool {
@ -191,11 +191,11 @@ impl Editor {
let mut target_right; let mut target_right;
if self.highlighted_rows.is_some() { if self.highlighted_rows.is_some() {
target_left = 0.0_f32; target_left = px(0.);
target_right = 0.0_f32; target_right = px(0.);
} else { } else {
target_left = std::f32::INFINITY; target_left = px(f32::INFINITY);
target_right = 0.0_f32; target_right = px(0.);
for selection in selections { for selection in selections {
let head = selection.head().to_display_point(&display_map); let head = selection.head().to_display_point(&display_map);
if head.row() >= start_row && head.row() < start_row + layouts.len() as u32 { if head.row() >= start_row && head.row() < start_row + layouts.len() as u32 {
@ -222,20 +222,15 @@ impl Editor {
return false; 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; let scroll_right = scroll_left + viewport_width;
if target_left < scroll_left { if target_left < scroll_left {
self.scroll_manager self.scroll_manager.anchor.offset.x = (target_left / max_glyph_width).into();
.anchor
.offset
.set_x(target_left / max_glyph_width);
true true
} else if target_right > scroll_right { } else if target_right > scroll_right {
self.scroll_manager self.scroll_manager.anchor.offset.x =
.anchor ((target_right - viewport_width) / max_glyph_width).into();
.offset
.set_x((target_right - viewport_width) / max_glyph_width);
true true
} else { } else {
false false

View File

@ -6,7 +6,7 @@ use std::{
}; };
use collections::HashMap; use collections::HashMap;
use gpui::{AppContext, ModelHandle}; use gpui::{AppContext, Model, Pixels};
use itertools::Itertools; use itertools::Itertools;
use language::{Bias, Point, Selection, SelectionGoal, TextDimension, ToPoint}; use language::{Bias, Point, Selection, SelectionGoal, TextDimension, ToPoint};
use util::post_inc; use util::post_inc;
@ -25,8 +25,8 @@ pub struct PendingSelection {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SelectionsCollection { pub struct SelectionsCollection {
display_map: ModelHandle<DisplayMap>, display_map: Model<DisplayMap>,
buffer: ModelHandle<MultiBuffer>, buffer: Model<MultiBuffer>,
pub next_selection_id: usize, pub next_selection_id: usize,
pub line_mode: bool, pub line_mode: bool,
disjoint: Arc<[Selection<Anchor>]>, disjoint: Arc<[Selection<Anchor>]>,
@ -34,7 +34,7 @@ pub struct SelectionsCollection {
} }
impl 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 { Self {
display_map, display_map,
buffer, buffer,
@ -306,19 +306,19 @@ impl SelectionsCollection {
&mut self, &mut self,
display_map: &DisplaySnapshot, display_map: &DisplaySnapshot,
row: u32, row: u32,
positions: &Range<f32>, positions: &Range<Pixels>,
reversed: bool, reversed: bool,
text_layout_details: &TextLayoutDetails, text_layout_details: &TextLayoutDetails,
) -> Option<Selection<Point>> { ) -> Option<Selection<Point>> {
let is_empty = positions.start == positions.end; let is_empty = positions.start == positions.end;
let line_len = display_map.line_len(row); 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; let start_col = line.closest_index_for_x(positions.start) as u32;
if start_col < line_len || (is_empty && positions.start == layed_out_line.width()) { if start_col < line_len || (is_empty && positions.start == line.width) {
let start = DisplayPoint::new(row, start_col); 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); let end = DisplayPoint::new(row, end_col);
Some(Selection { Some(Selection {
@ -327,8 +327,8 @@ impl SelectionsCollection {
end: end.to_point(display_map), end: end.to_point(display_map),
reversed, reversed,
goal: SelectionGoal::HorizontalRange { goal: SelectionGoal::HorizontalRange {
start: positions.start, start: positions.start.into(),
end: positions.end, end: positions.end.into(),
}, },
}) })
} else { } else {
@ -592,7 +592,10 @@ impl<'a> MutableSelectionsCollection<'a> {
self.select(selections) 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 buffer = self.buffer.read(self.cx).snapshot(self.cx);
let selections = ranges let selections = ranges
.into_iter() .into_iter()
@ -614,7 +617,6 @@ impl<'a> MutableSelectionsCollection<'a> {
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
self.select_anchors(selections) self.select_anchors(selections)
} }

View File

@ -6,7 +6,7 @@ use crate::{
DisplayPoint, Editor, EditorMode, MultiBuffer, DisplayPoint, Editor, EditorMode, MultiBuffer,
}; };
use gpui::{ModelHandle, ViewContext}; use gpui::{Context, Model, Pixels, ViewContext};
use project::Project; use project::Project;
use util::test::{marked_text_offsets, marked_text_ranges}; use util::test::{marked_text_offsets, marked_text_ranges};
@ -26,19 +26,11 @@ pub fn marked_display_snapshot(
) -> (DisplaySnapshot, Vec<DisplayPoint>) { ) -> (DisplaySnapshot, Vec<DisplayPoint>) {
let (unmarked_text, markers) = marked_text_offsets(text); let (unmarked_text, markers) = marked_text_offsets(text);
let family_id = cx let font = cx.text_style().font();
.font_cache() let font_size: Pixels = 14usize.into();
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let buffer = MultiBuffer::build_simple(&unmarked_text, cx); let buffer = MultiBuffer::build_simple(&unmarked_text, cx);
let display_map = let display_map = cx.new_model(|cx| DisplayMap::new(buffer, font, font_size, None, 1, 1, cx));
cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
let markers = markers let markers = markers
.into_iter() .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 // RA thinks this is dead code even though it is used in a whole lot of tests
#[allow(dead_code)] #[allow(dead_code)]
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub(crate) fn build_editor( pub(crate) fn build_editor(buffer: Model<MultiBuffer>, cx: &mut ViewContext<Editor>) -> Editor {
buffer: ModelHandle<MultiBuffer>, // todo!()
cx: &mut ViewContext<Editor>, Editor::new(EditorMode::Full, buffer, None, /*None,*/ cx)
) -> Editor {
Editor::new(EditorMode::Full, buffer, None, None, cx)
} }
pub(crate) fn build_editor_with_project( pub(crate) fn build_editor_with_project(
project: ModelHandle<Project>, project: Model<Project>,
buffer: ModelHandle<MultiBuffer>, buffer: Model<MultiBuffer>,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) -> Editor { ) -> Editor {
Editor::new(EditorMode::Full, buffer, Some(project), None, cx) // todo!()
Editor::new(EditorMode::Full, buffer, Some(project), /*None,*/ cx)
} }

View File

@ -5,11 +5,12 @@ use std::{
}; };
use anyhow::Result; use anyhow::Result;
use serde_json::json;
use crate::{Editor, ToPoint}; use crate::{Editor, ToPoint};
use collections::HashSet; use collections::HashSet;
use futures::Future; use futures::Future;
use gpui::{json, ViewContext, ViewHandle}; use gpui::{View, ViewContext, VisualTestContext};
use indoc::indoc; use indoc::indoc;
use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries}; use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries};
use lsp::{notification, request}; use lsp::{notification, request};
@ -18,12 +19,12 @@ use project::Project;
use smol::stream::StreamExt; use smol::stream::StreamExt;
use workspace::{AppState, Workspace, WorkspaceHandle}; use workspace::{AppState, Workspace, WorkspaceHandle};
use super::editor_test_context::EditorTestContext; use super::editor_test_context::{AssertionContextManager, EditorTestContext};
pub struct EditorLspTestContext<'a> { pub struct EditorLspTestContext<'a> {
pub cx: EditorTestContext<'a>, pub cx: EditorTestContext<'a>,
pub lsp: lsp::FakeLanguageServer, pub lsp: lsp::FakeLanguageServer,
pub workspace: ViewHandle<Workspace>, pub workspace: View<Workspace>,
pub buffer_lsp_url: lsp::Url, pub buffer_lsp_url: lsp::Url,
} }
@ -33,8 +34,6 @@ impl<'a> EditorLspTestContext<'a> {
capabilities: lsp::ServerCapabilities, capabilities: lsp::ServerCapabilities,
cx: &'a mut gpui::TestAppContext, cx: &'a mut gpui::TestAppContext,
) -> EditorLspTestContext<'a> { ) -> EditorLspTestContext<'a> {
use json::json;
let app_state = cx.update(AppState::test); let app_state = cx.update(AppState::test);
cx.update(|cx| { cx.update(|cx| {
@ -60,6 +59,7 @@ impl<'a> EditorLspTestContext<'a> {
.await; .await;
let project = Project::test(app_state.fs.clone(), [], cx).await; let project = Project::test(app_state.fs.clone(), [], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language))); project.update(cx, |project, _| project.languages().add(Arc::new(language)));
app_state app_state
@ -69,37 +69,38 @@ impl<'a> EditorLspTestContext<'a> {
.await; .await;
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); 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 project
.update(cx, |project, cx| { .update(&mut cx, |project, cx| {
project.find_or_create_local_worktree("/root", true, cx) project.find_or_create_local_worktree("/root", true, cx)
}) })
.await .await
.unwrap(); .unwrap();
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
.await; .await;
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
let item = workspace let item = workspace
.update(cx, |workspace, cx| { .update(&mut cx, |workspace, cx| {
workspace.open_path(file, None, true, cx) workspace.open_path(file, None, true, cx)
}) })
.await .await
.expect("Could not open test file"); .expect("Could not open test file");
let editor = cx.update(|cx| { let editor = cx.update(|cx| {
item.act_as::<Editor>(cx) item.act_as::<Editor>(cx)
.expect("Opened test file wasn't an editor") .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(); let lsp = fake_servers.next().await.unwrap();
Self { Self {
cx: EditorTestContext { cx: EditorTestContext {
cx, cx,
window: window.into(), window: window.into(),
editor, editor,
assertion_cx: AssertionContextManager::new(),
}, },
lsp, lsp,
workspace, workspace,
@ -257,7 +258,7 @@ impl<'a> EditorLspTestContext<'a> {
where where
F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T, 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>( pub fn handle_request<T, F, Fut>(

View File

@ -1,17 +1,23 @@
use crate::{ use crate::{
display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer, display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
}; };
use collections::BTreeMap;
use futures::Future; use futures::Future;
use gpui::{ use gpui::{
executor::Foreground, keymap_matcher::Keystroke, AnyWindowHandle, AppContext, ContextHandle, AnyWindowHandle, AppContext, Keystroke, ModelContext, View, ViewContext, VisualTestContext,
ModelContext, ViewContext, ViewHandle,
}; };
use indoc::indoc; use indoc::indoc;
use itertools::Itertools;
use language::{Buffer, BufferSnapshot}; use language::{Buffer, BufferSnapshot};
use parking_lot::RwLock;
use project::{FakeFs, Project}; use project::{FakeFs, Project};
use std::{ use std::{
any::TypeId, any::TypeId,
ops::{Deref, DerefMut, Range}, ops::{Deref, DerefMut, Range},
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
}; };
use util::{ use util::{
assert_set_eq, assert_set_eq,
@ -21,14 +27,15 @@ use util::{
use super::build_editor_with_project; use super::build_editor_with_project;
pub struct EditorTestContext<'a> { pub struct EditorTestContext<'a> {
pub cx: &'a mut gpui::TestAppContext, pub cx: gpui::VisualTestContext<'a>,
pub window: AnyWindowHandle, pub window: AnyWindowHandle,
pub editor: ViewHandle<Editor>, pub editor: View<Editor>,
pub assertion_cx: AssertionContextManager,
} }
impl<'a> EditorTestContext<'a> { impl<'a> EditorTestContext<'a> {
pub async fn new(cx: &'a mut gpui::TestAppContext) -> 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_file("/file", "".to_owned()).await;
fs.insert_tree( fs.insert_tree(
"/root", "/root",
@ -44,15 +51,18 @@ impl<'a> EditorTestContext<'a> {
}) })
.await .await
.unwrap(); .unwrap();
let window = cx.add_window(|cx| { let editor = cx.add_window(|cx| {
cx.focus_self(); let editor =
build_editor_with_project(project, MultiBuffer::build_from_buffer(buffer, cx), cx) 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 { Self {
cx, cx: VisualTestContext::from_window(*editor.deref(), cx),
window: window.into(), window: editor.into(),
editor, editor: editor_view,
assertion_cx: AssertionContextManager::new(),
} }
} }
@ -60,24 +70,28 @@ impl<'a> EditorTestContext<'a> {
&self, &self,
predicate: impl FnMut(&Editor, &AppContext) -> bool, predicate: impl FnMut(&Editor, &AppContext) -> bool,
) -> impl Future<Output = ()> { ) -> 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 where
F: FnOnce(&Editor, &ViewContext<Editor>) -> T, 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 pub fn update_editor<F, T>(&mut self, update: F) -> T
where where
F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T, 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 where
F: FnOnce(&MultiBuffer, &AppContext) -> T, F: FnOnce(&MultiBuffer, &AppContext) -> T,
{ {
@ -91,11 +105,11 @@ impl<'a> EditorTestContext<'a> {
self.update_editor(|editor, cx| editor.buffer().update(cx, update)) 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()) 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 where
F: FnOnce(&Buffer, &AppContext) -> T, 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()) 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 { pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
let keystroke_under_test_handle = let keystroke_under_test_handle =
self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text)); self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text));
@ -142,16 +164,16 @@ impl<'a> EditorTestContext<'a> {
// before returning. // before returning.
// NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too // NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too
// quickly races with async actions. // quickly races with async actions.
if let Foreground::Deterministic { cx_id: _, executor } = self.cx.foreground().as_ref() { self.cx.background_executor.run_until_parked();
executor.run_until_parked();
} else {
unreachable!();
}
keystrokes_under_test_handle 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); let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
assert_eq!(self.buffer_text(), unmarked_text); assert_eq!(self.buffer_text(), unmarked_text);
ranges ranges
@ -161,12 +183,12 @@ impl<'a> EditorTestContext<'a> {
let ranges = self.ranges(marked_text); let ranges = self.ranges(marked_text);
let snapshot = self let snapshot = self
.editor .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) ranges[0].start.to_display_point(&snapshot)
} }
// Returns anchors for the current buffer using `«` and `»` // 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 ranges = self.ranges(marked_text);
let snapshot = self.buffer_snapshot(); let snapshot = self.buffer_snapshot();
snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end) 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() marked_text.escape_debug().to_string()
)); ));
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true); 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.set_text(unmarked_text, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(selection_ranges) s.select_ranges(selection_ranges)
@ -207,7 +229,7 @@ impl<'a> EditorTestContext<'a> {
marked_text.escape_debug().to_string() marked_text.escape_debug().to_string()
)); ));
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true); 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); assert_eq!(editor.text(cx), unmarked_text);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(selection_ranges) s.select_ranges(selection_ranges)
@ -274,9 +296,12 @@ impl<'a> EditorTestContext<'a> {
self.assert_selections(expected_selections, expected_marked_text) 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 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() .into_iter()
.map(|s| { .map(|s| {
if s.reversed { if s.reversed {
@ -301,14 +326,14 @@ impl<'a> EditorTestContext<'a> {
panic!( panic!(
indoc! {" indoc! {"
{}Editor has unexpected selections. {}Editor has unexpected selections.
Expected selections: Expected selections:
{} {}
Actual selections: Actual selections:
{} {}
"}, "},
self.assertion_context(), self.assertion_context(),
expected_marked_text, expected_marked_text,
actual_marked_text, actual_marked_text,
@ -321,7 +346,7 @@ impl<'a> Deref for EditorTestContext<'a> {
type Target = gpui::TestAppContext; type Target = gpui::TestAppContext;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
self.cx &self.cx
} }
} }
@ -330,3 +355,50 @@ impl<'a> DerefMut for EditorTestContext<'a> {
&mut self.cx &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);
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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(),
);
}
}

View File

@ -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

View File

@ -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()));
}
}

View File

@ -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);
}
}

View File

@ -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
}
}
}

View File

@ -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(&macro_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"
}

View File

@ -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);
}
}
}

View File

@ -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,
)
}
}

View File

@ -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();
}
}

View File

@ -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.),
}
}
}

View File

@ -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))
}

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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);
}
}

View File

@ -13,7 +13,7 @@ test-support = []
[dependencies] [dependencies]
client = { package = "client2", path = "../client2" } client = { package = "client2", path = "../client2" }
db = { package = "db2", path = "../db2" } db = { package = "db2", path = "../db2" }
editor = { package = "editor2", path = "../editor2" } editor = { path = "../editor" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" } language = { package = "language2", path = "../language2" }
menu = { package = "menu2", path = "../menu2" } menu = { package = "menu2", path = "../menu2" }
@ -44,4 +44,4 @@ tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown",
urlencoding = "2.1.2" urlencoding = "2.1.2"
[dev-dependencies] [dev-dependencies]
editor = { package = "editor2", path = "../editor2", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] }

View File

@ -9,7 +9,7 @@ path = "src/file_finder.rs"
doctest = false doctest = false
[dependencies] [dependencies]
editor = { package = "editor2", path = "../editor2" } editor = { path = "../editor" }
collections = { path = "../collections" } collections = { path = "../collections" }
fuzzy = { package = "fuzzy2", path = "../fuzzy2" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
@ -26,7 +26,7 @@ postage.workspace = true
serde.workspace = true serde.workspace = true
[dev-dependencies] [dev-dependencies]
editor = { package = "editor2", path = "../editor2", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
language = { package = "language2", path = "../language2", features = ["test-support"] } language = { package = "language2", path = "../language2", features = ["test-support"] }
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }

View File

@ -9,7 +9,7 @@ path = "src/go_to_line.rs"
doctest = false doctest = false
[dependencies] [dependencies]
editor = { package = "editor2", path = "../editor2" } editor = { path = "../editor" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
menu = { package = "menu2", path = "../menu2" } menu = { package = "menu2", path = "../menu2" }
serde.workspace = true serde.workspace = true
@ -22,4 +22,4 @@ ui = { package = "ui2", path = "../ui2" }
util = { path = "../util" } util = { path = "../util" }
[dev-dependencies] [dev-dependencies]
editor = { package = "editor2", path = "../editor2", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] }

View File

@ -9,7 +9,7 @@ path = "src/journal2.rs"
doctest = false doctest = false
[dependencies] [dependencies]
editor = { package = "editor2", path = "../editor2" } editor = { path = "../editor" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
util = { path = "../util" } util = { path = "../util" }
workspace2 = { path = "../workspace2" } workspace2 = { path = "../workspace2" }
@ -24,4 +24,4 @@ log.workspace = true
shellexpand = "2.1.0" shellexpand = "2.1.0"
[dev-dependencies] [dev-dependencies]
editor = { package="editor2", path = "../editor2", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] }

View File

@ -9,7 +9,7 @@ path = "src/language_selector.rs"
doctest = false doctest = false
[dependencies] [dependencies]
editor = { package = "editor2", path = "../editor2" } editor = { path = "../editor" }
fuzzy = { package = "fuzzy2", path = "../fuzzy2" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
language = { package = "language2", path = "../language2" } language = { package = "language2", path = "../language2" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
@ -23,4 +23,4 @@ workspace = { package = "workspace2", path = "../workspace2" }
anyhow.workspace = true anyhow.workspace = true
[dev-dependencies] [dev-dependencies]
editor = { package = "editor2", path = "../editor2", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] }

View File

@ -10,7 +10,7 @@ doctest = false
[dependencies] [dependencies]
collections = { path = "../collections" } collections = { path = "../collections" }
editor = { package = "editor2", path = "../editor2" } editor = { path = "../editor" }
settings = { package = "settings2", path = "../settings2" } settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" } theme = { package = "theme2", path = "../theme2" }
language = { package = "language2", path = "../language2" } language = { package = "language2", path = "../language2" }
@ -27,7 +27,7 @@ tree-sitter.workspace = true
[dev-dependencies] [dev-dependencies]
client = { package = "client2", path = "../client2", features = ["test-support"] } 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"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] } util = { path = "../util", features = ["test-support"] }
env_logger.workspace = true env_logger.workspace = true

View File

@ -9,7 +9,7 @@ path = "src/outline.rs"
doctest = false doctest = false
[dependencies] [dependencies]
editor = { package = "editor2", path = "../editor2" } editor = { path = "../editor" }
fuzzy = { package = "fuzzy2", path = "../fuzzy2" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
ui = { package = "ui2", path = "../ui2" } ui = { package = "ui2", path = "../ui2" }
@ -26,4 +26,4 @@ postage.workspace = true
smol.workspace = true smol.workspace = true
[dev-dependencies] [dev-dependencies]
editor = { package = "editor2", path = "../editor2", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] }

View File

@ -9,7 +9,7 @@ path = "src/picker.rs"
doctest = false doctest = false
[dependencies] [dependencies]
editor = { package = "editor2", path = "../editor2" } editor = { path = "../editor" }
ui = { package = "ui2", path = "../ui2" } ui = { package = "ui2", path = "../ui2" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
menu = { package = "menu2", path = "../menu2" } menu = { package = "menu2", path = "../menu2" }
@ -21,7 +21,7 @@ workspace = { package = "workspace2", path = "../workspace2"}
parking_lot.workspace = true parking_lot.workspace = true
[dev-dependencies] [dev-dependencies]
editor = { package = "editor2", path = "../editor2", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
serde_json.workspace = true serde_json.workspace = true
ctor.workspace = true ctor.workspace = true

View File

@ -11,7 +11,7 @@ doctest = false
[dependencies] [dependencies]
collections = { path = "../collections" } collections = { path = "../collections" }
db = { path = "../db2", package = "db2" } db = { path = "../db2", package = "db2" }
editor = { path = "../editor2", package = "editor2" } editor = { path = "../editor" }
gpui = { path = "../gpui2", package = "gpui2" } gpui = { path = "../gpui2", package = "gpui2" }
menu = { path = "../menu2", package = "menu2" } menu = { path = "../menu2", package = "menu2" }
project = { path = "../project2", package = "project2" } project = { path = "../project2", package = "project2" }
@ -35,7 +35,7 @@ unicase = "2.6"
[dev-dependencies] [dev-dependencies]
client = { path = "../client2", package = "client2", features = ["test-support"] } client = { path = "../client2", package = "client2", features = ["test-support"] }
language = { path = "../language2", package = "language2", 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"] } gpui = { path = "../gpui2", package = "gpui2", features = ["test-support"] }
workspace = { path = "../workspace2", package = "workspace2", features = ["test-support"] } workspace = { path = "../workspace2", package = "workspace2", features = ["test-support"] }
serde_json.workspace = true serde_json.workspace = true

View File

@ -9,7 +9,7 @@ path = "src/project_symbols.rs"
doctest = false doctest = false
[dependencies] [dependencies]
editor = { package = "editor2", path = "../editor2" } editor = { path = "../editor" }
fuzzy = {package = "fuzzy2", path = "../fuzzy2" } fuzzy = {package = "fuzzy2", path = "../fuzzy2" }
gpui = {package = "gpui2", path = "../gpui2" } gpui = {package = "gpui2", path = "../gpui2" }
picker = {path = "../picker" } picker = {path = "../picker" }
@ -27,7 +27,7 @@ smol.workspace = true
[dev-dependencies] [dev-dependencies]
futures.workspace = true 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"] } settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
language = { package = "language2", path = "../language2", features = ["test-support"] } language = { package = "language2", path = "../language2", features = ["test-support"] }

View File

@ -10,13 +10,13 @@ doctest = false
[dependencies] [dependencies]
assistant = { package = "assistant2", path = "../assistant2" } assistant = { package = "assistant2", path = "../assistant2" }
editor = { package = "editor2", path = "../editor2" } editor = { path = "../editor" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
search = { path = "../search" } search = { path = "../search" }
workspace = { package = "workspace2", path = "../workspace2" } workspace = { package = "workspace2", path = "../workspace2" }
ui = { package = "ui2", path = "../ui2" } ui = { package = "ui2", path = "../ui2" }
[dev-dependencies] [dev-dependencies]
editor = { package = "editor2", path = "../editor2", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }

View File

@ -9,7 +9,7 @@ path = "src/recent_projects.rs"
doctest = false doctest = false
[dependencies] [dependencies]
editor = { package = "editor2", path = "../editor2" } editor = { path = "../editor" }
fuzzy = { package = "fuzzy2", path = "../fuzzy2" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" } language = { package = "language2", path = "../language2" }
@ -27,4 +27,4 @@ postage.workspace = true
smol.workspace = true smol.workspace = true
[dev-dependencies] [dev-dependencies]
editor = { package = "editor2", path = "../editor2", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] }

View File

@ -11,7 +11,7 @@ doctest = false
[dependencies] [dependencies]
bitflags = "1" bitflags = "1"
collections = { path = "../collections" } collections = { path = "../collections" }
editor = { package = "editor2", path = "../editor2" } editor = { path = "../editor" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" } language = { package = "language2", path = "../language2" }
menu = { package = "menu2", path = "../menu2" } menu = { package = "menu2", path = "../menu2" }
@ -33,7 +33,7 @@ smol.workspace = true
serde_json.workspace = true serde_json.workspace = true
[dev-dependencies] [dev-dependencies]
client = { package = "client2", path = "../client2", features = ["test-support"] } 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"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }

View File

@ -15,7 +15,7 @@ backtrace-on-stack-overflow = "0.3.0"
chrono = "0.4" chrono = "0.4"
clap = { version = "4.4", features = ["derive", "string"] } clap = { version = "4.4", features = ["derive", "string"] }
dialoguer = { version = "0.11.0", features = ["fuzzy-select"] } dialoguer = { version = "0.11.0", features = ["fuzzy-select"] }
editor = { package = "editor2", path = "../editor2" } editor = { path = "../editor" }
fuzzy = { package = "fuzzy2", path = "../fuzzy2" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
indoc.workspace = true indoc.workspace = true

View File

@ -9,7 +9,7 @@ path = "src/terminal_view.rs"
doctest = false doctest = false
[dependencies] [dependencies]
editor = { package = "editor2", path = "../editor2" } editor = { path = "../editor" }
language = { package = "language2", path = "../language2" } language = { package = "language2", path = "../language2" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
project = { package = "project2", path = "../project2" } project = { package = "project2", path = "../project2" }
@ -38,7 +38,7 @@ serde.workspace = true
serde_derive.workspace = true serde_derive.workspace = true
[dev-dependencies] [dev-dependencies]
editor = { package = "editor2", path = "../editor2", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
client = { package = "client2", path = "../client2", features = ["test-support"]} client = { package = "client2", path = "../client2", features = ["test-support"]}
project = { package = "project2", path = "../project2", features = ["test-support"]} project = { package = "project2", path = "../project2", features = ["test-support"]}

View File

@ -10,7 +10,7 @@ doctest = false
[dependencies] [dependencies]
client = { package = "client2", path = "../client2" } client = { package = "client2", path = "../client2" }
editor = { package = "editor2", path = "../editor2" } editor = { path = "../editor" }
feature_flags = { package = "feature_flags2", path = "../feature_flags2" } feature_flags = { package = "feature_flags2", path = "../feature_flags2" }
fs = { package = "fs2", path = "../fs2" } fs = { package = "fs2", path = "../fs2" }
fuzzy = { package = "fuzzy2", path = "../fuzzy2" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
@ -27,4 +27,4 @@ postage.workspace = true
smol.workspace = true smol.workspace = true
[dev-dependencies] [dev-dependencies]
editor = { package = "editor2", path = "../editor2", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] }

View File

@ -26,7 +26,7 @@ serde_json.workspace = true
collections = { path = "../collections" } collections = { path = "../collections" }
command_palette = { path = "../command_palette" } command_palette = { path = "../command_palette" }
editor = { package = "editor2", path = "../editor2" } editor = { path = "../editor" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" } language = { package = "language2", path = "../language2" }
search = { path = "../search" } search = { path = "../search" }
@ -42,7 +42,7 @@ indoc.workspace = true
parking_lot.workspace = true parking_lot.workspace = true
futures.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"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
language = { package = "language2", path = "../language2", features = ["test-support"] } language = { package = "language2", path = "../language2", features = ["test-support"] }
project = { package = "project2", path = "../project2", features = ["test-support"] } project = { package = "project2", path = "../project2", features = ["test-support"] }

View File

@ -12,7 +12,7 @@ test-support = []
[dependencies] [dependencies]
client = { package = "client2", path = "../client2" } client = { package = "client2", path = "../client2" }
editor = { package = "editor2", path = "../editor2" } editor = { path = "../editor" }
fs = { package = "fs2", path = "../fs2" } fs = { package = "fs2", path = "../fs2" }
fuzzy = { package = "fuzzy2", path = "../fuzzy2" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
@ -34,4 +34,4 @@ schemars.workspace = true
serde.workspace = true serde.workspace = true
[dev-dependencies] [dev-dependencies]
editor = { package = "editor2", path = "../editor2", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] }

View File

@ -33,7 +33,7 @@ copilot = { package = "copilot2", path = "../copilot2" }
copilot_button = { path = "../copilot_button" } copilot_button = { path = "../copilot_button" }
diagnostics = { path = "../diagnostics" } diagnostics = { path = "../diagnostics" }
db = { package = "db2", path = "../db2" } db = { package = "db2", path = "../db2" }
editor = { package="editor2", path = "../editor2" } editor = { path = "../editor" }
feedback = { path = "../feedback" } feedback = { path = "../feedback" }
file_finder = { path = "../file_finder" } file_finder = { path = "../file_finder" }
search = { path = "../search" } search = { path = "../search" }