Detailed changes
@@ -18,7 +18,7 @@ dependencies = [
"theme2",
"ui2",
"util",
- "workspace2",
+ "workspace",
]
[[package]]
@@ -403,7 +403,7 @@ dependencies = [
"ui2",
"util",
"uuid 1.4.1",
- "workspace2",
+ "workspace",
]
[[package]]
@@ -770,7 +770,7 @@ dependencies = [
"tempdir",
"theme2",
"util",
- "workspace2",
+ "workspace",
]
[[package]]
@@ -1099,7 +1099,7 @@ dependencies = [
"settings2",
"theme2",
"ui2",
- "workspace2",
+ "workspace",
]
[[package]]
@@ -1767,7 +1767,7 @@ dependencies = [
"unindent",
"util",
"uuid 1.4.1",
- "workspace2",
+ "workspace",
]
[[package]]
@@ -1812,7 +1812,7 @@ dependencies = [
"ui2",
"util",
"vcs_menu",
- "workspace2",
+ "workspace",
"zed_actions2",
]
@@ -1867,7 +1867,7 @@ dependencies = [
"theme2",
"ui2",
"util",
- "workspace2",
+ "workspace",
"zed_actions2",
]
@@ -1986,7 +1986,7 @@ dependencies = [
"smol",
"theme2",
"util",
- "workspace2",
+ "workspace",
"zed_actions2",
]
@@ -2520,7 +2520,7 @@ dependencies = [
"ui2",
"unindent",
"util",
- "workspace2",
+ "workspace",
]
[[package]]
@@ -2706,7 +2706,7 @@ dependencies = [
"ui2",
"unindent",
"util",
- "workspace2",
+ "workspace",
]
[[package]]
@@ -2924,7 +2924,7 @@ dependencies = [
"ui2",
"urlencoding",
"util",
- "workspace2",
+ "workspace",
]
[[package]]
@@ -2949,7 +2949,7 @@ dependencies = [
"theme2",
"ui2",
"util",
- "workspace2",
+ "workspace",
]
[[package]]
@@ -3497,7 +3497,7 @@ dependencies = [
"theme2",
"ui2",
"util",
- "workspace2",
+ "workspace",
]
[[package]]
@@ -4223,7 +4223,7 @@ dependencies = [
"settings2",
"shellexpand",
"util",
- "workspace2",
+ "workspace",
]
[[package]]
@@ -4417,7 +4417,7 @@ dependencies = [
"theme2",
"ui2",
"util",
- "workspace2",
+ "workspace",
]
[[package]]
@@ -4441,7 +4441,7 @@ dependencies = [
"ui2",
"unindent",
"util",
- "workspace2",
+ "workspace",
]
[[package]]
@@ -5680,7 +5680,7 @@ dependencies = [
"theme2",
"ui2",
"util",
- "workspace2",
+ "workspace",
]
[[package]]
@@ -5901,7 +5901,7 @@ dependencies = [
"theme2",
"ui2",
"util",
- "workspace2",
+ "workspace",
]
[[package]]
@@ -6330,7 +6330,7 @@ dependencies = [
"ui2",
"unicase",
"util",
- "workspace2",
+ "workspace",
]
[[package]]
@@ -6353,7 +6353,7 @@ dependencies = [
"text2",
"theme2",
"util",
- "workspace2",
+ "workspace",
]
[[package]]
@@ -6521,7 +6521,7 @@ dependencies = [
"gpui2",
"search",
"ui2",
- "workspace2",
+ "workspace",
]
[[package]]
@@ -6703,7 +6703,7 @@ dependencies = [
"theme2",
"ui2",
"util",
- "workspace2",
+ "workspace",
]
[[package]]
@@ -7550,7 +7550,7 @@ dependencies = [
"ui2",
"unindent",
"util",
- "workspace2",
+ "workspace",
]
[[package]]
@@ -7678,7 +7678,7 @@ dependencies = [
"tree-sitter-typescript",
"unindent",
"util",
- "workspace2",
+ "workspace",
]
[[package]]
@@ -8843,7 +8843,7 @@ dependencies = [
"thiserror",
"ui2",
"util",
- "workspace2",
+ "workspace",
]
[[package]]
@@ -8980,7 +8980,7 @@ dependencies = [
"theme2",
"ui2",
"util",
- "workspace2",
+ "workspace",
]
[[package]]
@@ -10044,7 +10044,7 @@ dependencies = [
"picker",
"ui2",
"util",
- "workspace2",
+ "workspace",
]
[[package]]
@@ -10083,7 +10083,7 @@ dependencies = [
"tokio",
"ui2",
"util",
- "workspace2",
+ "workspace",
"zed_actions2",
]
@@ -10497,7 +10497,7 @@ dependencies = [
"ui2",
"util",
"vim",
- "workspace2",
+ "workspace",
]
[[package]]
@@ -10742,46 +10742,6 @@ dependencies = [
[[package]]
name = "workspace"
version = "0.1.0"
-dependencies = [
- "anyhow",
- "async-recursion 1.0.5",
- "bincode",
- "call",
- "client",
- "collections",
- "context_menu",
- "db",
- "drag_and_drop",
- "env_logger",
- "fs",
- "futures 0.3.28",
- "gpui",
- "indoc",
- "install_cli",
- "itertools 0.10.5",
- "language",
- "lazy_static",
- "log",
- "menu",
- "node_runtime",
- "parking_lot 0.11.2",
- "postage",
- "project",
- "schemars",
- "serde",
- "serde_derive",
- "serde_json",
- "settings",
- "smallvec",
- "terminal",
- "theme",
- "util",
- "uuid 1.4.1",
-]
-
-[[package]]
-name = "workspace2"
-version = "0.1.0"
dependencies = [
"anyhow",
"async-recursion 1.0.5",
@@ -11021,7 +10981,7 @@ dependencies = [
"uuid 1.4.1",
"vim",
"welcome",
- "workspace2",
+ "workspace",
"zed_actions2",
]
@@ -105,7 +105,7 @@ members = [
"crates/story",
"crates/vim",
"crates/vcs_menu",
- "crates/workspace2",
+ "crates/workspace",
"crates/welcome",
"crates/xtask",
"crates/zed",
@@ -18,7 +18,7 @@ settings = { path = "../settings2", package = "settings2" }
ui = { path = "../ui2", package = "ui2" }
util = { path = "../util" }
theme = { path = "../theme2", package = "theme2" }
-workspace = { path = "../workspace2", package = "workspace2" }
+workspace = { path = "../workspace", package = "workspace" }
anyhow.workspace = true
futures.workspace = true
@@ -25,7 +25,7 @@ settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
ui = { package = "ui2", path = "../ui2" }
util = { path = "../util" }
-workspace = { package = "workspace2", path = "../workspace2" }
+workspace = { path = "../workspace" }
uuid.workspace = true
log.workspace = true
@@ -16,7 +16,7 @@ menu = { package = "menu2", path = "../menu2" }
project = { package = "project2", path = "../project2" }
settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
-workspace = { package = "workspace2", path = "../workspace2" }
+workspace = { path = "../workspace" }
util = { path = "../util" }
anyhow.workspace = true
isahc.workspace = true
@@ -18,11 +18,11 @@ project = { package = "project2", path = "../project2" }
search = { path = "../search" }
settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
-workspace = { package = "workspace2", path = "../workspace2" }
+workspace = { path = "../workspace" }
outline = { path = "../outline" }
itertools = "0.10"
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
-workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }
@@ -79,7 +79,7 @@ project = { package = "project2", path = "../project2", features = ["test-suppor
rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] }
settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
theme = { package = "theme2", path = "../theme2" }
-workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }
collab_ui = { path = "../collab_ui", features = ["test-support"] }
@@ -50,7 +50,7 @@ theme_selector = { path = "../theme_selector" }
vcs_menu = { path = "../vcs_menu" }
ui = { package = "ui2", path = "../ui2" }
util = { path = "../util" }
-workspace = { package = "workspace2", path = "../workspace2" }
+workspace = { path = "../workspace" }
zed-actions = { package="zed_actions2", path = "../zed_actions2"}
anyhow.workspace = true
@@ -75,7 +75,7 @@ project = { package = "project2", path = "../project2", features = ["test-suppor
rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] }
settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
-workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }
pretty_assertions.workspace = true
tree-sitter-markdown.workspace = true
@@ -19,7 +19,7 @@ settings = { package = "settings2", path = "../settings2" }
ui = { package = "ui2", path = "../ui2" }
util = { path = "../util" }
theme = { package = "theme2", path = "../theme2" }
-workspace = { package="workspace2", path = "../workspace2" }
+workspace = { path = "../workspace" }
zed_actions = { package = "zed_actions2", path = "../zed_actions2" }
anyhow.workspace = true
serde.workspace = true
@@ -32,6 +32,6 @@ project = { package="project2", path = "../project2", features = ["test-support"
menu = { package = "menu2", path = "../menu2" }
go_to_line = { path = "../go_to_line" }
serde_json.workspace = true
-workspace = { package="workspace2", path = "../workspace2", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }
ctor.workspace = true
env_logger.workspace = true
@@ -31,6 +31,6 @@ project = { package="project2", path = "../project2", features = ["test-support"
menu = { package = "menu2", path = "../menu2" }
go_to_line = { package = "go_to_line2", path = "../go_to_line2" }
serde_json.workspace = true
-workspace = { package="workspace2", path = "../workspace2", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }
ctor.workspace = true
env_logger.workspace = true
@@ -18,7 +18,7 @@ language = { package = "language2", path = "../language2" }
settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
util = { path = "../util" }
-workspace = { package = "workspace2", path = "../workspace2" }
+workspace = {path = "../workspace" }
anyhow.workspace = true
smol.workspace = true
futures.workspace = true
@@ -19,7 +19,7 @@ project = { package = "project2", path = "../project2" }
settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
util = { path = "../util" }
-workspace = { package = "workspace2", path = "../workspace2" }
+workspace = {path = "../workspace" }
log.workspace = true
anyhow.workspace = true
@@ -36,7 +36,7 @@ editor = { path = "../editor", features = ["test-support"] }
language = { package = "language2", path = "../language2", features = ["test-support"] }
lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
-workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
+workspace = {path = "../workspace", features = ["test-support"] }
theme = { package = "theme2", path = "../theme2", features = ["test-support"] }
serde_json.workspace = true
@@ -46,7 +46,7 @@ theme = { package="theme2", path = "../theme2" }
ui = { package = "ui2", path = "../ui2" }
util = { path = "../util" }
sqlez = { path = "../sqlez" }
-workspace = { package = "workspace2", path = "../workspace2" }
+workspace = { path = "../workspace" }
aho-corasick = "1.1"
anyhow.workspace = true
@@ -80,7 +80,7 @@ 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"] }
+workspace = { path = "../workspace", features = ["test-support"] }
multi_buffer = { path = "../multi_buffer", features = ["test-support"] }
ctor.workspace = true
@@ -23,7 +23,7 @@ settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
ui = { package = "ui2", path = "../ui2" }
util = { path = "../util" }
-workspace = { package = "workspace2", path = "../workspace2"}
+workspace = { path = "../workspace"}
bitflags = "2.4.1"
human_bytes = "0.4.1"
@@ -21,7 +21,7 @@ text = { package = "text2", path = "../text2" }
util = { path = "../util" }
theme = { package = "theme2", path = "../theme2" }
ui = { package = "ui2", path = "../ui2" }
-workspace = { package = "workspace2", path = "../workspace2" }
+workspace = { path = "../workspace" }
postage.workspace = true
serde.workspace = true
@@ -29,7 +29,7 @@ serde.workspace = true
editor = { path = "../editor", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
language = { package = "language2", path = "../language2", features = ["test-support"] }
-workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }
theme = { package = "theme2", path = "../theme2", features = ["test-support"] }
serde_json.workspace = true
@@ -15,7 +15,7 @@ menu = { package = "menu2", path = "../menu2" }
serde.workspace = true
settings = { package = "settings2", path = "../settings2" }
text = { package = "text2", path = "../text2" }
-workspace = { package = "workspace2", path = "../workspace2" }
+workspace = { path = "../workspace" }
postage.workspace = true
theme = { package = "theme2", path = "../theme2" }
ui = { package = "ui2", path = "../ui2" }
@@ -12,7 +12,7 @@ doctest = false
editor = { path = "../editor" }
gpui = { package = "gpui2", path = "../gpui2" }
util = { path = "../util" }
-workspace2 = { path = "../workspace2" }
+workspace = { path = "../workspace" }
settings2 = { path = "../settings2" }
anyhow.workspace = true
@@ -9,7 +9,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
-use workspace2::{AppState, Workspace};
+use workspace::{AppState, Workspace};
actions!(journal, [NewJournalEntry]);
@@ -93,7 +93,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut AppContext) {
cx.spawn(|mut cx| async move {
let (journal_dir, entry_path) = create_entry.await?;
let (workspace, _) = cx
- .update(|cx| workspace2::open_paths(&[journal_dir], &app_state, None, cx))?
+ .update(|cx| workspace::open_paths(&[journal_dir], &app_state, None, cx))?
.await?;
let _opened = workspace
@@ -19,7 +19,7 @@ theme = { package = "theme2", path = "../theme2" }
ui = { package = "ui2", path = "../ui2" }
settings = { package = "settings2", path = "../settings2" }
util = { path = "../util" }
-workspace = { package = "workspace2", path = "../workspace2" }
+workspace = { path = "../workspace" }
anyhow.workspace = true
[dev-dependencies]
@@ -15,7 +15,7 @@ settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
language = { package = "language2", path = "../language2" }
project = { package = "project2", path = "../project2" }
-workspace = { package = "workspace2", path = "../workspace2" }
+workspace = { path = "../workspace" }
gpui = { package = "gpui2", path = "../gpui2" }
ui = { package = "ui2", path = "../ui2" }
util = { path = "../util" }
@@ -18,7 +18,7 @@ picker = { path = "../picker" }
settings = { package = "settings2", path = "../settings2" }
text = { package = "text2", path = "../text2" }
theme = { package = "theme2", path = "../theme2" }
-workspace = { package = "workspace2", path = "../workspace2" }
+workspace = { path = "../workspace" }
util = { path = "../util" }
ordered-float.workspace = true
@@ -16,7 +16,7 @@ menu = { package = "menu2", path = "../menu2" }
settings = { package = "settings2", path = "../settings2" }
util = { path = "../util" }
theme = { package = "theme2", path = "../theme2" }
-workspace = { package = "workspace2", path = "../workspace2"}
+workspace = { path = "../workspace"}
parking_lot.workspace = true
@@ -20,7 +20,7 @@ settings = { path = "../settings2", package = "settings2" }
theme = { path = "../theme2", package = "theme2" }
ui = { path = "../ui2", package = "ui2" }
util = { path = "../util" }
-workspace = { path = "../workspace2", package = "workspace2" }
+workspace = { path = "../workspace", package = "workspace" }
anyhow.workspace = true
postage.workspace = true
futures.workspace = true
@@ -37,5 +37,5 @@ client = { path = "../client2", package = "client2", features = ["test-support"]
language = { path = "../language2", package = "language2", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui2", package = "gpui2", features = ["test-support"] }
-workspace = { path = "../workspace2", package = "workspace2", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }
serde_json.workspace = true
@@ -10,13 +10,13 @@ doctest = false
[dependencies]
editor = { path = "../editor" }
-fuzzy = {package = "fuzzy2", path = "../fuzzy2" }
-gpui = {package = "gpui2", path = "../gpui2" }
-picker = {path = "../picker" }
+fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+picker = { path = "../picker" }
project = { package = "project2", path = "../project2" }
-text = {package = "text2", path = "../text2" }
-settings = {package = "settings2", path = "../settings2" }
-workspace = {package = "workspace2", path = "../workspace2" }
+text = { package = "text2", path = "../text2" }
+settings = { package = "settings2", path = "../settings2" }
+workspace = { path = "../workspace" }
theme = { package = "theme2", path = "../theme2" }
util = { path = "../util" }
@@ -34,4 +34,4 @@ language = { package = "language2", path = "../language2", features = ["test-sup
lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] }
project = { package = "project2", path = "../project2", features = ["test-support"] }
theme = { package = "theme2", path = "../theme2", features = ["test-support"] }
-workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }
@@ -13,10 +13,10 @@ assistant = { package = "assistant2", path = "../assistant2" }
editor = { path = "../editor" }
gpui = { package = "gpui2", path = "../gpui2" }
search = { path = "../search" }
-workspace = { package = "workspace2", path = "../workspace2" }
+workspace = { path = "../workspace" }
ui = { package = "ui2", path = "../ui2" }
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
-workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }
@@ -19,7 +19,7 @@ text = { package = "text2", path = "../text2" }
util = { path = "../util"}
theme = { package = "theme2", path = "../theme2" }
ui = { package = "ui2", path = "../ui2" }
-workspace = { package = "workspace2", path = "../workspace2" }
+workspace = { path = "../workspace" }
futures.workspace = true
ordered-float.workspace = true
@@ -20,7 +20,7 @@ settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
util = { path = "../util" }
ui = {package = "ui2", path = "../ui2"}
-workspace = { package = "workspace2", path = "../workspace2" }
+workspace = { path = "../workspace" }
semantic_index = { package = "semantic_index2", path = "../semantic_index2" }
anyhow.workspace = true
futures.workspace = true
@@ -36,5 +36,5 @@ client = { package = "client2", path = "../client2", features = ["test-support"]
editor = { path = "../editor", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
-workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }
unindent.workspace = true
@@ -14,7 +14,7 @@ collections = { path = "../collections" }
gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" }
project = { package = "project2", path = "../project2" }
-workspace = { package = "workspace2", path = "../workspace2" }
+workspace = { path = "../workspace" }
util = { path = "../util" }
rpc = { package = "rpc2", path = "../rpc2" }
settings = { package = "settings2", path = "../settings2" }
@@ -45,7 +45,7 @@ gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
language = { package = "language2", path = "../language2", features = ["test-support"] }
project = { package = "project2", path = "../project2", features = ["test-support"] }
rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] }
-workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }
settings = { package = "settings2", path = "../settings2", features = ["test-support"]}
rust-embed = { version = "8.0", features = ["include-exclude"] }
client = { package = "client2", path = "../client2" }
@@ -17,7 +17,7 @@ project = { package = "project2", path = "../project2" }
settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
util = { path = "../util" }
-workspace = { package = "workspace2", path = "../workspace2" }
+workspace = { path = "../workspace" }
db = { package = "db2", path = "../db2" }
procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false }
terminal = { package = "terminal2", path = "../terminal2" }
@@ -42,5 +42,5 @@ editor = { path = "../editor", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
client = { package = "client2", path = "../client2", features = ["test-support"]}
project = { package = "project2", path = "../project2", features = ["test-support"]}
-workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }
rand.workspace = true
@@ -20,7 +20,7 @@ settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
ui = { package = "ui2", path = "../ui2" }
util = { path = "../util" }
-workspace = { package = "workspace2", path = "../workspace2" }
+workspace = { path = "../workspace" }
log.workspace = true
parking_lot.workspace = true
postage.workspace = true
@@ -12,6 +12,6 @@ gpui = {package = "gpui2", path = "../gpui2"}
picker = {path = "../picker"}
util = {path = "../util"}
ui = {package = "ui2", path = "../ui2"}
-workspace = {package = "workspace2", path = "../workspace2"}
+workspace = { path = "../workspace" }
anyhow.workspace = true
@@ -31,7 +31,7 @@ gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" }
search = { path = "../search" }
settings = { package = "settings2", path = "../settings2" }
-workspace = { package = "workspace2", path = "../workspace2" }
+workspace = { path = "../workspace" }
theme = { package = "theme2", path = "../theme2" }
ui = { package = "ui2", path = "../ui2"}
diagnostics = { path = "../diagnostics" }
@@ -48,6 +48,6 @@ language = { package = "language2", path = "../language2", features = ["test-sup
project = { package = "project2", path = "../project2", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
settings = { package = "settings2", path = "../settings2" }
-workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }
theme = { package = "theme2", path = "../theme2", features = ["test-support"] }
lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] }
@@ -25,7 +25,7 @@ theme = { package = "theme2", path = "../theme2" }
theme_selector = { path = "../theme_selector" }
util = { path = "../util" }
picker = { path = "../picker" }
-workspace = { package = "workspace2", path = "../workspace2" }
+workspace = { path = "../workspace" }
vim = { path = "../vim" }
anyhow.workspace = true
@@ -19,23 +19,23 @@ test-support = [
]
[dependencies]
-db = { path = "../db" }
-call = { path = "../call" }
-client = { path = "../client" }
+db = { path = "../db2", package = "db2" }
+call = { path = "../call2", package = "call2" }
+client = { path = "../client2", package = "client2" }
collections = { path = "../collections" }
-context_menu = { path = "../context_menu" }
-drag_and_drop = { path = "../drag_and_drop" }
-fs = { path = "../fs" }
-gpui = { path = "../gpui" }
-install_cli = { path = "../install_cli" }
-language = { path = "../language" }
-menu = { path = "../menu" }
+# context_menu = { path = "../context_menu" }
+fs = { path = "../fs2", package = "fs2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+install_cli = { path = "../install_cli2", package = "install_cli2" }
+language = { path = "../language2", package = "language2" }
+#menu = { path = "../menu" }
node_runtime = { path = "../node_runtime" }
-project = { path = "../project" }
-settings = { path = "../settings" }
-terminal = { path = "../terminal" }
-theme = { path = "../theme" }
+project = { path = "../project2", package = "project2" }
+settings = { path = "../settings2", package = "settings2" }
+terminal = { path = "../terminal2", package = "terminal2" }
+theme = { path = "../theme2", package = "theme2" }
util = { path = "../util" }
+ui = { package = "ui2", path = "../ui2" }
async-recursion = "1.0.0"
itertools = "0.10"
@@ -54,13 +54,13 @@ smallvec.workspace = true
uuid.workspace = true
[dev-dependencies]
-call = { path = "../call", features = ["test-support"] }
-client = { path = "../client", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
-project = { path = "../project", features = ["test-support"] }
-settings = { path = "../settings", features = ["test-support"] }
-fs = { path = "../fs", features = ["test-support"] }
-db = { path = "../db", features = ["test-support"] }
+call = { path = "../call2", package = "call2", features = ["test-support"] }
+client = { path = "../client2", package = "client2", features = ["test-support"] }
+gpui = { path = "../gpui2", package = "gpui2", features = ["test-support"] }
+project = { path = "../project2", package = "project2", features = ["test-support"] }
+settings = { path = "../settings2", package = "settings2", features = ["test-support"] }
+fs = { path = "../fs2", package = "fs2", features = ["test-support"] }
+db = { path = "../db2", package = "db2", features = ["test-support"] }
indoc.workspace = true
env_logger.workspace = true
@@ -1,70 +1,76 @@
-use crate::{StatusItemView, Workspace, WorkspaceBounds};
-use context_menu::{ContextMenu, ContextMenuItem};
+use crate::DraggedDock;
+use crate::{status_bar::StatusItemView, Workspace};
use gpui::{
- elements::*, platform::CursorStyle, platform::MouseButton, Action, AnyViewHandle, AppContext,
- Axis, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
+ div, px, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Entity, EntityId,
+ EventEmitter, FocusHandle, FocusableView, IntoElement, MouseButton, ParentElement, Render,
+ SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView, WindowContext,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use std::rc::Rc;
-use theme::ThemeSettings;
+use std::sync::Arc;
+use ui::{h_stack, ContextMenu, IconButton, Tooltip};
+use ui::{prelude::*, right_click_menu};
+
+const RESIZE_HANDLE_SIZE: Pixels = Pixels(6.);
+
+pub enum PanelEvent {
+ ChangePosition,
+ ZoomIn,
+ ZoomOut,
+ Activate,
+ Close,
+ Focus,
+}
-pub trait Panel: View {
+pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
+ fn persistent_name() -> &'static str;
fn position(&self, cx: &WindowContext) -> DockPosition;
fn position_is_valid(&self, position: DockPosition) -> bool;
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
- fn size(&self, cx: &WindowContext) -> f32;
- fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>);
- fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>;
- fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>);
+ fn size(&self, cx: &WindowContext) -> Pixels;
+ fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>);
+ fn icon(&self, cx: &WindowContext) -> Option<ui::Icon>;
+ fn icon_tooltip(&self, cx: &WindowContext) -> Option<&'static str>;
+ fn toggle_action(&self) -> Box<dyn Action>;
fn icon_label(&self, _: &WindowContext) -> Option<String> {
None
}
- fn should_change_position_on_event(_: &Self::Event) -> bool;
- fn should_zoom_in_on_event(_: &Self::Event) -> bool {
- false
- }
- fn should_zoom_out_on_event(_: &Self::Event) -> bool {
- false
- }
fn is_zoomed(&self, _cx: &WindowContext) -> bool {
false
}
fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {}
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
- fn should_activate_on_event(_: &Self::Event) -> bool {
- false
- }
- fn should_close_on_event(_: &Self::Event) -> bool {
- false
- }
- fn has_focus(&self, cx: &WindowContext) -> bool;
- fn is_focus_event(_: &Self::Event) -> bool;
}
-pub trait PanelHandle {
- fn id(&self) -> usize;
+pub trait PanelHandle: Send + Sync {
+ fn panel_id(&self) -> EntityId;
+ fn persistent_name(&self) -> &'static str;
fn position(&self, cx: &WindowContext) -> DockPosition;
fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool;
fn set_position(&self, position: DockPosition, cx: &mut WindowContext);
fn is_zoomed(&self, cx: &WindowContext) -> bool;
fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext);
fn set_active(&self, active: bool, cx: &mut WindowContext);
- fn size(&self, cx: &WindowContext) -> f32;
- fn set_size(&self, size: Option<f32>, cx: &mut WindowContext);
- fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>;
- fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>);
+ fn size(&self, cx: &WindowContext) -> Pixels;
+ fn set_size(&self, size: Option<Pixels>, cx: &mut WindowContext);
+ fn icon(&self, cx: &WindowContext) -> Option<ui::Icon>;
+ fn icon_tooltip(&self, cx: &WindowContext) -> Option<&'static str>;
+ fn toggle_action(&self, cx: &WindowContext) -> Box<dyn Action>;
fn icon_label(&self, cx: &WindowContext) -> Option<String>;
- fn has_focus(&self, cx: &WindowContext) -> bool;
- fn as_any(&self) -> &AnyViewHandle;
+ fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
+ fn to_any(&self) -> AnyView;
}
-impl<T> PanelHandle for ViewHandle<T>
+impl<T> PanelHandle for View<T>
where
T: Panel,
{
- fn id(&self) -> usize {
- self.id()
+ fn panel_id(&self) -> EntityId {
+ Entity::entity_id(self)
+ }
+
+ fn persistent_name(&self) -> &'static str {
+ T::persistent_name()
}
fn position(&self, cx: &WindowContext) -> DockPosition {
@@ -79,14 +85,6 @@ where
self.update(cx, |this, cx| this.set_position(position, cx))
}
- fn size(&self, cx: &WindowContext) -> f32 {
- self.read(cx).size(cx)
- }
-
- fn set_size(&self, size: Option<f32>, cx: &mut WindowContext) {
- self.update(cx, |this, cx| this.set_size(size, cx))
- }
-
fn is_zoomed(&self, cx: &WindowContext) -> bool {
self.read(cx).is_zoomed(cx)
}
@@ -99,30 +97,42 @@ where
self.update(cx, |this, cx| this.set_active(active, cx))
}
- fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> {
- self.read(cx).icon_path(cx)
+ fn size(&self, cx: &WindowContext) -> Pixels {
+ self.read(cx).size(cx)
}
- fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>) {
- self.read(cx).icon_tooltip()
+ fn set_size(&self, size: Option<Pixels>, cx: &mut WindowContext) {
+ self.update(cx, |this, cx| this.set_size(size, cx))
+ }
+
+ fn icon(&self, cx: &WindowContext) -> Option<ui::Icon> {
+ self.read(cx).icon(cx)
+ }
+
+ fn icon_tooltip(&self, cx: &WindowContext) -> Option<&'static str> {
+ self.read(cx).icon_tooltip(cx)
+ }
+
+ fn toggle_action(&self, cx: &WindowContext) -> Box<dyn Action> {
+ self.read(cx).toggle_action()
}
fn icon_label(&self, cx: &WindowContext) -> Option<String> {
self.read(cx).icon_label(cx)
}
- fn has_focus(&self, cx: &WindowContext) -> bool {
- self.read(cx).has_focus(cx)
+ fn to_any(&self) -> AnyView {
+ self.clone().into()
}
- fn as_any(&self) -> &AnyViewHandle {
- self
+ fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+ self.read(cx).focus_handle(cx).clone()
}
}
-impl From<&dyn PanelHandle> for AnyViewHandle {
+impl From<&dyn PanelHandle> for AnyView {
fn from(val: &dyn PanelHandle) -> Self {
- val.as_any().clone()
+ val.to_any()
}
}
@@ -131,6 +141,14 @@ pub struct Dock {
panel_entries: Vec<PanelEntry>,
is_open: bool,
active_panel_index: usize,
+ focus_handle: FocusHandle,
+ _focus_subscription: Subscription,
+}
+
+impl FocusableView for Dock {
+ fn focus_handle(&self, _: &AppContext) -> FocusHandle {
+ self.focus_handle.clone()
+ }
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
@@ -150,13 +168,14 @@ impl DockPosition {
}
}
- fn to_resize_handle_side(self) -> HandleSide {
- match self {
- Self::Left => HandleSide::Right,
- Self::Bottom => HandleSide::Top,
- Self::Right => HandleSide::Left,
- }
- }
+ // todo!()
+ // fn to_resize_handle_side(self) -> HandleSide {
+ // match self {
+ // Self::Left => HandleSide::Right,
+ // Self::Bottom => HandleSide::Top,
+ // Self::Right => HandleSide::Left,
+ // }
+ // }
pub fn axis(&self) -> Axis {
match self {
@@ -167,24 +186,54 @@ impl DockPosition {
}
struct PanelEntry {
- panel: Rc<dyn PanelHandle>,
- context_menu: ViewHandle<ContextMenu>,
+ panel: Arc<dyn PanelHandle>,
+ // todo!()
+ // context_menu: View<ContextMenu>,
_subscriptions: [Subscription; 2],
}
pub struct PanelButtons {
- dock: ViewHandle<Dock>,
- workspace: WeakViewHandle<Workspace>,
+ dock: View<Dock>,
}
impl Dock {
- pub fn new(position: DockPosition) -> Self {
- Self {
- position,
- panel_entries: Default::default(),
- active_panel_index: 0,
- is_open: false,
- }
+ pub fn new(position: DockPosition, cx: &mut ViewContext<Workspace>) -> View<Self> {
+ let focus_handle = cx.focus_handle();
+
+ let dock = cx.new_view(|cx: &mut ViewContext<Self>| {
+ let focus_subscription = cx.on_focus(&focus_handle, |dock, cx| {
+ if let Some(active_entry) = dock.panel_entries.get(dock.active_panel_index) {
+ active_entry.panel.focus_handle(cx).focus(cx)
+ }
+ });
+ Self {
+ position,
+ panel_entries: Default::default(),
+ active_panel_index: 0,
+ is_open: false,
+ focus_handle: focus_handle.clone(),
+ _focus_subscription: focus_subscription,
+ }
+ });
+
+ cx.observe(&dock, move |workspace, dock, cx| {
+ if dock.read(cx).is_open() {
+ if let Some(panel) = dock.read(cx).active_panel() {
+ if panel.is_zoomed(cx) {
+ workspace.zoomed = Some(panel.to_any().downgrade());
+ workspace.zoomed_position = Some(position);
+ return;
+ }
+ }
+ }
+ if workspace.zoomed_position == Some(position) {
+ workspace.zoomed = None;
+ workspace.zoomed_position = None;
+ }
+ })
+ .detach();
+
+ dock
}
pub fn position(&self) -> DockPosition {
@@ -195,28 +244,32 @@ impl Dock {
self.is_open
}
- pub fn has_focus(&self, cx: &WindowContext) -> bool {
- self.visible_panel()
- .map_or(false, |panel| panel.has_focus(cx))
- }
+ // todo!()
+ // pub fn has_focus(&self, cx: &WindowContext) -> bool {
+ // self.visible_panel()
+ // .map_or(false, |panel| panel.has_focus(cx))
+ // }
- pub fn panel<T: Panel>(&self) -> Option<ViewHandle<T>> {
+ pub fn panel<T: Panel>(&self) -> Option<View<T>> {
self.panel_entries
.iter()
- .find_map(|entry| entry.panel.as_any().clone().downcast())
+ .find_map(|entry| entry.panel.to_any().clone().downcast().ok())
}
pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
self.panel_entries
.iter()
- .position(|entry| entry.panel.as_any().is::<T>())
+ .position(|entry| entry.panel.to_any().downcast::<T>().is_ok())
}
- pub fn panel_index_for_ui_name(&self, ui_name: &str, cx: &AppContext) -> Option<usize> {
- self.panel_entries.iter().position(|entry| {
- let panel = entry.panel.as_any();
- cx.view_ui_name(panel.window(), panel.id()) == Some(ui_name)
- })
+ pub fn panel_index_for_persistent_name(
+ &self,
+ ui_name: &str,
+ _cx: &AppContext,
+ ) -> Option<usize> {
+ self.panel_entries
+ .iter()
+ .position(|entry| entry.panel.persistent_name() == ui_name)
}
pub fn active_panel_index(&self) -> usize {
@@ -234,14 +287,9 @@ impl Dock {
}
}
- pub fn set_panel_zoomed(
- &mut self,
- panel: &AnyViewHandle,
- zoomed: bool,
- cx: &mut ViewContext<Self>,
- ) {
+ pub fn set_panel_zoomed(&mut self, panel: &AnyView, zoomed: bool, cx: &mut ViewContext<Self>) {
for entry in &mut self.panel_entries {
- if entry.panel.as_any() == panel {
+ if entry.panel.panel_id() == panel.entity_id() {
if zoomed != entry.panel.is_zoomed(cx) {
entry.panel.set_zoomed(zoomed, cx);
}
@@ -261,46 +309,115 @@ impl Dock {
}
}
- pub(crate) fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>) {
+ pub(crate) fn add_panel<T: Panel>(
+ &mut self,
+ panel: View<T>,
+ workspace: WeakView<Workspace>,
+ cx: &mut ViewContext<Self>,
+ ) {
let subscriptions = [
cx.observe(&panel, |_, _, cx| cx.notify()),
- cx.subscribe(&panel, |this, panel, event, cx| {
- if T::should_activate_on_event(event) {
+ cx.subscribe(&panel, move |this, panel, event, cx| match event {
+ PanelEvent::ChangePosition => {
+ let new_position = panel.read(cx).position(cx);
+
+ let Ok(new_dock) = workspace.update(cx, |workspace, cx| {
+ if panel.is_zoomed(cx) {
+ workspace.zoomed_position = Some(new_position);
+ }
+ match new_position {
+ DockPosition::Left => &workspace.left_dock,
+ DockPosition::Bottom => &workspace.bottom_dock,
+ DockPosition::Right => &workspace.right_dock,
+ }
+ .clone()
+ }) else {
+ return;
+ };
+
+ let was_visible = this.is_open()
+ && this.visible_panel().map_or(false, |active_panel| {
+ active_panel.panel_id() == Entity::entity_id(&panel)
+ });
+
+ this.remove_panel(&panel, cx);
+
+ new_dock.update(cx, |new_dock, cx| {
+ new_dock.add_panel(panel.clone(), workspace.clone(), cx);
+ if was_visible {
+ new_dock.set_open(true, cx);
+ new_dock.activate_panel(new_dock.panels_len() - 1, cx);
+ }
+ });
+ }
+ PanelEvent::ZoomIn => {
+ this.set_panel_zoomed(&panel.to_any(), true, cx);
+ if !panel.focus_handle(cx).contains_focused(cx) {
+ cx.focus_view(&panel);
+ }
+ workspace
+ .update(cx, |workspace, cx| {
+ workspace.zoomed = Some(panel.downgrade().into());
+ workspace.zoomed_position = Some(panel.read(cx).position(cx));
+ })
+ .ok();
+ }
+ PanelEvent::ZoomOut => {
+ this.set_panel_zoomed(&panel.to_any(), false, cx);
+ workspace
+ .update(cx, |workspace, cx| {
+ if workspace.zoomed_position == Some(this.position) {
+ workspace.zoomed = None;
+ workspace.zoomed_position = None;
+ }
+ cx.notify();
+ })
+ .ok();
+ }
+ // todo!() we do not use this event in the production code (even in zed1), remove it
+ PanelEvent::Activate => {
if let Some(ix) = this
.panel_entries
.iter()
- .position(|entry| entry.panel.id() == panel.id())
+ .position(|entry| entry.panel.panel_id() == Entity::entity_id(&panel))
{
this.set_open(true, cx);
this.activate_panel(ix, cx);
- cx.focus(&panel);
+ cx.focus_view(&panel);
+ }
+ }
+ PanelEvent::Close => {
+ if this
+ .visible_panel()
+ .map_or(false, |p| p.panel_id() == Entity::entity_id(&panel))
+ {
+ this.set_open(false, cx);
}
- } else if T::should_close_on_event(event)
- && this.visible_panel().map_or(false, |p| p.id() == panel.id())
- {
- this.set_open(false, cx);
}
+ PanelEvent::Focus => {}
}),
];
- let dock_view_id = cx.view_id();
+ // todo!()
+ // let dock_view_id = cx.view_id();
self.panel_entries.push(PanelEntry {
- panel: Rc::new(panel),
- context_menu: cx.add_view(|cx| {
- let mut menu = ContextMenu::new(dock_view_id, cx);
- menu.set_position_mode(OverlayPositionMode::Local);
- menu
- }),
+ panel: Arc::new(panel),
+ // todo!()
+ // context_menu: cx.add_view(|cx| {
+ // let mut menu = ContextMenu::new(dock_view_id, cx);
+ // menu.set_position_mode(OverlayPositionMode::Local);
+ // menu
+ // }),
_subscriptions: subscriptions,
});
cx.notify()
}
- pub fn remove_panel<T: Panel>(&mut self, panel: &ViewHandle<T>, cx: &mut ViewContext<Self>) {
+ pub fn remove_panel<T: Panel>(&mut self, panel: &View<T>, cx: &mut ViewContext<Self>) {
if let Some(panel_ix) = self
.panel_entries
.iter()
- .position(|entry| entry.panel.id() == panel.id())
+ .position(|entry| entry.panel.panel_id() == Entity::entity_id(panel))
{
if panel_ix == self.active_panel_index {
self.active_panel_index = 0;
@@ -332,12 +449,12 @@ impl Dock {
}
}
- pub fn visible_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
+ pub fn visible_panel(&self) -> Option<&Arc<dyn PanelHandle>> {
let entry = self.visible_entry()?;
Some(&entry.panel)
}
- pub fn active_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
+ pub fn active_panel(&self) -> Option<&Arc<dyn PanelHandle>> {
Some(&self.panel_entries.get(self.active_panel_index)?.panel)
}
@@ -349,7 +466,7 @@ impl Dock {
}
}
- pub fn zoomed_panel(&self, cx: &WindowContext) -> Option<Rc<dyn PanelHandle>> {
+ pub fn zoomed_panel(&self, cx: &WindowContext) -> Option<Arc<dyn PanelHandle>> {
let entry = self.visible_entry()?;
if entry.panel.is_zoomed(cx) {
Some(entry.panel.clone())
@@ -358,14 +475,14 @@ impl Dock {
}
}
- pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<f32> {
+ pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<Pixels> {
self.panel_entries
.iter()
- .find(|entry| entry.panel.id() == panel.id())
+ .find(|entry| entry.panel.panel_id() == panel.panel_id())
.map(|entry| entry.panel.size(cx))
}
- pub fn active_panel_size(&self, cx: &WindowContext) -> Option<f32> {
+ pub fn active_panel_size(&self, cx: &WindowContext) -> Option<Pixels> {
if self.is_open {
self.panel_entries
.get(self.active_panel_index)
@@ -375,304 +492,243 @@ impl Dock {
}
}
- pub fn resize_active_panel(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
+ pub fn resize_active_panel(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) {
+ let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE));
entry.panel.set_size(size, cx);
cx.notify();
}
}
- pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement<Workspace> {
- if let Some(active_entry) = self.visible_entry() {
- Empty::new()
- .into_any()
- .contained()
- .with_style(self.style(cx))
- .resizable::<WorkspaceBounds>(
- self.position.to_resize_handle_side(),
- active_entry.panel.size(cx),
- |_, _, _| {},
- )
- .into_any()
- } else {
- Empty::new().into_any()
+ pub fn toggle_action(&self) -> Box<dyn Action> {
+ match self.position {
+ DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
+ DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
+ DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
}
}
-
- fn style(&self, cx: &WindowContext) -> ContainerStyle {
- let theme = &settings::get::<ThemeSettings>(cx).theme;
- let style = match self.position {
- DockPosition::Left => theme.workspace.dock.left,
- DockPosition::Bottom => theme.workspace.dock.bottom,
- DockPosition::Right => theme.workspace.dock.right,
- };
- style
- }
}
-impl Entity for Dock {
- type Event = ();
-}
+impl Render for Dock {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ if let Some(entry) = self.visible_entry() {
+ let size = entry.panel.size(cx);
+
+ let position = self.position;
+ let mut handle = div()
+ .id("resize-handle")
+ .on_drag(DraggedDock(position), |dock, cx| {
+ cx.stop_propagation();
+ cx.new_view(|_| dock.clone())
+ })
+ .on_click(cx.listener(|v, e: &ClickEvent, cx| {
+ if e.down.button == MouseButton::Left && e.down.click_count == 2 {
+ v.resize_active_panel(None, cx);
+ cx.stop_propagation();
+ }
+ }))
+ .z_index(1)
+ .block_mouse();
+
+ match self.position() {
+ DockPosition::Left => {
+ handle = handle
+ .absolute()
+ .right(px(0.))
+ .top(px(0.))
+ .h_full()
+ .w(RESIZE_HANDLE_SIZE)
+ .cursor_col_resize();
+ }
+ DockPosition::Bottom => {
+ handle = handle
+ .absolute()
+ .top(px(0.))
+ .left(px(0.))
+ .w_full()
+ .h(RESIZE_HANDLE_SIZE)
+ .cursor_row_resize();
+ }
+ DockPosition::Right => {
+ handle = handle
+ .absolute()
+ .top(px(0.))
+ .left(px(0.))
+ .h_full()
+ .w(RESIZE_HANDLE_SIZE)
+ .cursor_col_resize();
+ }
+ }
-impl View for Dock {
- fn ui_name() -> &'static str {
- "Dock"
- }
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- if let Some(active_entry) = self.visible_entry() {
- let style = self.style(cx);
- ChildView::new(active_entry.panel.as_any(), cx)
- .contained()
- .with_style(style)
- .resizable::<WorkspaceBounds>(
- self.position.to_resize_handle_side(),
- active_entry.panel.size(cx),
- |dock: &mut Self, size, cx| dock.resize_active_panel(size, cx),
+ div()
+ .flex()
+ .bg(cx.theme().colors().panel_background)
+ .border_color(cx.theme().colors().border)
+ .overflow_hidden()
+ .map(|this| match self.position().axis() {
+ Axis::Horizontal => this.w(size).h_full().flex_row(),
+ Axis::Vertical => this.h(size).w_full().flex_col(),
+ })
+ .map(|this| match self.position() {
+ DockPosition::Left => this.border_r(),
+ DockPosition::Right => this.border_l(),
+ DockPosition::Bottom => this.border_t(),
+ })
+ .child(
+ div()
+ .map(|this| match self.position().axis() {
+ Axis::Horizontal => this.min_w(size).h_full(),
+ Axis::Vertical => this.min_h(size).w_full(),
+ })
+ .child(entry.panel.to_any()),
)
- .into_any()
+ .child(handle)
} else {
- Empty::new().into_any()
- }
- }
-
- fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
- if cx.is_self_focused() {
- if let Some(active_entry) = self.visible_entry() {
- cx.focus(active_entry.panel.as_any());
- } else {
- cx.focus_parent();
- }
+ div()
}
}
}
impl PanelButtons {
- pub fn new(
- dock: ViewHandle<Dock>,
- workspace: WeakViewHandle<Workspace>,
- cx: &mut ViewContext<Self>,
- ) -> Self {
+ pub fn new(dock: View<Dock>, cx: &mut ViewContext<Self>) -> Self {
cx.observe(&dock, |_, _, cx| cx.notify()).detach();
- Self { dock, workspace }
+ Self { dock }
}
}
-impl Entity for PanelButtons {
- type Event = ();
-}
-
-impl View for PanelButtons {
- fn ui_name() -> &'static str {
- "PanelButtons"
- }
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let theme = &settings::get::<ThemeSettings>(cx).theme;
- let tooltip_style = theme.tooltip.clone();
- let theme = &theme.workspace.status_bar.panel_buttons;
- let button_style = theme.button.clone();
+impl Render for PanelButtons {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ // todo!()
let dock = self.dock.read(cx);
- let active_ix = dock.active_panel_index;
+ let active_index = dock.active_panel_index;
let is_open = dock.is_open;
let dock_position = dock.position;
- let group_style = match dock_position {
- DockPosition::Left => theme.group_left,
- DockPosition::Bottom => theme.group_bottom,
- DockPosition::Right => theme.group_right,
- };
- let menu_corner = match dock_position {
- DockPosition::Left => AnchorCorner::BottomLeft,
- DockPosition::Bottom | DockPosition::Right => AnchorCorner::BottomRight,
+
+ let (menu_anchor, menu_attach) = match dock.position {
+ DockPosition::Left => (AnchorCorner::BottomLeft, AnchorCorner::TopLeft),
+ DockPosition::Bottom | DockPosition::Right => {
+ (AnchorCorner::BottomRight, AnchorCorner::TopRight)
+ }
};
- let panels = dock
+ let buttons = dock
.panel_entries
.iter()
- .map(|item| (item.panel.clone(), item.context_menu.clone()))
- .collect::<Vec<_>>();
- Flex::row()
- .with_children(panels.into_iter().enumerate().filter_map(
- |(panel_ix, (view, context_menu))| {
- let icon_path = view.icon_path(cx)?;
- let is_active = is_open && panel_ix == active_ix;
- let (tooltip, tooltip_action) = if is_active {
- (
- format!("Close {} dock", dock_position.to_label()),
- Some(match dock_position {
- DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
- DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
- DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
- }),
- )
- } else {
- view.icon_tooltip(cx)
- };
- Some(
- Stack::new()
- .with_child(
- MouseEventHandler::new::<Self, _>(panel_ix, cx, |state, cx| {
- let style = button_style.in_state(is_active);
-
- let style = style.style_for(state);
- Flex::row()
- .with_child(
- Svg::new(icon_path)
- .with_color(style.icon_color)
- .constrained()
- .with_width(style.icon_size)
- .aligned(),
- )
- .with_children(if let Some(label) = view.icon_label(cx) {
- Some(
- Label::new(label, style.label.text.clone())
- .contained()
- .with_style(style.label.container)
- .aligned(),
- )
- } else {
- None
+ .enumerate()
+ .filter_map(|(i, entry)| {
+ let icon = entry.panel.icon(cx)?;
+ let icon_tooltip = entry.panel.icon_tooltip(cx)?;
+ let name = entry.panel.persistent_name();
+ let panel = entry.panel.clone();
+
+ let is_active_button = i == active_index && is_open;
+
+ let (action, tooltip) = if is_active_button {
+ let action = dock.toggle_action();
+
+ let tooltip: SharedString =
+ format!("Close {} dock", dock.position.to_label()).into();
+
+ (action, tooltip)
+ } else {
+ let action = entry.panel.toggle_action(cx);
+
+ (action, icon_tooltip.into())
+ };
+
+ Some(
+ right_click_menu(name)
+ .menu(move |cx| {
+ const POSITIONS: [DockPosition; 3] = [
+ DockPosition::Left,
+ DockPosition::Right,
+ DockPosition::Bottom,
+ ];
+
+ ContextMenu::build(cx, |mut menu, cx| {
+ for position in POSITIONS {
+ if position != dock_position
+ && panel.position_is_valid(position, cx)
+ {
+ let panel = panel.clone();
+ menu = menu.entry(position.to_label(), None, move |cx| {
+ panel.set_position(position, cx);
})
- .constrained()
- .with_height(style.icon_size)
- .contained()
- .with_style(style.container)
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, {
- let tooltip_action =
- tooltip_action.as_ref().map(|action| action.boxed_clone());
- move |_, this, cx| {
- if let Some(tooltip_action) = &tooltip_action {
- let window = cx.window();
- let view_id = this.workspace.id();
- let tooltip_action = tooltip_action.boxed_clone();
- cx.spawn(|_, mut cx| async move {
- window.dispatch_action(
- view_id,
- &*tooltip_action,
- &mut cx,
- );
- })
- .detach();
- }
}
+ }
+ menu
+ })
+ })
+ .anchor(menu_anchor)
+ .attach(menu_attach)
+ .trigger(
+ IconButton::new(name, icon)
+ .icon_size(IconSize::Small)
+ .selected(is_active_button)
+ .on_click({
+ let action = action.boxed_clone();
+ move |_, cx| cx.dispatch_action(action.boxed_clone())
})
- .on_click(MouseButton::Right, {
- let view = view.clone();
- let menu = context_menu.clone();
- move |_, _, cx| {
- const POSITIONS: [DockPosition; 3] = [
- DockPosition::Left,
- DockPosition::Right,
- DockPosition::Bottom,
- ];
-
- menu.update(cx, |menu, cx| {
- let items = POSITIONS
- .into_iter()
- .filter(|position| {
- *position != dock_position
- && view.position_is_valid(*position, cx)
- })
- .map(|position| {
- let view = view.clone();
- ContextMenuItem::handler(
- format!("Dock {}", position.to_label()),
- move |cx| view.set_position(position, cx),
- )
- })
- .collect();
- menu.show(Default::default(), menu_corner, items, cx);
- })
- }
- })
- .with_tooltip::<Self>(
- panel_ix,
- tooltip,
- tooltip_action,
- tooltip_style.clone(),
- cx,
- ),
- )
- .with_child(ChildView::new(&context_menu, cx)),
- )
- },
- ))
- .contained()
- .with_style(group_style)
- .into_any()
+ .tooltip(move |cx| {
+ Tooltip::for_action(tooltip.clone(), &*action, cx)
+ }),
+ ),
+ )
+ });
+
+ h_stack().gap_0p5().children(buttons)
}
}
impl StatusItemView for PanelButtons {
fn set_active_pane_item(
&mut self,
- _: Option<&dyn crate::ItemHandle>,
- _: &mut ViewContext<Self>,
+ _active_pane_item: Option<&dyn crate::ItemHandle>,
+ _cx: &mut ViewContext<Self>,
) {
+ // Nothing to do, panel buttons don't depend on the active center item
}
}
#[cfg(any(test, feature = "test-support"))]
pub mod test {
use super::*;
- use gpui::{ViewContext, WindowContext};
-
- #[derive(Debug)]
- pub enum TestPanelEvent {
- PositionChanged,
- Activated,
- Closed,
- ZoomIn,
- ZoomOut,
- Focus,
- }
+ use gpui::{actions, div, ViewContext, WindowContext};
pub struct TestPanel {
pub position: DockPosition,
pub zoomed: bool,
pub active: bool,
- pub has_focus: bool,
- pub size: f32,
+ pub focus_handle: FocusHandle,
+ pub size: Pixels,
}
+ actions!(test, [ToggleTestPanel]);
+
+ impl EventEmitter<PanelEvent> for TestPanel {}
impl TestPanel {
- pub fn new(position: DockPosition) -> Self {
+ pub fn new(position: DockPosition, cx: &mut WindowContext) -> Self {
Self {
position,
zoomed: false,
active: false,
- has_focus: false,
- size: 300.,
+ focus_handle: cx.focus_handle(),
+ size: px(300.),
}
}
}
- impl Entity for TestPanel {
- type Event = TestPanelEvent;
+ impl Render for TestPanel {
+ fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+ div()
+ }
}
- impl View for TestPanel {
- fn ui_name() -> &'static str {
+ impl Panel for TestPanel {
+ fn persistent_name() -> &'static str {
"TestPanel"
}
- fn render(&mut self, _: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
- Empty::new().into_any()
- }
-
- fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
- self.has_focus = true;
- cx.emit(TestPanelEvent::Focus);
- }
-
- fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
- self.has_focus = false;
- }
- }
-
- impl Panel for TestPanel {
fn position(&self, _: &gpui::WindowContext) -> super::DockPosition {
self.position
}
@@ -683,63 +739,45 @@ pub mod test {
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
self.position = position;
- cx.emit(TestPanelEvent::PositionChanged);
+ cx.emit(PanelEvent::ChangePosition);
}
- fn is_zoomed(&self, _: &WindowContext) -> bool {
- self.zoomed
- }
-
- fn set_zoomed(&mut self, zoomed: bool, _cx: &mut ViewContext<Self>) {
- self.zoomed = zoomed;
- }
-
- fn set_active(&mut self, active: bool, _cx: &mut ViewContext<Self>) {
- self.active = active;
- }
-
- fn size(&self, _: &WindowContext) -> f32 {
+ fn size(&self, _: &WindowContext) -> Pixels {
self.size
}
- fn set_size(&mut self, size: Option<f32>, _: &mut ViewContext<Self>) {
- self.size = size.unwrap_or(300.);
- }
-
- fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
- Some("icons/test_panel.svg")
- }
-
- fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
- ("Test Panel".into(), None)
+ fn set_size(&mut self, size: Option<Pixels>, _: &mut ViewContext<Self>) {
+ self.size = size.unwrap_or(px(300.));
}
- fn should_change_position_on_event(event: &Self::Event) -> bool {
- matches!(event, TestPanelEvent::PositionChanged)
+ fn icon(&self, _: &WindowContext) -> Option<ui::Icon> {
+ None
}
- fn should_zoom_in_on_event(event: &Self::Event) -> bool {
- matches!(event, TestPanelEvent::ZoomIn)
+ fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
+ None
}
- fn should_zoom_out_on_event(event: &Self::Event) -> bool {
- matches!(event, TestPanelEvent::ZoomOut)
+ fn toggle_action(&self) -> Box<dyn Action> {
+ ToggleTestPanel.boxed_clone()
}
- fn should_activate_on_event(event: &Self::Event) -> bool {
- matches!(event, TestPanelEvent::Activated)
+ fn is_zoomed(&self, _: &WindowContext) -> bool {
+ self.zoomed
}
- fn should_close_on_event(event: &Self::Event) -> bool {
- matches!(event, TestPanelEvent::Closed)
+ fn set_zoomed(&mut self, zoomed: bool, _cx: &mut ViewContext<Self>) {
+ self.zoomed = zoomed;
}
- fn has_focus(&self, _cx: &WindowContext) -> bool {
- self.has_focus
+ fn set_active(&mut self, active: bool, _cx: &mut ViewContext<Self>) {
+ self.active = active;
}
+ }
- fn is_focus_event(event: &Self::Event) -> bool {
- matches!(event, TestPanelEvent::Focus)
+ impl FocusableView for TestPanel {
+ fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
+ self.focus_handle.clone()
}
}
}
@@ -1,29 +1,29 @@
use crate::{
- pane, persistence::model::ItemId, searchable::SearchableItemHandle, FollowableItemBuilders,
- ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
+ pane::{self, Pane},
+ persistence::model::ItemId,
+ searchable::SearchableItemHandle,
+ workspace_settings::{AutosaveSetting, WorkspaceSettings},
+ DelayedDebouncedEditAction, FollowableItemBuilders, ItemNavHistory, ToolbarItemLocation,
+ ViewId, Workspace, WorkspaceId,
};
-use crate::{AutosaveSetting, DelayedDebouncedEditAction, WorkspaceSettings};
use anyhow::Result;
use client::{
proto::{self, PeerId},
Client,
};
-use gpui::geometry::vector::Vector2F;
-use gpui::AnyWindowHandle;
use gpui::{
- fonts::HighlightStyle, AnyElement, AnyViewHandle, AppContext, ModelHandle, Task, View,
- ViewContext, ViewHandle, WeakViewHandle, WindowContext,
+ AnyElement, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, FocusableView,
+ HighlightStyle, Model, Pixels, Point, SharedString, Task, View, ViewContext, WeakView,
+ WindowContext,
};
use project::{Project, ProjectEntryId, ProjectPath};
use schemars::JsonSchema;
-use serde_derive::{Deserialize, Serialize};
-use settings::Setting;
+use serde::{Deserialize, Serialize};
+use settings::Settings;
use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
- borrow::Cow,
cell::RefCell,
- fmt,
ops::Range,
path::PathBuf,
rc::Rc,
@@ -64,7 +64,7 @@ pub struct ItemSettingsContent {
close_position: Option<ClosePosition>,
}
-impl Setting for ItemSettings {
+impl Settings for ItemSettings {
const KEY: Option<&'static str> = Some("tabs");
type FileContent = ItemSettingsContent;
@@ -72,13 +72,13 @@ impl Setting for ItemSettings {
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
- _: &gpui::AppContext,
- ) -> anyhow::Result<Self> {
+ _: &mut AppContext,
+ ) -> Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}
-#[derive(Eq, PartialEq, Hash, Debug)]
+#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)]
pub enum ItemEvent {
CloseItem,
UpdateTab,
@@ -92,30 +92,38 @@ pub struct BreadcrumbText {
pub highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
}
-pub trait Item: View {
+pub trait Item: FocusableView + EventEmitter<Self::Event> {
+ type Event;
+
fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
fn workspace_deactivated(&mut self, _: &mut ViewContext<Self>) {}
fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
false
}
- fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
+ fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
None
}
- fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option<Cow<str>> {
+ fn tab_description(&self, _: usize, _: &AppContext) -> Option<SharedString> {
None
}
- fn tab_content<V: 'static>(
+ fn tab_content(&self, detail: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement;
+
+ /// (model id, Item)
+ fn for_each_project_item(
&self,
- detail: Option<usize>,
- style: &theme::Tab,
- cx: &AppContext,
- ) -> AnyElement<V>;
- fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project::Item)) {} // (model id, Item)
+ _: &AppContext,
+ _: &mut dyn FnMut(EntityId, &dyn project::Item),
+ ) {
+ }
fn is_singleton(&self, _cx: &AppContext) -> bool {
false
}
fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>) {}
- fn clone_on_split(&self, _workspace_id: WorkspaceId, _: &mut ViewContext<Self>) -> Option<Self>
+ fn clone_on_split(
+ &self,
+ _workspace_id: WorkspaceId,
+ _: &mut ViewContext<Self>,
+ ) -> Option<View<Self>>
where
Self: Sized,
{
@@ -130,16 +138,12 @@ pub trait Item: View {
fn can_save(&self, _cx: &AppContext) -> bool {
false
}
- fn save(
- &mut self,
- _project: ModelHandle<Project>,
- _cx: &mut ViewContext<Self>,
- ) -> Task<Result<()>> {
+ fn save(&mut self, _project: Model<Project>, _cx: &mut ViewContext<Self>) -> Task<Result<()>> {
unimplemented!("save() must be implemented if can_save() returns true")
}
fn save_as(
&mut self,
- _project: ModelHandle<Project>,
+ _project: Model<Project>,
_abs_path: PathBuf,
_cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
@@ -147,35 +151,28 @@ pub trait Item: View {
}
fn reload(
&mut self,
- _project: ModelHandle<Project>,
+ _project: Model<Project>,
_cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
unimplemented!("reload() must be implemented if can_save() returns true")
}
- fn to_item_events(_event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
- SmallVec::new()
- }
- fn should_close_item_on_event(_: &Self::Event) -> bool {
- false
- }
- fn should_update_tab_on_event(_: &Self::Event) -> bool {
- false
- }
+
+ fn to_item_events(event: &Self::Event, f: impl FnMut(ItemEvent));
fn act_as_type<'a>(
&'a self,
type_id: TypeId,
- self_handle: &'a ViewHandle<Self>,
+ self_handle: &'a View<Self>,
_: &'a AppContext,
- ) -> Option<&AnyViewHandle> {
+ ) -> Option<AnyView> {
if TypeId::of::<Self>() == type_id {
- Some(self_handle)
+ Some(self_handle.clone().into())
} else {
None
}
}
- fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+ fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
None
}
@@ -194,12 +191,12 @@ pub trait Item: View {
}
fn deserialize(
- _project: ModelHandle<Project>,
- _workspace: WeakViewHandle<Workspace>,
+ _project: Model<Project>,
+ _workspace: WeakView<Workspace>,
_workspace_id: WorkspaceId,
_item_id: ItemId,
_cx: &mut ViewContext<Pane>,
- ) -> Task<Result<ViewHandle<Self>>> {
+ ) -> Task<Result<View<Self>>> {
unimplemented!(
"deserialize() must be implemented if serialized_item_kind() returns Some(_)"
)
@@ -207,35 +204,30 @@ pub trait Item: View {
fn show_toolbar(&self) -> bool {
true
}
- fn pixel_position_of_cursor(&self, _: &AppContext) -> Option<Vector2F> {
+ fn pixel_position_of_cursor(&self, _: &AppContext) -> Option<Point<Pixels>> {
None
}
}
-pub trait ItemHandle: 'static + fmt::Debug {
+pub trait ItemHandle: 'static + Send {
fn subscribe_to_item_events(
&self,
cx: &mut WindowContext,
handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
) -> gpui::Subscription;
- fn tab_tooltip_text<'a>(&self, cx: &'a AppContext) -> Option<Cow<'a, str>>;
- fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>>;
- fn tab_content(
- &self,
- detail: Option<usize>,
- style: &theme::Tab,
- cx: &AppContext,
- ) -> AnyElement<Pane>;
- fn dragged_tab_content(
- &self,
- detail: Option<usize>,
- style: &theme::Tab,
- cx: &AppContext,
- ) -> AnyElement<Workspace>;
+ fn focus_handle(&self, cx: &WindowContext) -> FocusHandle;
+ fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString>;
+ fn tab_description(&self, detail: usize, cx: &AppContext) -> Option<SharedString>;
+ fn tab_content(&self, detail: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement;
+ fn dragged_tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement;
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
- fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[usize; 3]>;
- fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project::Item));
+ fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[EntityId; 3]>;
+ fn for_each_project_item(
+ &self,
+ _: &AppContext,
+ _: &mut dyn FnMut(EntityId, &dyn project::Item),
+ );
fn is_singleton(&self, cx: &AppContext) -> bool;
fn boxed_clone(&self) -> Box<dyn ItemHandle>;
fn clone_on_split(
@@ -246,95 +238,85 @@ pub trait ItemHandle: 'static + fmt::Debug {
fn added_to_pane(
&self,
workspace: &mut Workspace,
- pane: ViewHandle<Pane>,
+ pane: View<Pane>,
cx: &mut ViewContext<Workspace>,
);
fn deactivated(&self, cx: &mut WindowContext);
fn workspace_deactivated(&self, cx: &mut WindowContext);
fn navigate(&self, data: Box<dyn Any>, cx: &mut WindowContext) -> bool;
- fn id(&self) -> usize;
- fn window(&self) -> AnyWindowHandle;
- fn as_any(&self) -> &AnyViewHandle;
+ fn item_id(&self) -> EntityId;
+ fn to_any(&self) -> AnyView;
fn is_dirty(&self, cx: &AppContext) -> bool;
fn has_conflict(&self, cx: &AppContext) -> bool;
fn can_save(&self, cx: &AppContext) -> bool;
- fn save(&self, project: ModelHandle<Project>, cx: &mut WindowContext) -> Task<Result<()>>;
+ fn save(&self, project: Model<Project>, cx: &mut WindowContext) -> Task<Result<()>>;
fn save_as(
&self,
- project: ModelHandle<Project>,
+ project: Model<Project>,
abs_path: PathBuf,
cx: &mut WindowContext,
) -> Task<Result<()>>;
- fn reload(&self, project: ModelHandle<Project>, cx: &mut WindowContext) -> Task<Result<()>>;
- fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a AppContext) -> Option<&'a AnyViewHandle>;
+ fn reload(&self, project: Model<Project>, cx: &mut WindowContext) -> Task<Result<()>>;
+ fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyView>;
fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
fn on_release(
&self,
cx: &mut AppContext,
- callback: Box<dyn FnOnce(&mut AppContext)>,
+ callback: Box<dyn FnOnce(&mut AppContext) + Send>,
) -> gpui::Subscription;
fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation;
fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>>;
fn serialized_item_kind(&self) -> Option<&'static str>;
fn show_toolbar(&self, cx: &AppContext) -> bool;
- fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F>;
+ fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>>;
}
-pub trait WeakItemHandle {
- fn id(&self) -> usize;
- fn window(&self) -> AnyWindowHandle;
- fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>;
+pub trait WeakItemHandle: Send + Sync {
+ fn id(&self) -> EntityId;
+ fn upgrade(&self) -> Option<Box<dyn ItemHandle>>;
}
impl dyn ItemHandle {
- pub fn downcast<T: View>(&self) -> Option<ViewHandle<T>> {
- self.as_any().clone().downcast()
+ pub fn downcast<V: 'static>(&self) -> Option<View<V>> {
+ self.to_any().downcast().ok()
}
- pub fn act_as<T: View>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
- self.act_as_type(TypeId::of::<T>(), cx)
- .and_then(|t| t.clone().downcast())
+ pub fn act_as<V: 'static>(&self, cx: &AppContext) -> Option<View<V>> {
+ self.act_as_type(TypeId::of::<V>(), cx)
+ .and_then(|t| t.downcast().ok())
}
}
-impl<T: Item> ItemHandle for ViewHandle<T> {
+impl<T: Item> ItemHandle for View<T> {
fn subscribe_to_item_events(
&self,
cx: &mut WindowContext,
handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
) -> gpui::Subscription {
cx.subscribe(self, move |_, event, cx| {
- for item_event in T::to_item_events(event) {
- handler(item_event, cx)
- }
+ T::to_item_events(event, |item_event| handler(item_event, cx));
})
}
- fn tab_tooltip_text<'a>(&self, cx: &'a AppContext) -> Option<Cow<'a, str>> {
+ fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
+ self.focus_handle(cx)
+ }
+
+ fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
self.read(cx).tab_tooltip_text(cx)
}
- fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
+ fn tab_description(&self, detail: usize, cx: &AppContext) -> Option<SharedString> {
self.read(cx).tab_description(detail, cx)
}
- fn tab_content(
- &self,
- detail: Option<usize>,
- style: &theme::Tab,
- cx: &AppContext,
- ) -> AnyElement<Pane> {
- self.read(cx).tab_content(detail, style, cx)
+ fn tab_content(&self, detail: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement {
+ self.read(cx).tab_content(detail, selected, cx)
}
- fn dragged_tab_content(
- &self,
- detail: Option<usize>,
- style: &theme::Tab,
- cx: &AppContext,
- ) -> AnyElement<Workspace> {
- self.read(cx).tab_content(detail, style, cx)
+ fn dragged_tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement {
+ self.read(cx).tab_content(detail, true, cx)
}
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
@@ -358,7 +340,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
result
}
- fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[usize; 3]> {
+ fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[EntityId; 3]> {
let mut result = SmallVec::new();
self.read(cx).for_each_project_item(cx, &mut |id, _| {
result.push(id);
@@ -366,7 +348,11 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
result
}
- 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.read(cx).for_each_project_item(cx, f)
}
@@ -383,18 +369,17 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
workspace_id: WorkspaceId,
cx: &mut WindowContext,
) -> Option<Box<dyn ItemHandle>> {
- self.update(cx, |item, cx| {
- cx.add_option_view(|cx| item.clone_on_split(workspace_id, cx))
- })
- .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
+ self.update(cx, |item, cx| item.clone_on_split(workspace_id, cx))
+ .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
}
fn added_to_pane(
&self,
workspace: &mut Workspace,
- pane: ViewHandle<Pane>,
+ pane: View<Pane>,
cx: &mut ViewContext<Workspace>,
) {
+ let weak_item = self.downgrade();
let history = pane.read(cx).nav_history_for_item(self);
self.update(cx, |this, cx| {
this.set_nav_history(history, cx);
@@ -419,19 +404,19 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
if workspace
.panes_by_item
- .insert(self.id(), pane.downgrade())
+ .insert(self.item_id(), pane.downgrade())
.is_none()
{
let mut pending_autosave = DelayedDebouncedEditAction::new();
let pending_update = Rc::new(RefCell::new(None));
- let pending_update_scheduled = Rc::new(AtomicBool::new(false));
+ let pending_update_scheduled = Arc::new(AtomicBool::new(false));
let mut event_subscription =
Some(cx.subscribe(self, move |workspace, item, event, cx| {
let pane = if let Some(pane) = workspace
.panes_by_item
- .get(&item.id())
- .and_then(|pane| pane.upgrade(cx))
+ .get(&item.item_id())
+ .and_then(|pane| pane.upgrade())
{
pane
} else {
@@ -443,7 +428,10 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
let is_project_item = item.is_project_item(cx);
let leader_id = workspace.leader_for_pane(&pane);
- if leader_id.is_some() && item.should_unfollow_on_event(event, cx) {
+ let follow_event = item.to_follow_event(event);
+ if leader_id.is_some()
+ && matches!(follow_event, Some(FollowEvent::Unfollow))
+ {
workspace.unfollow(&pane, cx);
}
@@ -454,7 +442,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
) && !pending_update_scheduled.load(Ordering::SeqCst)
{
pending_update_scheduled.store(true, Ordering::SeqCst);
- cx.after_window_update({
+ cx.on_next_frame({
let pending_update = pending_update.clone();
let pending_update_scheduled = pending_update_scheduled.clone();
move |this, cx| {
@@ -477,51 +465,48 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
}
}
- for item_event in T::to_item_events(event).into_iter() {
- match item_event {
- ItemEvent::CloseItem => {
- pane.update(cx, |pane, cx| {
- pane.close_item_by_id(item.id(), crate::SaveIntent::Close, cx)
- })
- .detach_and_log_err(cx);
- return;
- }
+ T::to_item_events(event, |event| match event {
+ ItemEvent::CloseItem => {
+ pane.update(cx, |pane, cx| {
+ pane.close_item_by_id(item.item_id(), crate::SaveIntent::Close, cx)
+ })
+ .detach_and_log_err(cx);
+ return;
+ }
- ItemEvent::UpdateTab => {
- pane.update(cx, |_, cx| {
- cx.emit(pane::Event::ChangeItemTitle);
- cx.notify();
- });
- }
+ ItemEvent::UpdateTab => {
+ pane.update(cx, |_, cx| {
+ cx.emit(pane::Event::ChangeItemTitle);
+ cx.notify();
+ });
+ }
- ItemEvent::Edit => {
- let autosave = settings::get::<WorkspaceSettings>(cx).autosave;
- if let AutosaveSetting::AfterDelay { milliseconds } = autosave {
- let delay = Duration::from_millis(milliseconds);
- let item = item.clone();
- pending_autosave.fire_new(delay, cx, move |workspace, cx| {
- Pane::autosave_item(&item, workspace.project().clone(), cx)
- });
- }
+ ItemEvent::Edit => {
+ let autosave = WorkspaceSettings::get_global(cx).autosave;
+ if let AutosaveSetting::AfterDelay { milliseconds } = autosave {
+ let delay = Duration::from_millis(milliseconds);
+ let item = item.clone();
+ pending_autosave.fire_new(delay, cx, move |workspace, cx| {
+ Pane::autosave_item(&item, workspace.project().clone(), cx)
+ });
}
-
- _ => {}
}
- }
+
+ _ => {}
+ });
}));
- cx.observe_focus(self, move |workspace, item, focused, cx| {
- if !focused
- && settings::get::<WorkspaceSettings>(cx).autosave
- == AutosaveSetting::OnFocusChange
- {
- Pane::autosave_item(&item, workspace.project.clone(), cx)
- .detach_and_log_err(cx);
+ cx.on_blur(&self.focus_handle(cx), move |workspace, cx| {
+ if WorkspaceSettings::get_global(cx).autosave == AutosaveSetting::OnFocusChange {
+ if let Some(item) = weak_item.upgrade() {
+ Pane::autosave_item(&item, workspace.project.clone(), cx)
+ .detach_and_log_err(cx);
+ }
}
})
.detach();
- let item_id = self.id();
+ let item_id = self.item_id();
cx.observe_release(self, move |workspace, _, _| {
workspace.panes_by_item.remove(&item_id);
event_subscription.take();
@@ -546,16 +531,12 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
self.update(cx, |this, cx| this.navigate(data, cx))
}
- fn id(&self) -> usize {
- self.id()
+ fn item_id(&self) -> EntityId {
+ self.entity_id()
}
- fn window(&self) -> AnyWindowHandle {
- AnyViewHandle::window(self)
- }
-
- fn as_any(&self) -> &AnyViewHandle {
- self
+ fn to_any(&self) -> AnyView {
+ self.clone().into()
}
fn is_dirty(&self, cx: &AppContext) -> bool {
@@ -570,32 +551,32 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
self.read(cx).can_save(cx)
}
- fn save(&self, project: ModelHandle<Project>, cx: &mut WindowContext) -> Task<Result<()>> {
+ fn save(&self, project: Model<Project>, cx: &mut WindowContext) -> Task<Result<()>> {
self.update(cx, |item, cx| item.save(project, cx))
}
fn save_as(
&self,
- project: ModelHandle<Project>,
+ project: Model<Project>,
abs_path: PathBuf,
cx: &mut WindowContext,
) -> Task<anyhow::Result<()>> {
self.update(cx, |item, cx| item.save_as(project, abs_path, cx))
}
- fn reload(&self, project: ModelHandle<Project>, cx: &mut WindowContext) -> Task<Result<()>> {
+ fn reload(&self, project: Model<Project>, cx: &mut WindowContext) -> Task<Result<()>> {
self.update(cx, |item, cx| item.reload(project, cx))
}
- fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a AppContext) -> Option<&'a AnyViewHandle> {
+ fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a AppContext) -> Option<AnyView> {
self.read(cx).act_as_type(type_id, self, cx)
}
fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>> {
if cx.has_global::<FollowableItemBuilders>() {
let builders = cx.global::<FollowableItemBuilders>();
- let item = self.as_any();
- Some(builders.get(&item.view_type())?.1(item))
+ let item = self.to_any();
+ Some(builders.get(&item.entity_type())?.1(&item))
} else {
None
}
@@ -604,7 +585,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
fn on_release(
&self,
cx: &mut AppContext,
- callback: Box<dyn FnOnce(&mut AppContext)>,
+ callback: Box<dyn FnOnce(&mut AppContext) + Send>,
) -> gpui::Subscription {
cx.observe_release(self, move |_, cx| callback(cx))
}
@@ -629,20 +610,20 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
self.read(cx).show_toolbar()
}
- fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
+ fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
self.read(cx).pixel_position_of_cursor(cx)
}
}
-impl From<Box<dyn ItemHandle>> for AnyViewHandle {
+impl From<Box<dyn ItemHandle>> for AnyView {
fn from(val: Box<dyn ItemHandle>) -> Self {
- val.as_any().clone()
+ val.to_any()
}
}
-impl From<&Box<dyn ItemHandle>> for AnyViewHandle {
+impl From<&Box<dyn ItemHandle>> for AnyView {
fn from(val: &Box<dyn ItemHandle>) -> Self {
- val.as_any().clone()
+ val.to_any()
}
}
@@ -652,84 +633,85 @@ impl Clone for Box<dyn ItemHandle> {
}
}
-impl<T: Item> WeakItemHandle for WeakViewHandle<T> {
- fn id(&self) -> usize {
- self.id()
- }
-
- fn window(&self) -> AnyWindowHandle {
- self.window()
+impl<T: Item> WeakItemHandle for WeakView<T> {
+ fn id(&self) -> EntityId {
+ self.entity_id()
}
- fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
- self.upgrade(cx).map(|v| Box::new(v) as Box<dyn ItemHandle>)
+ fn upgrade(&self) -> Option<Box<dyn ItemHandle>> {
+ self.upgrade().map(|v| Box::new(v) as Box<dyn ItemHandle>)
}
}
pub trait ProjectItem: Item {
- type Item: project::Item + gpui::Entity;
+ type Item: project::Item;
fn for_project_item(
- project: ModelHandle<Project>,
- item: ModelHandle<Self::Item>,
+ project: Model<Project>,
+ item: Model<Self::Item>,
cx: &mut ViewContext<Self>,
- ) -> Self;
+ ) -> Self
+ where
+ Self: Sized;
+}
+
+pub enum FollowEvent {
+ Unfollow,
}
pub trait FollowableItem: Item {
fn remote_id(&self) -> Option<ViewId>;
- fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
+ fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant>;
fn from_state_proto(
- pane: ViewHandle<Pane>,
- project: ViewHandle<Workspace>,
+ pane: View<Pane>,
+ project: View<Workspace>,
id: ViewId,
state: &mut Option<proto::view::Variant>,
- cx: &mut AppContext,
- ) -> Option<Task<Result<ViewHandle<Self>>>>;
+ cx: &mut WindowContext,
+ ) -> Option<Task<Result<View<Self>>>>;
+ fn to_follow_event(event: &Self::Event) -> Option<FollowEvent>;
fn add_event_to_update_proto(
&self,
event: &Self::Event,
update: &mut Option<proto::update_view::Variant>,
- cx: &AppContext,
+ cx: &WindowContext,
) -> bool;
fn apply_update_proto(
&mut self,
- project: &ModelHandle<Project>,
+ project: &Model<Project>,
message: proto::update_view::Variant,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>>;
- fn is_project_item(&self, cx: &AppContext) -> bool;
-
+ fn is_project_item(&self, cx: &WindowContext) -> bool;
fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>);
- fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool;
}
pub trait FollowableItemHandle: ItemHandle {
- fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId>;
+ fn remote_id(&self, client: &Arc<Client>, cx: &WindowContext) -> Option<ViewId>;
fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext);
- fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
+ fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant>;
fn add_event_to_update_proto(
&self,
event: &dyn Any,
update: &mut Option<proto::update_view::Variant>,
- cx: &AppContext,
+ cx: &WindowContext,
) -> bool;
+ fn to_follow_event(&self, event: &dyn Any) -> Option<FollowEvent>;
fn apply_update_proto(
&self,
- project: &ModelHandle<Project>,
+ project: &Model<Project>,
message: proto::update_view::Variant,
cx: &mut WindowContext,
) -> Task<Result<()>>;
- fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool;
- fn is_project_item(&self, cx: &AppContext) -> bool;
+ fn is_project_item(&self, cx: &WindowContext) -> bool;
}
-impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
- fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId> {
+impl<T: FollowableItem> FollowableItemHandle for View<T> {
+ fn remote_id(&self, client: &Arc<Client>, cx: &WindowContext) -> Option<ViewId> {
self.read(cx).remote_id().or_else(|| {
client.peer_id().map(|creator| ViewId {
creator,
- id: self.id() as u64,
+ id: self.item_id().as_u64(),
})
})
}
@@ -738,7 +720,7 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
self.update(cx, |this, cx| this.set_leader_peer_id(leader_peer_id, cx))
}
- fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
+ fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
self.read(cx).to_state_proto(cx)
}
@@ -746,7 +728,7 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
&self,
event: &dyn Any,
update: &mut Option<proto::update_view::Variant>,
- cx: &AppContext,
+ cx: &WindowContext,
) -> bool {
if let Some(event) = event.downcast_ref() {
self.read(cx).add_event_to_update_proto(event, update, cx)
@@ -755,24 +737,20 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
}
}
+ fn to_follow_event(&self, event: &dyn Any) -> Option<FollowEvent> {
+ T::to_follow_event(event.downcast_ref()?)
+ }
+
fn apply_update_proto(
&self,
- project: &ModelHandle<Project>,
+ project: &Model<Project>,
message: proto::update_view::Variant,
cx: &mut WindowContext,
) -> Task<Result<()>> {
self.update(cx, |this, cx| this.apply_update_proto(project, message, cx))
}
- fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool {
- if let Some(event) = event.downcast_ref() {
- T::should_unfollow_on_event(event, cx)
- } else {
- false
- }
- }
-
- fn is_project_item(&self, cx: &AppContext) -> bool {
+ fn is_project_item(&self, cx: &WindowContext) -> bool {
self.read(cx).is_project_item(cx)
}
}
@@ -782,12 +760,12 @@ pub mod test {
use super::{Item, ItemEvent};
use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId};
use gpui::{
- elements::Empty, AnyElement, AppContext, Element, Entity, ModelHandle, Task, View,
- ViewContext, ViewHandle, WeakViewHandle,
+ AnyElement, AppContext, Context as _, EntityId, EventEmitter, FocusableView,
+ InteractiveElement, IntoElement, Model, Render, SharedString, Task, View, ViewContext,
+ VisualContext, WeakView,
};
use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
- use smallvec::SmallVec;
- use std::{any::Any, borrow::Cow, cell::Cell, path::Path};
+ use std::{any::Any, cell::Cell, path::Path};
pub struct TestProjectItem {
pub entry_id: Option<ProjectEntryId>,
@@ -804,14 +782,11 @@ pub mod test {
pub is_dirty: bool,
pub is_singleton: bool,
pub has_conflict: bool,
- pub project_items: Vec<ModelHandle<TestProjectItem>>,
+ pub project_items: Vec<Model<TestProjectItem>>,
pub nav_history: Option<ItemNavHistory>,
pub tab_descriptions: Option<Vec<&'static str>>,
pub tab_detail: Cell<Option<usize>>,
- }
-
- impl Entity for TestProjectItem {
- type Event = ();
+ focus_handle: gpui::FocusHandle,
}
impl project::Item for TestProjectItem {
@@ -828,41 +803,42 @@ pub mod test {
Edit,
}
- impl Clone for TestItem {
- fn clone(&self) -> Self {
- Self {
- state: self.state.clone(),
- label: self.label.clone(),
- save_count: self.save_count,
- save_as_count: self.save_as_count,
- reload_count: self.reload_count,
- is_dirty: self.is_dirty,
- is_singleton: self.is_singleton,
- has_conflict: self.has_conflict,
- project_items: self.project_items.clone(),
- nav_history: None,
- tab_descriptions: None,
- tab_detail: Default::default(),
- workspace_id: self.workspace_id,
- }
- }
- }
+ // impl Clone for TestItem {
+ // fn clone(&self) -> Self {
+ // Self {
+ // state: self.state.clone(),
+ // label: self.label.clone(),
+ // save_count: self.save_count,
+ // save_as_count: self.save_as_count,
+ // reload_count: self.reload_count,
+ // is_dirty: self.is_dirty,
+ // is_singleton: self.is_singleton,
+ // has_conflict: self.has_conflict,
+ // project_items: self.project_items.clone(),
+ // nav_history: None,
+ // tab_descriptions: None,
+ // tab_detail: Default::default(),
+ // workspace_id: self.workspace_id,
+ // focus_handle: self.focus_handle.clone(),
+ // }
+ // }
+ // }
impl TestProjectItem {
- pub fn new(id: u64, path: &str, cx: &mut AppContext) -> ModelHandle<Self> {
+ pub fn new(id: u64, path: &str, cx: &mut AppContext) -> Model<Self> {
let entry_id = Some(ProjectEntryId::from_proto(id));
let project_path = Some(ProjectPath {
worktree_id: WorktreeId::from_usize(0),
path: Path::new(path).into(),
});
- cx.add_model(|_| Self {
+ cx.new_model(|_| Self {
entry_id,
project_path,
})
}
- pub fn new_untitled(cx: &mut AppContext) -> ModelHandle<Self> {
- cx.add_model(|_| Self {
+ pub fn new_untitled(cx: &mut AppContext) -> Model<Self> {
+ cx.new_model(|_| Self {
project_path: None,
entry_id: None,
})
@@ -870,7 +846,7 @@ pub mod test {
}
impl TestItem {
- pub fn new() -> Self {
+ pub fn new(cx: &mut ViewContext<Self>) -> Self {
Self {
state: String::new(),
label: String::new(),
@@ -885,11 +861,12 @@ pub mod test {
tab_descriptions: None,
tab_detail: Default::default(),
workspace_id: 0,
+ focus_handle: cx.focus_handle(),
}
}
- pub fn new_deserialized(id: WorkspaceId) -> Self {
- let mut this = Self::new();
+ pub fn new_deserialized(id: WorkspaceId, cx: &mut ViewContext<Self>) -> Self {
+ let mut this = Self::new(cx);
this.workspace_id = id;
this
}
@@ -914,7 +891,7 @@ pub mod test {
self
}
- pub fn with_project_items(mut self, items: &[ModelHandle<TestProjectItem>]) -> Self {
+ pub fn with_project_items(mut self, items: &[Model<TestProjectItem>]) -> Self {
self.project_items.clear();
self.project_items.extend(items.iter().cloned());
self
@@ -932,46 +909,52 @@ pub mod test {
}
}
- impl Entity for TestItem {
- type Event = TestItemEvent;
+ impl Render for TestItem {
+ fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
+ gpui::div().track_focus(&self.focus_handle)
+ }
}
- impl View for TestItem {
- fn ui_name() -> &'static str {
- "TestItem"
- }
+ impl EventEmitter<ItemEvent> for TestItem {}
- fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
- Empty::new().into_any()
+ impl FocusableView for TestItem {
+ fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
+ self.focus_handle.clone()
}
}
impl Item for TestItem {
- fn tab_description(&self, detail: usize, _: &AppContext) -> Option<Cow<str>> {
+ type Event = ItemEvent;
+
+ fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
+ f(*event)
+ }
+
+ fn tab_description(&self, detail: usize, _: &AppContext) -> Option<SharedString> {
self.tab_descriptions.as_ref().and_then(|descriptions| {
let description = *descriptions.get(detail).or_else(|| descriptions.last())?;
Some(description.into())
})
}
- fn tab_content<V: 'static>(
+ fn tab_content(
&self,
detail: Option<usize>,
- _: &theme::Tab,
- _: &AppContext,
- ) -> AnyElement<V> {
+ _selected: bool,
+ _cx: &ui::prelude::WindowContext,
+ ) -> AnyElement {
self.tab_detail.set(detail);
- Empty::new().into_any()
+ gpui::div().into_any_element()
}
fn for_each_project_item(
&self,
cx: &AppContext,
- f: &mut dyn FnMut(usize, &dyn project::Item),
+ f: &mut dyn FnMut(EntityId, &dyn project::Item),
) {
self.project_items
.iter()
- .for_each(|item| f(item.id(), item.read(cx)))
+ .for_each(|item| f(item.entity_id(), item.read(cx)))
}
fn is_singleton(&self, _: &AppContext) -> bool {
@@ -1,35 +1,39 @@
use crate::{Toast, Workspace};
use collections::HashMap;
-use gpui::{AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle};
+use gpui::{
+ AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, Render,
+ View, ViewContext, VisualContext,
+};
use std::{any::TypeId, ops::DerefMut};
pub fn init(cx: &mut AppContext) {
cx.set_global(NotificationTracker::new());
- simple_message_notification::init(cx);
+ // todo!()
+ // simple_message_notification::init(cx);
}
-pub trait Notification: View {
- fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool;
-}
+pub trait Notification: EventEmitter<DismissEvent> + Render {}
+
+impl<V: EventEmitter<DismissEvent> + Render> Notification for V {}
-pub trait NotificationHandle {
- fn id(&self) -> usize;
- fn as_any(&self) -> &AnyViewHandle;
+pub trait NotificationHandle: Send {
+ fn id(&self) -> EntityId;
+ fn to_any(&self) -> AnyView;
}
-impl<T: Notification> NotificationHandle for ViewHandle<T> {
- fn id(&self) -> usize {
- self.id()
+impl<T: Notification> NotificationHandle for View<T> {
+ fn id(&self) -> EntityId {
+ self.entity_id()
}
- fn as_any(&self) -> &AnyViewHandle {
- self
+ fn to_any(&self) -> AnyView {
+ self.clone().into()
}
}
-impl From<&dyn NotificationHandle> for AnyViewHandle {
+impl From<&dyn NotificationHandle> for AnyView {
fn from(val: &dyn NotificationHandle) -> Self {
- val.as_any().clone()
+ val.to_any()
}
}
@@ -75,14 +79,12 @@ impl Workspace {
&mut self,
id: usize,
cx: &mut ViewContext<Self>,
- build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
+ build_notification: impl FnOnce(&mut ViewContext<Self>) -> View<V>,
) {
if !self.has_shown_notification_once::<V>(id, cx) {
- cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
- let entry = tracker.entry(TypeId::of::<V>()).or_default();
- entry.push(id);
- });
-
+ let tracker = cx.global_mut::<NotificationTracker>();
+ let entry = tracker.entry(TypeId::of::<V>()).or_default();
+ entry.push(id);
self.show_notification::<V>(id, cx, build_notification)
}
}
@@ -91,7 +93,7 @@ impl Workspace {
&mut self,
id: usize,
cx: &mut ViewContext<Self>,
- build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
+ build_notification: impl FnOnce(&mut ViewContext<Self>) -> View<V>,
) {
let type_id = TypeId::of::<V>();
if self
@@ -102,10 +104,8 @@ impl Workspace {
})
{
let notification = build_notification(cx);
- cx.subscribe(¬ification, move |this, handle, event, cx| {
- if handle.read(cx).should_dismiss_notification_on_event(event) {
- this.dismiss_notification_internal(type_id, id, cx);
- }
+ cx.subscribe(¬ification, move |this, _, _: &DismissEvent, cx| {
+ this.dismiss_notification_internal(type_id, id, cx);
})
.detach();
self.notifications
@@ -114,6 +114,17 @@ impl Workspace {
}
}
+ pub fn show_error<E>(&mut self, err: &E, cx: &mut ViewContext<Self>)
+ where
+ E: std::fmt::Debug,
+ {
+ self.show_notification(0, cx, |cx| {
+ cx.new_view(|_cx| {
+ simple_message_notification::MessageNotification::new(format!("Error: {err:?}"))
+ })
+ });
+ }
+
pub fn dismiss_notification<V: Notification>(&mut self, id: usize, cx: &mut ViewContext<Self>) {
let type_id = TypeId::of::<V>();
@@ -123,7 +134,7 @@ impl Workspace {
pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
self.dismiss_notification::<simple_message_notification::MessageNotification>(toast.id, cx);
self.show_notification(toast.id, cx, |cx| {
- cx.add_view(|_cx| match toast.on_click.as_ref() {
+ cx.new_view(|_cx| match toast.on_click.as_ref() {
Some((click_msg, on_click)) => {
let on_click = on_click.clone();
simple_message_notification::MessageNotification::new(toast.msg.clone())
@@ -158,78 +169,29 @@ impl Workspace {
}
pub mod simple_message_notification {
- use super::Notification;
- use crate::Workspace;
use gpui::{
- actions,
- elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
- fonts::TextStyle,
- impl_actions,
- platform::{CursorStyle, MouseButton},
- AnyElement, AppContext, Element, Entity, View, ViewContext,
+ div, DismissEvent, EventEmitter, InteractiveElement, ParentElement, Render, SharedString,
+ StatefulInteractiveElement, Styled, ViewContext,
};
- use menu::Cancel;
- use serde::Deserialize;
- use std::{borrow::Cow, sync::Arc};
-
- actions!(message_notifications, [CancelMessageNotification]);
-
- #[derive(Clone, Default, Deserialize, PartialEq)]
- pub struct OsOpen(pub Cow<'static, str>);
-
- impl OsOpen {
- pub fn new<I: Into<Cow<'static, str>>>(url: I) -> Self {
- OsOpen(url.into())
- }
- }
-
- impl_actions!(message_notifications, [OsOpen]);
-
- pub fn init(cx: &mut AppContext) {
- cx.add_action(MessageNotification::dismiss);
- cx.add_action(
- |_workspace: &mut Workspace, open_action: &OsOpen, cx: &mut ViewContext<Workspace>| {
- cx.platform().open_url(open_action.0.as_ref());
- },
- )
- }
-
- enum NotificationMessage {
- Text(Cow<'static, str>),
- Element(fn(TextStyle, &AppContext) -> AnyElement<MessageNotification>),
- }
+ use std::sync::Arc;
+ use ui::prelude::*;
+ use ui::{h_stack, v_stack, Button, Icon, IconElement, Label, StyledExt};
pub struct MessageNotification {
- message: NotificationMessage,
+ message: SharedString,
on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
- click_message: Option<Cow<'static, str>>,
+ click_message: Option<SharedString>,
}
- pub enum MessageNotificationEvent {
- Dismiss,
- }
-
- impl Entity for MessageNotification {
- type Event = MessageNotificationEvent;
- }
+ impl EventEmitter<DismissEvent> for MessageNotification {}
impl MessageNotification {
pub fn new<S>(message: S) -> MessageNotification
where
- S: Into<Cow<'static, str>>,
+ S: Into<SharedString>,
{
Self {
- message: NotificationMessage::Text(message.into()),
- on_click: None,
- click_message: None,
- }
- }
-
- pub fn new_element(
- message: fn(TextStyle, &AppContext) -> AnyElement<MessageNotification>,
- ) -> MessageNotification {
- Self {
- message: NotificationMessage::Element(message),
+ message: message.into(),
on_click: None,
click_message: None,
}
@@ -237,7 +199,7 @@ pub mod simple_message_notification {
pub fn with_click_message<S>(mut self, message: S) -> Self
where
- S: Into<Cow<'static, str>>,
+ S: Into<SharedString>,
{
self.click_message = Some(message.into());
self
@@ -251,117 +213,139 @@ pub mod simple_message_notification {
self
}
- pub fn dismiss(&mut self, _: &CancelMessageNotification, cx: &mut ViewContext<Self>) {
- cx.emit(MessageNotificationEvent::Dismiss);
+ pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+ cx.emit(DismissEvent);
}
}
- impl View for MessageNotification {
- fn ui_name() -> &'static str {
- "MessageNotification"
- }
-
- fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
- let theme = theme::current(cx).clone();
- let theme = &theme.simple_message_notification;
-
- enum MessageNotificationTag {}
-
- let click_message = self.click_message.clone();
- let message = match &self.message {
- NotificationMessage::Text(text) => {
- Text::new(text.to_owned(), theme.message.text.clone()).into_any()
- }
- NotificationMessage::Element(e) => e(theme.message.text.clone(), cx),
- };
- let on_click = self.on_click.clone();
- let has_click_action = on_click.is_some();
-
- Flex::column()
- .with_child(
- Flex::row()
- .with_child(
- message
- .contained()
- .with_style(theme.message.container)
- .aligned()
- .top()
- .left()
- .flex(1., true),
- )
- .with_child(
- MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| {
- let style = theme.dismiss_button.style_for(state);
- Svg::new("icons/x.svg")
- .with_color(style.color)
- .constrained()
- .with_width(style.icon_width)
- .aligned()
- .contained()
- .with_style(style.container)
- .constrained()
- .with_width(style.button_width)
- .with_height(style.button_width)
- })
- .with_padding(Padding::uniform(5.))
- .on_click(MouseButton::Left, move |_, this, cx| {
- this.dismiss(&Default::default(), cx);
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .aligned()
- .constrained()
- .with_height(cx.font_cache().line_height(theme.message.text.font_size))
- .aligned()
- .top()
- .flex_float(),
+ impl Render for MessageNotification {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ v_stack()
+ .elevation_3(cx)
+ .p_4()
+ .child(
+ h_stack()
+ .justify_between()
+ .child(div().max_w_80().child(Label::new(self.message.clone())))
+ .child(
+ div()
+ .id("cancel")
+ .child(IconElement::new(Icon::Close))
+ .cursor_pointer()
+ .on_click(cx.listener(|this, _, cx| this.dismiss(cx))),
),
)
- .with_children({
- click_message
- .map(|click_message| {
- MouseEventHandler::new::<MessageNotificationTag, _>(
- 0,
- cx,
- |state, _| {
- let style = theme.action_message.style_for(state);
-
- Flex::row()
- .with_child(
- Text::new(click_message, style.text.clone())
- .contained()
- .with_style(style.container),
- )
- .contained()
- },
- )
- .on_click(MouseButton::Left, move |_, this, cx| {
- if let Some(on_click) = on_click.as_ref() {
- on_click(cx);
- this.dismiss(&Default::default(), cx);
- }
- })
- // Since we're not using a proper overlay, we have to capture these extra events
- .on_down(MouseButton::Left, |_, _, _| {})
- .on_up(MouseButton::Left, |_, _, _| {})
- .with_cursor_style(if has_click_action {
- CursorStyle::PointingHand
- } else {
- CursorStyle::Arrow
- })
- })
- .into_iter()
- })
- .into_any()
- }
- }
-
- impl Notification for MessageNotification {
- fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
- match event {
- MessageNotificationEvent::Dismiss => true,
- }
+ .children(self.click_message.iter().map(|message| {
+ Button::new(message.clone(), message.clone()).on_click(cx.listener(
+ |this, _, cx| {
+ if let Some(on_click) = this.on_click.as_ref() {
+ (on_click)(cx)
+ };
+ this.dismiss(cx)
+ },
+ ))
+ }))
}
}
+ // todo!()
+ // impl View for MessageNotification {
+ // fn ui_name() -> &'static str {
+ // "MessageNotification"
+ // }
+
+ // fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
+ // let theme = theme2::current(cx).clone();
+ // let theme = &theme.simple_message_notification;
+
+ // enum MessageNotificationTag {}
+
+ // let click_message = self.click_message.clone();
+ // let message = match &self.message {
+ // NotificationMessage::Text(text) => {
+ // Text::new(text.to_owned(), theme.message.text.clone()).into_any()
+ // }
+ // NotificationMessage::Element(e) => e(theme.message.text.clone(), cx),
+ // };
+ // let on_click = self.on_click.clone();
+ // let has_click_action = on_click.is_some();
+
+ // Flex::column()
+ // .with_child(
+ // Flex::row()
+ // .with_child(
+ // message
+ // .contained()
+ // .with_style(theme.message.container)
+ // .aligned()
+ // .top()
+ // .left()
+ // .flex(1., true),
+ // )
+ // .with_child(
+ // MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| {
+ // let style = theme.dismiss_button.style_for(state);
+ // Svg::new("icons/x.svg")
+ // .with_color(style.color)
+ // .constrained()
+ // .with_width(style.icon_width)
+ // .aligned()
+ // .contained()
+ // .with_style(style.container)
+ // .constrained()
+ // .with_width(style.button_width)
+ // .with_height(style.button_width)
+ // })
+ // .with_padding(Padding::uniform(5.))
+ // .on_click(MouseButton::Left, move |_, this, cx| {
+ // this.dismiss(&Default::default(), cx);
+ // })
+ // .with_cursor_style(CursorStyle::PointingHand)
+ // .aligned()
+ // .constrained()
+ // .with_height(cx.font_cache().line_height(theme.message.text.font_size))
+ // .aligned()
+ // .top()
+ // .flex_float(),
+ // ),
+ // )
+ // .with_children({
+ // click_message
+ // .map(|click_message| {
+ // MouseEventHandler::new::<MessageNotificationTag, _>(
+ // 0,
+ // cx,
+ // |state, _| {
+ // let style = theme.action_message.style_for(state);
+
+ // Flex::row()
+ // .with_child(
+ // Text::new(click_message, style.text.clone())
+ // .contained()
+ // .with_style(style.container),
+ // )
+ // .contained()
+ // },
+ // )
+ // .on_click(MouseButton::Left, move |_, this, cx| {
+ // if let Some(on_click) = on_click.as_ref() {
+ // on_click(cx);
+ // this.dismiss(&Default::default(), cx);
+ // }
+ // })
+ // // Since we're not using a proper overlay, we have to capture these extra events
+ // .on_down(MouseButton::Left, |_, _, _| {})
+ // .on_up(MouseButton::Left, |_, _, _| {})
+ // .with_cursor_style(if has_click_action {
+ // CursorStyle::PointingHand
+ // } else {
+ // CursorStyle::Arrow
+ // })
+ // })
+ // .into_iter()
+ // })
+ // .into_any()
+ // }
+ // }
}
pub trait NotifyResultExt {
@@ -372,6 +356,8 @@ pub trait NotifyResultExt {
workspace: &mut Workspace,
cx: &mut ViewContext<Workspace>,
) -> Option<Self::Ok>;
+
+ fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<Self::Ok>;
}
impl<T, E> NotifyResultExt for Result<T, E>
@@ -384,15 +370,24 @@ where
match self {
Ok(value) => Some(value),
Err(err) => {
- workspace.show_notification(0, cx, |cx| {
- cx.add_view(|_cx| {
- simple_message_notification::MessageNotification::new(format!(
- "Error: {:?}",
- err,
- ))
- })
- });
+ log::error!("TODO {err:?}");
+ workspace.show_error(&err, cx);
+ None
+ }
+ }
+ }
+ fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<T> {
+ match self {
+ Ok(value) => Some(value),
+ Err(err) => {
+ log::error!("TODO {err:?}");
+ cx.update(|view, cx| {
+ if let Ok(workspace) = view.downcast::<Workspace>() {
+ workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
+ }
+ })
+ .ok();
None
}
}
@@ -1,39 +1,24 @@
-mod dragged_item_receiver;
-
-use super::{ItemHandle, SplitDirection};
-pub use crate::toolbar::Toolbar;
use crate::{
- item::{ItemSettings, WeakItemHandle},
- notify_of_new_dock, AutosaveSetting, Item, NewCenterTerminal, NewFile, NewSearch, ToggleZoom,
- Workspace, WorkspaceSettings,
+ item::{ClosePosition, Item, ItemHandle, ItemSettings, WeakItemHandle},
+ toolbar::Toolbar,
+ workspace_settings::{AutosaveSetting, WorkspaceSettings},
+ NewCenterTerminal, NewFile, NewSearch, SplitDirection, ToggleZoom, Workspace,
};
use anyhow::Result;
use collections::{HashMap, HashSet, VecDeque};
-use context_menu::{ContextMenu, ContextMenuItem};
-use drag_and_drop::{DragAndDrop, Draggable};
-use dragged_item_receiver::dragged_item_receiver;
-use fs::repository::GitFileStatus;
-use futures::StreamExt;
use gpui::{
- actions,
- elements::*,
- geometry::{
- rect::RectF,
- vector::{vec2f, Vector2F},
- },
- impl_actions,
- keymap_matcher::KeymapContext,
- platform::{CursorStyle, MouseButton, NavigationDirection, PromptLevel},
- Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
- ModelHandle, MouseRegion, Quad, Task, View, ViewContext, ViewHandle, WeakViewHandle,
- WindowContext,
+ actions, impl_actions, overlay, prelude::*, Action, AnchorCorner, AnyElement, AppContext,
+ AsyncWindowContext, DismissEvent, Div, DragMoveEvent, EntityId, EventEmitter, FocusHandle,
+ FocusableView, Model, MouseButton, NavigationDirection, Pixels, Point, PromptLevel, Render,
+ ScrollHandle, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
};
+use parking_lot::Mutex;
use project::{Project, ProjectEntryId, ProjectPath};
use serde::Deserialize;
+use settings::Settings;
use std::{
any::Any,
- cell::RefCell,
- cmp, mem,
+ cmp, fmt, mem,
path::{Path, PathBuf},
rc::Rc,
sync::{
@@ -41,8 +26,14 @@ use std::{
Arc,
},
};
-use theme::{Theme, ThemeSettings};
-use util::truncate_and_remove_front;
+use theme::ThemeSettings;
+
+use ui::{
+ prelude::*, right_click_menu, ButtonSize, Color, Icon, IconButton, IconSize, Indicator, Label,
+ Tab, TabBar, TabPosition, Tooltip,
+};
+use ui::{v_stack, ContextMenu};
+use util::{maybe, truncate_and_remove_front, ResultExt};
#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
@@ -63,26 +54,26 @@ pub enum SaveIntent {
Skip,
}
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, PartialEq, Debug)]
pub struct ActivateItem(pub usize);
-#[derive(Clone, PartialEq)]
-pub struct CloseItemById {
- pub item_id: usize,
- pub pane: WeakViewHandle<Pane>,
-}
+// #[derive(Clone, PartialEq)]
+// pub struct CloseItemById {
+// pub item_id: usize,
+// pub pane: WeakView<Pane>,
+// }
-#[derive(Clone, PartialEq)]
-pub struct CloseItemsToTheLeftById {
- pub item_id: usize,
- pub pane: WeakViewHandle<Pane>,
-}
+// #[derive(Clone, PartialEq)]
+// pub struct CloseItemsToTheLeftById {
+// pub item_id: usize,
+// pub pane: WeakView<Pane>,
+// }
-#[derive(Clone, PartialEq)]
-pub struct CloseItemsToTheRightById {
- pub item_id: usize,
- pub pane: WeakViewHandle<Pane>,
-}
+// #[derive(Clone, PartialEq)]
+// pub struct CloseItemsToTheRightById {
+// pub item_id: usize,
+// pub pane: WeakView<Pane>,
+// }
#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
@@ -90,7 +81,7 @@ pub struct CloseActiveItem {
pub save_intent: Option<SaveIntent>,
}
-#[derive(Clone, PartialEq, Debug, Deserialize)]
+#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CloseAllItems {
pub save_intent: Option<SaveIntent>,
@@ -102,6 +93,16 @@ pub struct RevealInProjectPanel {
pub entry_id: u64,
}
+impl_actions!(
+ pane,
+ [
+ CloseAllItems,
+ CloseActiveItem,
+ ActivateItem,
+ RevealInProjectPanel
+ ]
+);
+
actions!(
pane,
[
@@ -122,59 +123,13 @@ actions!(
]
);
-impl_actions!(
- pane,
- [
- ActivateItem,
- CloseActiveItem,
- CloseAllItems,
- RevealInProjectPanel,
- ]
-);
-
const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
-pub type BackgroundActions = fn() -> &'static [(&'static str, &'static dyn Action)];
-
-pub fn init(cx: &mut AppContext) {
- cx.add_action(Pane::toggle_zoom);
- cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
- pane.activate_item(action.0, true, true, cx);
- });
- cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| {
- pane.activate_item(pane.items.len() - 1, true, true, cx);
- });
- cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
- pane.activate_prev_item(true, cx);
- });
- cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
- pane.activate_next_item(true, cx);
- });
- cx.add_async_action(Pane::close_active_item);
- cx.add_async_action(Pane::close_inactive_items);
- cx.add_async_action(Pane::close_clean_items);
- cx.add_async_action(Pane::close_items_to_the_left);
- cx.add_async_action(Pane::close_items_to_the_right);
- cx.add_async_action(Pane::close_all_items);
- cx.add_action(|pane: &mut Pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx));
- cx.add_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx));
- cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx));
- cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx));
- cx.add_action(|pane: &mut Pane, action: &RevealInProjectPanel, cx| {
- pane.project.update(cx, |_, cx| {
- cx.emit(project::Event::RevealInProjectPanel(
- ProjectEntryId::from_proto(action.entry_id),
- ))
- })
- });
-}
-
-#[derive(Debug)]
pub enum Event {
AddItem { item: Box<dyn ItemHandle> },
ActivateItem { local: bool },
Remove,
- RemoveItem { item_id: usize },
+ RemoveItem { item_id: EntityId },
Split(SplitDirection),
ChangeItemTitle,
Focus,
@@ -182,41 +137,73 @@ pub enum Event {
ZoomOut,
}
+impl fmt::Debug for Event {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Event::AddItem { item } => f
+ .debug_struct("AddItem")
+ .field("item", &item.item_id())
+ .finish(),
+ Event::ActivateItem { local } => f
+ .debug_struct("ActivateItem")
+ .field("local", local)
+ .finish(),
+ Event::Remove => f.write_str("Remove"),
+ Event::RemoveItem { item_id } => f
+ .debug_struct("RemoveItem")
+ .field("item_id", item_id)
+ .finish(),
+ Event::Split(direction) => f
+ .debug_struct("Split")
+ .field("direction", direction)
+ .finish(),
+ Event::ChangeItemTitle => f.write_str("ChangeItemTitle"),
+ Event::Focus => f.write_str("Focus"),
+ Event::ZoomIn => f.write_str("ZoomIn"),
+ Event::ZoomOut => f.write_str("ZoomOut"),
+ }
+ }
+}
+
pub struct Pane {
+ focus_handle: FocusHandle,
items: Vec<Box<dyn ItemHandle>>,
- activation_history: Vec<usize>,
+ activation_history: Vec<EntityId>,
zoomed: bool,
+ was_focused: bool,
active_item_index: usize,
- last_focused_view_by_item: HashMap<usize, AnyWeakViewHandle>,
- autoscroll: bool,
+ last_focused_view_by_item: HashMap<EntityId, FocusHandle>,
nav_history: NavHistory,
- toolbar: ViewHandle<Toolbar>,
- tab_bar_context_menu: TabBarContextMenu,
- tab_context_menu: ViewHandle<ContextMenu>,
- _background_actions: BackgroundActions,
- workspace: WeakViewHandle<Workspace>,
- project: ModelHandle<Project>,
- has_focus: bool,
- can_drop: Rc<dyn Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool>,
+ toolbar: View<Toolbar>,
+ new_item_menu: Option<View<ContextMenu>>,
+ split_item_menu: Option<View<ContextMenu>>,
+ // tab_context_menu: View<ContextMenu>,
+ workspace: WeakView<Workspace>,
+ project: Model<Project>,
+ drag_split_direction: Option<SplitDirection>,
+ can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut WindowContext) -> bool>>,
can_split: bool,
- render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>>,
+ render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement>,
+ _subscriptions: Vec<Subscription>,
+ tab_bar_scroll_handle: ScrollHandle,
+ display_nav_history_buttons: bool,
}
pub struct ItemNavHistory {
history: NavHistory,
- item: Rc<dyn WeakItemHandle>,
+ item: Arc<dyn WeakItemHandle>,
}
#[derive(Clone)]
-pub struct NavHistory(Rc<RefCell<NavHistoryState>>);
+pub struct NavHistory(Arc<Mutex<NavHistoryState>>);
struct NavHistoryState {
mode: NavigationMode,
backward_stack: VecDeque<NavigationEntry>,
forward_stack: VecDeque<NavigationEntry>,
closed_stack: VecDeque<NavigationEntry>,
- paths_by_item: HashMap<usize, (ProjectPath, Option<PathBuf>)>,
- pane: WeakViewHandle<Pane>,
+ paths_by_item: HashMap<EntityId, (ProjectPath, Option<PathBuf>)>,
+ pane: WeakView<Pane>,
next_timestamp: Arc<AtomicUsize>,
}
@@ -237,115 +224,135 @@ impl Default for NavigationMode {
}
pub struct NavigationEntry {
- pub item: Rc<dyn WeakItemHandle>,
- pub data: Option<Box<dyn Any>>,
+ pub item: Arc<dyn WeakItemHandle>,
+ pub data: Option<Box<dyn Any + Send>>,
pub timestamp: usize,
}
-pub struct DraggedItem {
- pub handle: Box<dyn ItemHandle>,
- pub pane: WeakViewHandle<Pane>,
-}
-
-pub enum ReorderBehavior {
- None,
- MoveAfterActive,
- MoveToIndex(usize),
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum TabBarContextMenuKind {
- New,
- Split,
-}
-
-struct TabBarContextMenu {
- kind: TabBarContextMenuKind,
- handle: ViewHandle<ContextMenu>,
-}
-
-impl TabBarContextMenu {
- fn handle_if_kind(&self, kind: TabBarContextMenuKind) -> Option<ViewHandle<ContextMenu>> {
- if self.kind == kind {
- return Some(self.handle.clone());
- }
- None
- }
+#[derive(Clone)]
+pub struct DraggedTab {
+ pub pane: View<Pane>,
+ pub ix: usize,
+ pub item_id: EntityId,
+ pub detail: usize,
+ pub is_active: bool,
}
-#[allow(clippy::too_many_arguments)]
-fn nav_button<A: Action, F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>)>(
- svg_path: &'static str,
- style: theme::Interactive<theme::IconButton>,
- nav_button_height: f32,
- tooltip_style: TooltipStyle,
- enabled: bool,
- on_click: F,
- tooltip_action: A,
- action_name: &str,
- cx: &mut ViewContext<Pane>,
-) -> AnyElement<Pane> {
- MouseEventHandler::new::<A, _>(0, cx, |state, _| {
- let style = if enabled {
- style.style_for(state)
- } else {
- style.disabled_style()
- };
- Svg::new(svg_path)
- .with_color(style.color)
- .constrained()
- .with_width(style.icon_width)
- .aligned()
- .contained()
- .with_style(style.container)
- .constrained()
- .with_width(style.button_width)
- .with_height(nav_button_height)
- .aligned()
- .top()
- })
- .with_cursor_style(if enabled {
- CursorStyle::PointingHand
- } else {
- CursorStyle::default()
- })
- .on_click(MouseButton::Left, move |_, toolbar, cx| {
- on_click(toolbar, cx)
- })
- .with_tooltip::<A>(
- 0,
- action_name.to_string(),
- Some(Box::new(tooltip_action)),
- tooltip_style,
- cx,
- )
- .contained()
- .into_any_named("nav button")
-}
+// pub struct DraggedItem {
+// pub handle: Box<dyn ItemHandle>,
+// pub pane: WeakView<Pane>,
+// }
+
+// pub enum ReorderBehavior {
+// None,
+// MoveAfterActive,
+// MoveToIndex(usize),
+// }
+
+// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
+// enum TabBarContextMenuKind {
+// New,
+// Split,
+// }
+
+// struct TabBarContextMenu {
+// kind: TabBarContextMenuKind,
+// handle: View<ContextMenu>,
+// }
+
+// impl TabBarContextMenu {
+// fn handle_if_kind(&self, kind: TabBarContextMenuKind) -> Option<View<ContextMenu>> {
+// if self.kind == kind {
+// return Some(self.handle.clone());
+// }
+// None
+// }
+// }
+
+// #[allow(clippy::too_many_arguments)]
+// fn nav_button<A: Action, F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>)>(
+// svg_path: &'static str,
+// style: theme2::Interactive<theme2::IconButton>,
+// nav_button_height: f32,
+// tooltip_style: TooltipStyle,
+// enabled: bool,
+// on_click: F,
+// tooltip_action: A,
+// action_name: &str,
+// cx: &mut ViewContext<Pane>,
+// ) -> AnyElement<Pane> {
+// MouseEventHandler::new::<A, _>(0, cx, |state, _| {
+// let style = if enabled {
+// style.style_for(state)
+// } else {
+// style.disabled_style()
+// };
+// Svg::new(svg_path)
+// .with_color(style.color)
+// .constrained()
+// .with_width(style.icon_width)
+// .aligned()
+// .contained()
+// .with_style(style.container)
+// .constrained()
+// .with_width(style.button_width)
+// .with_height(nav_button_height)
+// .aligned()
+// .top()
+// })
+// .with_cursor_style(if enabled {
+// CursorStyle::PointingHand
+// } else {
+// CursorStyle::default()
+// })
+// .on_click(MouseButton::Left, move |_, toolbar, cx| {
+// on_click(toolbar, cx)
+// })
+// .with_tooltip::<A>(
+// 0,
+// action_name.to_string(),
+// Some(Box::new(tooltip_action)),
+// tooltip_style,
+// cx,
+// )
+// .contained()
+// .into_any_named("nav button")
+// }
+
+impl EventEmitter<Event> for Pane {}
impl Pane {
pub fn new(
- workspace: WeakViewHandle<Workspace>,
- project: ModelHandle<Project>,
- background_actions: BackgroundActions,
+ workspace: WeakView<Workspace>,
+ project: Model<Project>,
next_timestamp: Arc<AtomicUsize>,
+ can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut WindowContext) -> bool + 'static>>,
cx: &mut ViewContext<Self>,
) -> Self {
- let pane_view_id = cx.view_id();
- let handle = cx.weak_handle();
- let context_menu = cx.add_view(|cx| ContextMenu::new(pane_view_id, cx));
- context_menu.update(cx, |menu, _| {
- menu.set_position_mode(OverlayPositionMode::Local)
- });
-
+ // todo!("context menu")
+ // let pane_view_id = cx.view_id();
+ // let context_menu = cx.build_view(|cx| ContextMenu::new(pane_view_id, cx));
+ // context_menu.update(cx, |menu, _| {
+ // menu.set_position_mode(OverlayPositionMode::Local)
+ // });
+ //
+ let focus_handle = cx.focus_handle();
+
+ let subscriptions = vec![
+ cx.on_focus_in(&focus_handle, move |this, cx| this.focus_in(cx)),
+ cx.on_focus_out(&focus_handle, move |this, cx| this.focus_out(cx)),
+ ];
+
+ let handle = cx.view().downgrade();
Self {
+ focus_handle,
items: Vec::new(),
activation_history: Vec::new(),
+ was_focused: false,
zoomed: false,
active_item_index: 0,
last_focused_view_by_item: Default::default(),
- autoscroll: false,
- nav_history: NavHistory(Rc::new(RefCell::new(NavHistoryState {
+ nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState {
mode: NavigationMode::Normal,
backward_stack: Default::default(),
forward_stack: Default::default(),
@@ -354,96 +361,151 @@ impl Pane {
pane: handle.clone(),
next_timestamp,
}))),
- toolbar: cx.add_view(|_| Toolbar::new()),
- tab_bar_context_menu: TabBarContextMenu {
- kind: TabBarContextMenuKind::New,
- handle: context_menu,
- },
- tab_context_menu: cx.add_view(|cx| ContextMenu::new(pane_view_id, cx)),
- _background_actions: background_actions,
+ toolbar: cx.new_view(|_| Toolbar::new()),
+ new_item_menu: None,
+ split_item_menu: None,
+ tab_bar_scroll_handle: ScrollHandle::new(),
+ drag_split_direction: None,
+ // tab_bar_context_menu: TabBarContextMenu {
+ // kind: TabBarContextMenuKind::New,
+ // handle: context_menu,
+ // },
+ // tab_context_menu: cx.build_view(|_| ContextMenu::new(pane_view_id, cx)),
workspace,
project,
- has_focus: false,
- can_drop: Rc::new(|_, _| true),
+ can_drop_predicate,
can_split: true,
render_tab_bar_buttons: Rc::new(move |pane, cx| {
- Flex::row()
- // New menu
- .with_child(Self::render_tab_bar_button(
- 0,
- "icons/plus.svg",
- false,
- Some(("New...".into(), None)),
- cx,
- |pane, cx| pane.deploy_new_menu(cx),
- |pane, cx| {
- pane.tab_bar_context_menu
- .handle
- .update(cx, |menu, _| menu.delay_cancel())
- },
- pane.tab_bar_context_menu
- .handle_if_kind(TabBarContextMenuKind::New),
- ))
- .with_child(Self::render_tab_bar_button(
- 1,
- "icons/split.svg",
- false,
- Some(("Split Pane".into(), None)),
- cx,
- |pane, cx| pane.deploy_split_menu(cx),
- |pane, cx| {
- pane.tab_bar_context_menu
- .handle
- .update(cx, |menu, _| menu.delay_cancel())
- },
- pane.tab_bar_context_menu
- .handle_if_kind(TabBarContextMenuKind::Split),
- ))
- .with_child({
- let icon_path;
- let tooltip_label;
- if pane.is_zoomed() {
- icon_path = "icons/minimize.svg";
- tooltip_label = "Zoom In";
- } else {
- icon_path = "icons/maximize.svg";
- tooltip_label = "Zoom In";
- }
-
- Pane::render_tab_bar_button(
- 2,
- icon_path,
- pane.is_zoomed(),
- Some((tooltip_label, Some(Box::new(ToggleZoom)))),
- cx,
- move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
- move |_, _| {},
- None,
- )
+ h_stack()
+ .gap_2()
+ .child(
+ IconButton::new("plus", Icon::Plus)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted)
+ .on_click(cx.listener(|pane, _, cx| {
+ let menu = ContextMenu::build(cx, |menu, _| {
+ menu.action("New File", NewFile.boxed_clone())
+ .action("New Terminal", NewCenterTerminal.boxed_clone())
+ .action("New Search", NewSearch.boxed_clone())
+ });
+ cx.subscribe(&menu, |pane, _, _: &DismissEvent, cx| {
+ pane.focus(cx);
+ pane.new_item_menu = None;
+ })
+ .detach();
+ pane.new_item_menu = Some(menu);
+ }))
+ .tooltip(|cx| Tooltip::text("New...", cx)),
+ )
+ .when_some(pane.new_item_menu.as_ref(), |el, new_item_menu| {
+ el.child(Self::render_menu_overlay(new_item_menu))
+ })
+ .child(
+ IconButton::new("split", Icon::Split)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted)
+ .on_click(cx.listener(|pane, _, cx| {
+ let menu = ContextMenu::build(cx, |menu, _| {
+ menu.action("Split Right", SplitRight.boxed_clone())
+ .action("Split Left", SplitLeft.boxed_clone())
+ .action("Split Up", SplitUp.boxed_clone())
+ .action("Split Down", SplitDown.boxed_clone())
+ });
+ cx.subscribe(&menu, |pane, _, _: &DismissEvent, cx| {
+ pane.focus(cx);
+ pane.split_item_menu = None;
+ })
+ .detach();
+ pane.split_item_menu = Some(menu);
+ }))
+ .tooltip(|cx| Tooltip::text("Split Pane", cx)),
+ )
+ .child({
+ let zoomed = pane.is_zoomed();
+ IconButton::new("toggle_zoom", Icon::Maximize)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted)
+ .selected(zoomed)
+ .selected_icon(Icon::Minimize)
+ .on_click(cx.listener(|pane, _, cx| {
+ pane.toggle_zoom(&crate::ToggleZoom, cx);
+ }))
+ .tooltip(move |cx| {
+ Tooltip::text(if zoomed { "Zoom Out" } else { "Zoom In" }, cx)
+ })
+ })
+ .when_some(pane.split_item_menu.as_ref(), |el, split_item_menu| {
+ el.child(Self::render_menu_overlay(split_item_menu))
})
- .into_any()
+ .into_any_element()
}),
+ display_nav_history_buttons: true,
+ _subscriptions: subscriptions,
}
}
- pub(crate) fn workspace(&self) -> &WeakViewHandle<Workspace> {
- &self.workspace
+ pub fn has_focus(&self, cx: &WindowContext) -> bool {
+ // todo!(); // inline this manually
+ self.focus_handle.contains_focused(cx)
+ }
+
+ fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
+ if !self.was_focused {
+ self.was_focused = true;
+ cx.emit(Event::Focus);
+ cx.notify();
+ }
+
+ self.toolbar.update(cx, |toolbar, cx| {
+ toolbar.focus_changed(true, cx);
+ });
+
+ if let Some(active_item) = self.active_item() {
+ if self.focus_handle.is_focused(cx) {
+ // Pane was focused directly. We need to either focus a view inside the active item,
+ // or focus the active item itself
+ if let Some(weak_last_focused_view) =
+ self.last_focused_view_by_item.get(&active_item.item_id())
+ {
+ weak_last_focused_view.focus(cx);
+ return;
+ }
+
+ active_item.focus_handle(cx).focus(cx);
+ } else if let Some(focused) = cx.focused() {
+ if !self.context_menu_focused(cx) {
+ self.last_focused_view_by_item
+ .insert(active_item.item_id(), focused);
+ }
+ }
+ }
}
- pub fn has_focus(&self) -> bool {
- self.has_focus
+ fn context_menu_focused(&self, cx: &mut ViewContext<Self>) -> bool {
+ self.new_item_menu
+ .as_ref()
+ .or(self.split_item_menu.as_ref())
+ .map_or(false, |menu| menu.focus_handle(cx).is_focused(cx))
+ }
+
+ fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
+ self.was_focused = false;
+ self.toolbar.update(cx, |toolbar, cx| {
+ toolbar.focus_changed(false, cx);
+ });
+ cx.notify();
}
pub fn active_item_index(&self) -> usize {
self.active_item_index
}
- pub fn on_can_drop<F>(&mut self, can_drop: F)
- where
- F: 'static + Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool,
- {
- self.can_drop = Rc::new(can_drop);
- }
+ // pub fn on_can_drop<F>(&mut self, can_drop: F)
+ // where
+ // F: 'static + Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool,
+ // {
+ // self.can_drop = Rc::new(can_drop);
+ // }
pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
self.can_split = can_split;
@@ -459,16 +521,16 @@ impl Pane {
pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
where
- F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>,
+ F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement,
{
self.render_tab_bar_buttons = Rc::new(render);
cx.notify();
}
- pub fn nav_history_for_item<T: Item>(&self, item: &ViewHandle<T>) -> ItemNavHistory {
+ pub fn nav_history_for_item<T: Item>(&self, item: &View<T>) -> ItemNavHistory {
ItemNavHistory {
history: self.nav_history.clone(),
- item: Rc::new(item.downgrade()),
+ item: Arc::new(item.downgrade()),
}
}
@@ -489,11 +551,33 @@ impl Pane {
}
pub fn can_navigate_backward(&self) -> bool {
- !self.nav_history.0.borrow().backward_stack.is_empty()
+ !self.nav_history.0.lock().backward_stack.is_empty()
}
pub fn can_navigate_forward(&self) -> bool {
- !self.nav_history.0.borrow().forward_stack.is_empty()
+ !self.nav_history.0.lock().forward_stack.is_empty()
+ }
+
+ fn navigate_backward(&mut self, cx: &mut ViewContext<Self>) {
+ if let Some(workspace) = self.workspace.upgrade() {
+ let pane = cx.view().downgrade();
+ cx.window_context().defer(move |cx| {
+ workspace.update(cx, |workspace, cx| {
+ workspace.go_back(pane, cx).detach_and_log_err(cx)
+ })
+ })
+ }
+ }
+
+ fn navigate_forward(&mut self, cx: &mut ViewContext<Self>) {
+ if let Some(workspace) = self.workspace.upgrade() {
+ let pane = cx.view().downgrade();
+ cx.window_context().defer(move |cx| {
+ workspace.update(cx, |workspace, cx| {
+ workspace.go_forward(pane, cx).detach_and_log_err(cx)
+ })
+ })
+ }
}
fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
@@ -545,9 +629,9 @@ impl Pane {
let abs_path = project.absolute_path(&project_path, cx);
self.nav_history
.0
- .borrow_mut()
+ .lock()
.paths_by_item
- .insert(item.id(), (project_path, abs_path));
+ .insert(item.item_id(), (project_path, abs_path));
}
}
}
@@ -571,7 +655,7 @@ impl Pane {
};
let existing_item_index = self.items.iter().position(|existing_item| {
- if existing_item.id() == item.id() {
+ if existing_item.item_id() == item.item_id() {
true
} else if existing_item.is_singleton(cx) {
existing_item
@@ -636,17 +720,17 @@ impl Pane {
self.items.iter()
}
- pub fn items_of_type<T: View>(&self) -> impl '_ + Iterator<Item = ViewHandle<T>> {
+ pub fn items_of_type<T: Render>(&self) -> impl '_ + Iterator<Item = View<T>> {
self.items
.iter()
- .filter_map(|item| item.as_any().clone().downcast())
+ .filter_map(|item| item.to_any().downcast().ok())
}
pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
self.items.get(self.active_item_index).cloned()
}
- pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
+ pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
self.items
.get(self.active_item_index)?
.pixel_position_of_cursor(cx)
@@ -667,19 +751,20 @@ impl Pane {
}
pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
- self.items.iter().position(|i| i.id() == item.id())
+ self.items
+ .iter()
+ .position(|i| i.item_id() == item.item_id())
}
- pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
- // Potentially warn the user of the new keybinding
- let workspace_handle = self.workspace().clone();
- cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) })
- .detach();
+ pub fn item_for_index(&self, ix: usize) -> Option<&dyn ItemHandle> {
+ self.items.get(ix).map(|i| i.as_ref())
+ }
+ pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
if self.zoomed {
cx.emit(Event::ZoomOut);
} else if !self.items.is_empty() {
- if !self.has_focus {
+ if !self.focus_handle.contains_focused(cx) {
cx.focus_self();
}
cx.emit(Event::ZoomIn);
@@ -712,18 +797,19 @@ impl Pane {
if let Some(newly_active_item) = self.items.get(index) {
self.activation_history
.retain(|&previously_active_item_id| {
- previously_active_item_id != newly_active_item.id()
+ previously_active_item_id != newly_active_item.item_id()
});
- self.activation_history.push(newly_active_item.id());
+ self.activation_history.push(newly_active_item.item_id());
}
self.update_toolbar(cx);
+ self.update_status_bar(cx);
if focus_item {
self.focus_active_item(cx);
}
- self.autoscroll = true;
+ self.tab_bar_scroll_handle.scroll_to_item(index);
cx.notify();
}
}
@@ -756,7 +842,7 @@ impl Pane {
if self.items.is_empty() {
return None;
}
- let active_item_id = self.items[self.active_item_index].id();
+ let active_item_id = self.items[self.active_item_index].item_id();
Some(self.close_item_by_id(
active_item_id,
action.save_intent.unwrap_or(SaveIntent::Close),
@@ -766,7 +852,7 @@ impl Pane {
pub fn close_item_by_id(
&mut self,
- item_id_to_close: usize,
+ item_id_to_close: EntityId,
save_intent: SaveIntent,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
@@ -782,7 +868,7 @@ impl Pane {
return None;
}
- let active_item_id = self.items[self.active_item_index].id();
+ let active_item_id = self.items[self.active_item_index].item_id();
Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
item_id != active_item_id
}))
@@ -796,7 +882,7 @@ impl Pane {
let item_ids: Vec<_> = self
.items()
.filter(|item| !item.is_dirty(cx))
- .map(|item| item.id())
+ .map(|item| item.item_id())
.collect();
Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
item_ids.contains(&item_id)
@@ -811,19 +897,19 @@ impl Pane {
if self.items.is_empty() {
return None;
}
- let active_item_id = self.items[self.active_item_index].id();
+ let active_item_id = self.items[self.active_item_index].item_id();
Some(self.close_items_to_the_left_by_id(active_item_id, cx))
}
pub fn close_items_to_the_left_by_id(
&mut self,
- item_id: usize,
+ item_id: EntityId,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
let item_ids: Vec<_> = self
.items()
- .take_while(|item| item.id() != item_id)
- .map(|item| item.id())
+ .take_while(|item| item.item_id() != item_id)
+ .map(|item| item.item_id())
.collect();
self.close_items(cx, SaveIntent::Close, move |item_id| {
item_ids.contains(&item_id)
@@ -838,20 +924,20 @@ impl Pane {
if self.items.is_empty() {
return None;
}
- let active_item_id = self.items[self.active_item_index].id();
+ let active_item_id = self.items[self.active_item_index].item_id();
Some(self.close_items_to_the_right_by_id(active_item_id, cx))
}
pub fn close_items_to_the_right_by_id(
&mut self,
- item_id: usize,
+ item_id: EntityId,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
let item_ids: Vec<_> = self
.items()
.rev()
- .take_while(|item| item.id() != item_id)
- .map(|item| item.id())
+ .take_while(|item| item.item_id() != item_id)
+ .map(|item| item.item_id())
.collect();
self.close_items(cx, SaveIntent::Close, move |item_id| {
item_ids.contains(&item_id)
@@ -913,13 +999,13 @@ impl Pane {
&mut self,
cx: &mut ViewContext<Pane>,
mut save_intent: SaveIntent,
- should_close: impl 'static + Fn(usize) -> bool,
+ should_close: impl Fn(EntityId) -> bool,
) -> Task<Result<()>> {
// Find the items to close.
let mut items_to_close = Vec::new();
let mut dirty_items = Vec::new();
for item in &self.items {
- if should_close(item.id()) {
+ if should_close(item.item_id()) {
items_to_close.push(item.boxed_clone());
if item.is_dirty(cx) {
dirty_items.push(item.boxed_clone());
@@ -936,7 +1022,7 @@ impl Pane {
let workspace = self.workspace.clone();
cx.spawn(|pane, mut cx| async move {
if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
- let mut answer = pane.update(&mut cx, |_, cx| {
+ let answer = pane.update(&mut cx, |_, cx| {
let prompt =
Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx);
cx.prompt(
@@ -945,9 +1031,9 @@ impl Pane {
&["Save all", "Discard all", "Cancel"],
)
})?;
- match answer.next().await {
- Some(0) => save_intent = SaveIntent::SaveAll,
- Some(1) => save_intent = SaveIntent::Skip,
+ match answer.await {
+ Ok(0) => save_intent = SaveIntent::SaveAll,
+ Ok(1) => save_intent = SaveIntent::Skip,
_ => {}
}
}
@@ -1,15 +1,14 @@
use super::DraggedItem;
use crate::{Pane, SplitDirection, Workspace};
-use drag_and_drop::DragAndDrop;
use gpui::{
color::Color,
- elements::{Canvas, MouseEventHandler, ParentElement, Stack},
+ elements::{Canvas, MouseEventHandler, ParentComponent, Stack},
geometry::{rect::RectF, vector::Vector2F},
platform::MouseButton,
scene::MouseUp,
AppContext, Element, EventContext, MouseState, Quad, ViewContext, WeakViewHandle,
};
-use project::ProjectEntryId;
+use project2::ProjectEntryId;
pub fn dragged_item_receiver<Tag, D, F>(
pane: &Pane,
@@ -236,5 +235,5 @@ fn drop_split_direction(
}
fn overlay_color(cx: &AppContext) -> Color {
- theme::current(cx).workspace.drop_target_overlay_color
+ theme2::current(cx).workspace.drop_target_overlay_color
}
@@ -1,23 +1,22 @@
-use crate::{pane_group::element::PaneAxisElement, AppState, FollowerState, Pane, Workspace};
+use crate::{pane_group::element::pane_axis, AppState, FollowerState, Pane, Workspace};
use anyhow::{anyhow, Result};
use call::{ActiveCall, ParticipantLocation};
use collections::HashMap;
use gpui::{
- elements::*,
- geometry::{rect::RectF, vector::Vector2F},
- platform::{CursorStyle, MouseButton},
- AnyViewHandle, Axis, ModelHandle, ViewContext, ViewHandle,
+ point, size, AnyWeakView, Axis, Bounds, Entity as _, IntoElement, Model, Pixels, Point, View,
+ ViewContext,
};
+use parking_lot::Mutex;
use project::Project;
use serde::Deserialize;
-use std::{cell::RefCell, rc::Rc, sync::Arc};
-use theme::Theme;
+use std::sync::Arc;
+use ui::{prelude::*, Button};
-const HANDLE_HITBOX_SIZE: f32 = 4.0;
+const HANDLE_HITBOX_SIZE: f32 = 10.0; //todo!(change this back to 4)
const HORIZONTAL_MIN_SIZE: f32 = 80.;
const VERTICAL_MIN_SIZE: f32 = 100.;
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone)]
pub struct PaneGroup {
pub(crate) root: Member,
}
@@ -27,7 +26,7 @@ impl PaneGroup {
Self { root }
}
- pub fn new(pane: ViewHandle<Pane>) -> Self {
+ pub fn new(pane: View<Pane>) -> Self {
Self {
root: Member::Pane(pane),
}
@@ -35,8 +34,8 @@ impl PaneGroup {
pub fn split(
&mut self,
- old_pane: &ViewHandle<Pane>,
- new_pane: &ViewHandle<Pane>,
+ old_pane: &View<Pane>,
+ new_pane: &View<Pane>,
direction: SplitDirection,
) -> Result<()> {
match &mut self.root {
@@ -52,14 +51,14 @@ impl PaneGroup {
}
}
- pub fn bounding_box_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<RectF> {
+ pub fn bounding_box_for_pane(&self, pane: &View<Pane>) -> Option<Bounds<Pixels>> {
match &self.root {
Member::Pane(_) => None,
Member::Axis(axis) => axis.bounding_box_for_pane(pane),
}
}
- pub fn pane_at_pixel_position(&self, coordinate: Vector2F) -> Option<&ViewHandle<Pane>> {
+ pub fn pane_at_pixel_position(&self, coordinate: Point<Pixels>) -> Option<&View<Pane>> {
match &self.root {
Member::Pane(pane) => Some(pane),
Member::Axis(axis) => axis.pane_at_pixel_position(coordinate),
@@ -70,7 +69,7 @@ impl PaneGroup {
/// - Ok(true) if it found and removed a pane
/// - Ok(false) if it found but did not remove the pane
/// - Err(_) if it did not find the pane
- pub fn remove(&mut self, pane: &ViewHandle<Pane>) -> Result<bool> {
+ pub fn remove(&mut self, pane: &View<Pane>) -> Result<bool> {
match &mut self.root {
Member::Pane(_) => Ok(false),
Member::Axis(axis) => {
@@ -82,7 +81,7 @@ impl PaneGroup {
}
}
- pub fn swap(&mut self, from: &ViewHandle<Pane>, to: &ViewHandle<Pane>) {
+ pub fn swap(&mut self, from: &View<Pane>, to: &View<Pane>) {
match &mut self.root {
Member::Pane(_) => {}
Member::Axis(axis) => axis.swap(from, to),
@@ -91,19 +90,17 @@ impl PaneGroup {
pub(crate) fn render(
&self,
- project: &ModelHandle<Project>,
- theme: &Theme,
- follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
- active_call: Option<&ModelHandle<ActiveCall>>,
- active_pane: &ViewHandle<Pane>,
- zoomed: Option<&AnyViewHandle>,
+ project: &Model<Project>,
+ follower_states: &HashMap<View<Pane>, FollowerState>,
+ active_call: Option<&Model<ActiveCall>>,
+ active_pane: &View<Pane>,
+ zoomed: Option<&AnyWeakView>,
app_state: &Arc<AppState>,
cx: &mut ViewContext<Workspace>,
- ) -> AnyElement<Workspace> {
+ ) -> impl IntoElement {
self.root.render(
project,
0,
- theme,
follower_states,
active_call,
active_pane,
@@ -113,25 +110,25 @@ impl PaneGroup {
)
}
- pub(crate) fn panes(&self) -> Vec<&ViewHandle<Pane>> {
+ pub(crate) fn panes(&self) -> Vec<&View<Pane>> {
let mut panes = Vec::new();
self.root.collect_panes(&mut panes);
panes
}
+
+ pub(crate) fn first_pane(&self) -> View<Pane> {
+ self.root.first_pane()
+ }
}
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone)]
pub(crate) enum Member {
Axis(PaneAxis),
- Pane(ViewHandle<Pane>),
+ Pane(View<Pane>),
}
impl Member {
- fn new_axis(
- old_pane: ViewHandle<Pane>,
- new_pane: ViewHandle<Pane>,
- direction: SplitDirection,
- ) -> Self {
+ fn new_axis(old_pane: View<Pane>, new_pane: View<Pane>, direction: SplitDirection) -> Self {
use Axis::*;
use SplitDirection::*;
@@ -148,52 +145,52 @@ impl Member {
Member::Axis(PaneAxis::new(axis, members))
}
- fn contains(&self, needle: &ViewHandle<Pane>) -> bool {
+ fn contains(&self, needle: &View<Pane>) -> bool {
match self {
Member::Axis(axis) => axis.members.iter().any(|member| member.contains(needle)),
Member::Pane(pane) => pane == needle,
}
}
+ fn first_pane(&self) -> View<Pane> {
+ match self {
+ Member::Axis(axis) => axis.members[0].first_pane(),
+ Member::Pane(pane) => pane.clone(),
+ }
+ }
+
pub fn render(
&self,
- project: &ModelHandle<Project>,
+ project: &Model<Project>,
basis: usize,
- theme: &Theme,
- follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
- active_call: Option<&ModelHandle<ActiveCall>>,
- active_pane: &ViewHandle<Pane>,
- zoomed: Option<&AnyViewHandle>,
+ follower_states: &HashMap<View<Pane>, FollowerState>,
+ active_call: Option<&Model<ActiveCall>>,
+ active_pane: &View<Pane>,
+ zoomed: Option<&AnyWeakView>,
app_state: &Arc<AppState>,
cx: &mut ViewContext<Workspace>,
- ) -> AnyElement<Workspace> {
- enum FollowIntoExternalProject {}
-
+ ) -> impl IntoElement {
match self {
Member::Pane(pane) => {
- let pane_element = if Some(&**pane) == zoomed {
- Empty::new().into_any()
- } else {
- ChildView::new(pane, cx).into_any()
- };
+ if zoomed == Some(&pane.downgrade().into()) {
+ return div().into_any();
+ }
let leader = follower_states.get(pane).and_then(|state| {
let room = active_call?.read(cx).room()?.read(cx);
room.remote_participant_for_peer_id(state.leader_id)
});
- let mut leader_border = Border::default();
+ let mut leader_border = None;
let mut leader_status_box = None;
if let Some(leader) = &leader {
- let leader_color = theme
- .editor
- .selection_style_for_room_participant(leader.participant_index.0)
+ let mut leader_color = cx
+ .theme()
+ .players()
+ .color_for_participant(leader.participant_index.0)
.cursor;
- leader_border = Border::all(theme.workspace.leader_border_width, leader_color);
- leader_border
- .color
- .fade_out(1. - theme.workspace.leader_border_opacity);
- leader_border.overlay = true;
+ leader_color.fade_out(0.3);
+ leader_border = Some(leader_color);
leader_status_box = match leader.location {
ParticipantLocation::SharedProject {
@@ -205,97 +202,90 @@ impl Member {
let leader_user = leader.user.clone();
let leader_user_id = leader.user.id;
Some(
- MouseEventHandler::new::<FollowIntoExternalProject, _>(
- pane.id(),
- cx,
- |_, _| {
- Label::new(
- format!(
- "Follow {} to their active project",
- leader_user.github_login,
- ),
- theme
- .workspace
- .external_location_message
- .text
- .clone(),
- )
- .contained()
- .with_style(
- theme.workspace.external_location_message.container,
+ Button::new(
+ ("leader-status", pane.entity_id()),
+ format!(
+ "Follow {} to their active project",
+ leader_user.github_login,
+ ),
+ )
+ .on_click(cx.listener(
+ move |this, _, cx| {
+ crate::join_remote_project(
+ leader_project_id,
+ leader_user_id,
+ this.app_state().clone(),
+ cx,
)
+ .detach_and_log_err(cx);
},
- )
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- crate::join_remote_project(
- leader_project_id,
- leader_user_id,
- this.app_state().clone(),
- cx,
- )
- .detach_and_log_err(cx);
- })
- .aligned()
- .bottom()
- .right()
- .into_any(),
+ )),
)
}
}
- ParticipantLocation::UnsharedProject => Some(
- Label::new(
- format!(
- "{} is viewing an unshared Zed project",
- leader.user.github_login
- ),
- theme.workspace.external_location_message.text.clone(),
- )
- .contained()
- .with_style(theme.workspace.external_location_message.container)
- .aligned()
- .bottom()
- .right()
- .into_any(),
- ),
- ParticipantLocation::External => Some(
- Label::new(
- format!(
- "{} is viewing a window outside of Zed",
- leader.user.github_login
- ),
- theme.workspace.external_location_message.text.clone(),
- )
- .contained()
- .with_style(theme.workspace.external_location_message.container)
- .aligned()
- .bottom()
- .right()
- .into_any(),
- ),
+ ParticipantLocation::UnsharedProject => Some(Button::new(
+ ("leader-status", pane.entity_id()),
+ format!(
+ "{} is viewing an unshared Zed project",
+ leader.user.github_login
+ ),
+ )),
+ ParticipantLocation::External => Some(Button::new(
+ ("leader-status", pane.entity_id()),
+ format!(
+ "{} is viewing a window outside of Zed",
+ leader.user.github_login
+ ),
+ )),
};
}
- Stack::new()
- .with_child(pane_element.contained().with_border(leader_border))
- .with_children(leader_status_box)
+ div()
+ .relative()
+ .flex_1()
+ .size_full()
+ .child(pane.clone())
+ .when_some(leader_border, |this, color| {
+ this.border_2().border_color(color)
+ })
+ .when_some(leader_status_box, |this, status_box| {
+ this.child(
+ div()
+ .absolute()
+ .w_96()
+ .bottom_3()
+ .right_3()
+ .z_index(1)
+ .child(status_box),
+ )
+ })
.into_any()
+
+ // let el = div()
+ // .flex()
+ // .flex_1()
+ // .gap_px()
+ // .w_full()
+ // .h_full()
+ // .bg(cx.theme().colors().editor)
+ // .children();
}
- Member::Axis(axis) => axis.render(
- project,
- basis + 1,
- theme,
- follower_states,
- active_call,
- active_pane,
- zoomed,
- app_state,
- cx,
- ),
+ Member::Axis(axis) => axis
+ .render(
+ project,
+ basis + 1,
+ follower_states,
+ active_call,
+ active_pane,
+ zoomed,
+ app_state,
+ cx,
+ )
+ .into_any(),
}
}
- fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a ViewHandle<Pane>>) {
+ fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a View<Pane>>) {
match self {
Member::Axis(axis) => {
for member in &axis.members {
@@ -307,18 +297,18 @@ impl Member {
}
}
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone)]
pub(crate) struct PaneAxis {
pub axis: Axis,
pub members: Vec<Member>,
- pub flexes: Rc<RefCell<Vec<f32>>>,
- pub bounding_boxes: Rc<RefCell<Vec<Option<RectF>>>>,
+ pub flexes: Arc<Mutex<Vec<f32>>>,
+ pub bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
}
impl PaneAxis {
pub fn new(axis: Axis, members: Vec<Member>) -> Self {
- let flexes = Rc::new(RefCell::new(vec![1.; members.len()]));
- let bounding_boxes = Rc::new(RefCell::new(vec![None; members.len()]));
+ let flexes = Arc::new(Mutex::new(vec![1.; members.len()]));
+ let bounding_boxes = Arc::new(Mutex::new(vec![None; members.len()]));
Self {
axis,
members,
@@ -331,8 +321,8 @@ impl PaneAxis {
let flexes = flexes.unwrap_or_else(|| vec![1.; members.len()]);
debug_assert!(members.len() == flexes.len());
- let flexes = Rc::new(RefCell::new(flexes));
- let bounding_boxes = Rc::new(RefCell::new(vec![None; members.len()]));
+ let flexes = Arc::new(Mutex::new(flexes));
+ let bounding_boxes = Arc::new(Mutex::new(vec![None; members.len()]));
Self {
axis,
members,
@@ -343,8 +333,8 @@ impl PaneAxis {
fn split(
&mut self,
- old_pane: &ViewHandle<Pane>,
- new_pane: &ViewHandle<Pane>,
+ old_pane: &View<Pane>,
+ new_pane: &View<Pane>,
direction: SplitDirection,
) -> Result<()> {
for (mut idx, member) in self.members.iter_mut().enumerate() {
@@ -362,7 +352,7 @@ impl PaneAxis {
}
self.members.insert(idx, Member::Pane(new_pane.clone()));
- *self.flexes.borrow_mut() = vec![1.; self.members.len()];
+ *self.flexes.lock() = vec![1.; self.members.len()];
} else {
*member =
Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
@@ -375,7 +365,7 @@ impl PaneAxis {
Err(anyhow!("Pane not found"))
}
- fn remove(&mut self, pane_to_remove: &ViewHandle<Pane>) -> Result<Option<Member>> {
+ fn remove(&mut self, pane_to_remove: &View<Pane>) -> Result<Option<Member>> {
let mut found_pane = false;
let mut remove_member = None;
for (idx, member) in self.members.iter_mut().enumerate() {
@@ -402,12 +392,12 @@ impl PaneAxis {
if found_pane {
if let Some(idx) = remove_member {
self.members.remove(idx);
- *self.flexes.borrow_mut() = vec![1.; self.members.len()];
+ *self.flexes.lock() = vec![1.; self.members.len()];
}
if self.members.len() == 1 {
let result = self.members.pop();
- *self.flexes.borrow_mut() = vec![1.; self.members.len()];
+ *self.flexes.lock() = vec![1.; self.members.len()];
Ok(result)
} else {
Ok(None)
@@ -417,7 +407,7 @@ impl PaneAxis {
}
}
- fn swap(&mut self, from: &ViewHandle<Pane>, to: &ViewHandle<Pane>) {
+ fn swap(&mut self, from: &View<Pane>, to: &View<Pane>) {
for member in self.members.iter_mut() {
match member {
Member::Axis(axis) => axis.swap(from, to),
@@ -432,14 +422,14 @@ impl PaneAxis {
}
}
- fn bounding_box_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<RectF> {
- debug_assert!(self.members.len() == self.bounding_boxes.borrow().len());
+ fn bounding_box_for_pane(&self, pane: &View<Pane>) -> Option<Bounds<Pixels>> {
+ debug_assert!(self.members.len() == self.bounding_boxes.lock().len());
for (idx, member) in self.members.iter().enumerate() {
match member {
Member::Pane(found) => {
if pane == found {
- return self.bounding_boxes.borrow()[idx];
+ return self.bounding_boxes.lock()[idx];
}
}
Member::Axis(axis) => {
@@ -452,14 +442,14 @@ impl PaneAxis {
None
}
- fn pane_at_pixel_position(&self, coordinate: Vector2F) -> Option<&ViewHandle<Pane>> {
- debug_assert!(self.members.len() == self.bounding_boxes.borrow().len());
+ fn pane_at_pixel_position(&self, coordinate: Point<Pixels>) -> Option<&View<Pane>> {
+ debug_assert!(self.members.len() == self.bounding_boxes.lock().len());
- let bounding_boxes = self.bounding_boxes.borrow();
+ let bounding_boxes = self.bounding_boxes.lock();
for (idx, member) in self.members.iter().enumerate() {
if let Some(coordinates) = bounding_boxes[idx] {
- if coordinates.contains_point(coordinate) {
+ if coordinates.contains(&coordinate) {
return match member {
Member::Pane(found) => Some(found),
Member::Axis(axis) => axis.pane_at_pixel_position(coordinate),
@@ -472,65 +462,43 @@ impl PaneAxis {
fn render(
&self,
- project: &ModelHandle<Project>,
+ project: &Model<Project>,
basis: usize,
- theme: &Theme,
- follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
- active_call: Option<&ModelHandle<ActiveCall>>,
- active_pane: &ViewHandle<Pane>,
- zoomed: Option<&AnyViewHandle>,
+ follower_states: &HashMap<View<Pane>, FollowerState>,
+ active_call: Option<&Model<ActiveCall>>,
+ active_pane: &View<Pane>,
+ zoomed: Option<&AnyWeakView>,
app_state: &Arc<AppState>,
cx: &mut ViewContext<Workspace>,
- ) -> AnyElement<Workspace> {
- debug_assert!(self.members.len() == self.flexes.borrow().len());
+ ) -> gpui::AnyElement {
+ debug_assert!(self.members.len() == self.flexes.lock().len());
+ let mut active_pane_ix = None;
- let mut pane_axis = PaneAxisElement::new(
+ pane_axis(
self.axis,
basis,
self.flexes.clone(),
self.bounding_boxes.clone(),
- );
- let mut active_pane_ix = None;
-
- let mut members = self.members.iter().enumerate().peekable();
- while let Some((ix, member)) = members.next() {
- let last = members.peek().is_none();
-
+ )
+ .children(self.members.iter().enumerate().map(|(ix, member)| {
if member.contains(active_pane) {
active_pane_ix = Some(ix);
}
-
- let mut member = member.render(
- project,
- (basis + ix) * 10,
- theme,
- follower_states,
- active_call,
- active_pane,
- zoomed,
- app_state,
- cx,
- );
-
- if !last {
- let mut border = theme.workspace.pane_divider;
- border.left = false;
- border.right = false;
- border.top = false;
- border.bottom = false;
-
- match self.axis {
- Axis::Vertical => border.bottom = true,
- Axis::Horizontal => border.right = true,
- }
-
- member = member.contained().with_border(border).into_any();
- }
-
- pane_axis = pane_axis.with_child(member.into_any());
- }
- pane_axis.set_active_pane(active_pane_ix);
- pane_axis.into_any()
+ member
+ .render(
+ project,
+ (basis + ix) * 10,
+ follower_states,
+ active_call,
+ active_pane,
+ zoomed,
+ app_state,
+ cx,
+ )
+ .into_any_element()
+ }))
+ .with_active_pane(active_pane_ix)
+ .into_any_element()
}
}
@@ -547,28 +515,33 @@ impl SplitDirection {
[Self::Up, Self::Down, Self::Left, Self::Right]
}
- pub fn edge(&self, rect: RectF) -> f32 {
+ pub fn edge(&self, rect: Bounds<Pixels>) -> Pixels {
match self {
- Self::Up => rect.min_y(),
- Self::Down => rect.max_y(),
- Self::Left => rect.min_x(),
- Self::Right => rect.max_x(),
+ Self::Up => rect.origin.y,
+ Self::Down => rect.lower_left().y,
+ Self::Left => rect.lower_left().x,
+ Self::Right => rect.lower_right().x,
}
}
- // Returns a new rectangle which shares an edge in SplitDirection and has `size` along SplitDirection
- pub fn along_edge(&self, rect: RectF, size: f32) -> RectF {
+ pub fn along_edge(&self, bounds: Bounds<Pixels>, length: Pixels) -> Bounds<Pixels> {
match self {
- Self::Up => RectF::new(rect.origin(), Vector2F::new(rect.width(), size)),
- Self::Down => RectF::new(
- rect.lower_left() - Vector2F::new(0., size),
- Vector2F::new(rect.width(), size),
- ),
- Self::Left => RectF::new(rect.origin(), Vector2F::new(size, rect.height())),
- Self::Right => RectF::new(
- rect.upper_right() - Vector2F::new(size, 0.),
- Vector2F::new(size, rect.height()),
- ),
+ Self::Up => Bounds {
+ origin: bounds.origin,
+ size: size(bounds.size.width, length),
+ },
+ Self::Down => Bounds {
+ origin: point(bounds.lower_left().x, bounds.lower_left().y - length),
+ size: size(bounds.size.width, length),
+ },
+ Self::Left => Bounds {
+ origin: bounds.origin,
+ size: size(length, bounds.size.height),
+ },
+ Self::Right => Bounds {
+ origin: point(bounds.lower_right().x - length, bounds.lower_left().y),
+ size: size(length, bounds.size.height),
+ },
}
}
@@ -588,402 +561,305 @@ impl SplitDirection {
}
mod element {
- use std::{cell::RefCell, iter::from_fn, ops::Range, rc::Rc};
+
+ use std::{cell::RefCell, iter, rc::Rc, sync::Arc};
use gpui::{
- geometry::{
- rect::RectF,
- vector::{vec2f, Vector2F},
- },
- json::{self, ToJson},
- platform::{CursorStyle, MouseButton},
- scene::MouseDrag,
- AnyElement, Axis, CursorRegion, Element, EventContext, MouseRegion, RectFExt,
- SizeConstraint, Vector2FExt, ViewContext,
+ px, relative, Along, AnyElement, Axis, Bounds, CursorStyle, Element, InteractiveBounds,
+ IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point,
+ Size, Style, WindowContext,
};
+ use parking_lot::Mutex;
+ use smallvec::SmallVec;
+ use ui::prelude::*;
- use crate::{
- pane_group::{HANDLE_HITBOX_SIZE, HORIZONTAL_MIN_SIZE, VERTICAL_MIN_SIZE},
- Workspace, WorkspaceSettings,
- };
+ use super::{HANDLE_HITBOX_SIZE, HORIZONTAL_MIN_SIZE, VERTICAL_MIN_SIZE};
+
+ const DIVIDER_SIZE: f32 = 1.0;
+
+ pub fn pane_axis(
+ axis: Axis,
+ basis: usize,
+ flexes: Arc<Mutex<Vec<f32>>>,
+ bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
+ ) -> PaneAxisElement {
+ PaneAxisElement {
+ axis,
+ basis,
+ flexes,
+ bounding_boxes,
+ children: SmallVec::new(),
+ active_pane_ix: None,
+ }
+ }
pub struct PaneAxisElement {
axis: Axis,
basis: usize,
+ flexes: Arc<Mutex<Vec<f32>>>,
+ bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
+ children: SmallVec<[AnyElement; 2]>,
active_pane_ix: Option<usize>,
- flexes: Rc<RefCell<Vec<f32>>>,
- children: Vec<AnyElement<Workspace>>,
- bounding_boxes: Rc<RefCell<Vec<Option<RectF>>>>,
}
impl PaneAxisElement {
- pub fn new(
- axis: Axis,
- basis: usize,
- flexes: Rc<RefCell<Vec<f32>>>,
- bounding_boxes: Rc<RefCell<Vec<Option<RectF>>>>,
- ) -> Self {
- Self {
- axis,
- basis,
- flexes,
- bounding_boxes,
- active_pane_ix: None,
- children: Default::default(),
- }
- }
-
- pub fn set_active_pane(&mut self, active_pane_ix: Option<usize>) {
+ pub fn with_active_pane(mut self, active_pane_ix: Option<usize>) -> Self {
self.active_pane_ix = active_pane_ix;
+ self
}
- fn layout_children(
- &mut self,
- active_pane_magnification: f32,
- constraint: SizeConstraint,
- remaining_space: &mut f32,
- remaining_flex: &mut f32,
- cross_axis_max: &mut f32,
- view: &mut Workspace,
- cx: &mut ViewContext<Workspace>,
+ fn compute_resize(
+ flexes: &Arc<Mutex<Vec<f32>>>,
+ e: &MouseMoveEvent,
+ ix: usize,
+ axis: Axis,
+ child_start: Point<Pixels>,
+ container_size: Size<Pixels>,
+ cx: &mut WindowContext,
) {
- let flexes = self.flexes.borrow();
- let cross_axis = self.axis.invert();
- for (ix, child) in self.children.iter_mut().enumerate() {
- let flex = if active_pane_magnification != 1. {
- if let Some(active_pane_ix) = self.active_pane_ix {
- if ix == active_pane_ix {
- active_pane_magnification
- } else {
- 1.
- }
- } else {
- 1.
- }
- } else {
- flexes[ix]
- };
+ let min_size = match axis {
+ Axis::Horizontal => px(HORIZONTAL_MIN_SIZE),
+ Axis::Vertical => px(VERTICAL_MIN_SIZE),
+ };
+ let mut flexes = flexes.lock();
+ debug_assert!(flex_values_in_bounds(flexes.as_slice()));
- let child_size = if *remaining_flex == 0.0 {
- *remaining_space
- } else {
- let space_per_flex = *remaining_space / *remaining_flex;
- space_per_flex * flex
- };
+ let size = move |ix, flexes: &[f32]| {
+ container_size.along(axis) * (flexes[ix] / flexes.len() as f32)
+ };
- let child_constraint = match self.axis {
- Axis::Horizontal => SizeConstraint::new(
- vec2f(child_size, constraint.min.y()),
- vec2f(child_size, constraint.max.y()),
- ),
- Axis::Vertical => SizeConstraint::new(
- vec2f(constraint.min.x(), child_size),
- vec2f(constraint.max.x(), child_size),
- ),
- };
- let child_size = child.layout(child_constraint, view, cx);
- *remaining_space -= child_size.along(self.axis);
- *remaining_flex -= flex;
- *cross_axis_max = cross_axis_max.max(child_size.along(cross_axis));
+ // Don't allow resizing to less than the minimum size, if elements are already too small
+ if min_size - px(1.) > size(ix, flexes.as_slice()) {
+ return;
}
- }
- fn handle_resize(
- flexes: Rc<RefCell<Vec<f32>>>,
- axis: Axis,
- preceding_ix: usize,
- child_start: Vector2F,
- drag_bounds: RectF,
- ) -> impl Fn(MouseDrag, &mut Workspace, &mut EventContext<Workspace>) {
- let size = move |ix, flexes: &[f32]| {
- drag_bounds.length_along(axis) * (flexes[ix] / flexes.len() as f32)
+ let mut proposed_current_pixel_change =
+ (e.position - child_start).along(axis) - size(ix, flexes.as_slice());
+
+ let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| {
+ let flex_change = pixel_dx / container_size.along(axis);
+ let current_target_flex = flexes[target_ix] + flex_change;
+ let next_target_flex = flexes[(target_ix as isize + next) as usize] - flex_change;
+ (current_target_flex, next_target_flex)
};
- move |drag, workspace: &mut Workspace, cx| {
- if drag.end {
- // TODO: Clear cascading resize state
- return;
- }
- let min_size = match axis {
- Axis::Horizontal => HORIZONTAL_MIN_SIZE,
- Axis::Vertical => VERTICAL_MIN_SIZE,
- };
- let mut flexes = flexes.borrow_mut();
+ let mut successors = iter::from_fn({
+ let forward = proposed_current_pixel_change > px(0.);
+ let mut ix_offset = 0;
+ let len = flexes.len();
+ move || {
+ let result = if forward {
+ (ix + 1 + ix_offset < len).then(|| ix + ix_offset)
+ } else {
+ (ix as isize - ix_offset as isize >= 0).then(|| ix - ix_offset)
+ };
- // Don't allow resizing to less than the minimum size, if elements are already too small
- if min_size - 1. > size(preceding_ix, flexes.as_slice()) {
- return;
- }
+ ix_offset += 1;
- let mut proposed_current_pixel_change = (drag.position - child_start).along(axis)
- - size(preceding_ix, flexes.as_slice());
+ result
+ }
+ });
- let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| {
- let flex_change = pixel_dx / drag_bounds.length_along(axis);
- let current_target_flex = flexes[target_ix] + flex_change;
- let next_target_flex =
- flexes[(target_ix as isize + next) as usize] - flex_change;
- (current_target_flex, next_target_flex)
+ while proposed_current_pixel_change.abs() > px(0.) {
+ let Some(current_ix) = successors.next() else {
+ break;
};
- let mut successors = from_fn({
- let forward = proposed_current_pixel_change > 0.;
- let mut ix_offset = 0;
- let len = flexes.len();
- move || {
- let result = if forward {
- (preceding_ix + 1 + ix_offset < len).then(|| preceding_ix + ix_offset)
- } else {
- (preceding_ix as isize - ix_offset as isize >= 0)
- .then(|| preceding_ix - ix_offset)
- };
-
- ix_offset += 1;
+ let next_target_size = Pixels::max(
+ size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change,
+ min_size,
+ );
- result
- }
- });
+ let current_target_size = Pixels::max(
+ size(current_ix, flexes.as_slice()) + size(current_ix + 1, flexes.as_slice())
+ - next_target_size,
+ min_size,
+ );
- while proposed_current_pixel_change.abs() > 0. {
- let Some(current_ix) = successors.next() else {
- break;
- };
+ let current_pixel_change =
+ current_target_size - size(current_ix, flexes.as_slice());
- let next_target_size = f32::max(
- size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change,
- min_size,
- );
+ let (current_target_flex, next_target_flex) =
+ flex_changes(current_pixel_change, current_ix, 1, flexes.as_slice());
- let current_target_size = f32::max(
- size(current_ix, flexes.as_slice())
- + size(current_ix + 1, flexes.as_slice())
- - next_target_size,
- min_size,
- );
+ flexes[current_ix] = current_target_flex;
+ flexes[current_ix + 1] = next_target_flex;
- let current_pixel_change =
- current_target_size - size(current_ix, flexes.as_slice());
+ proposed_current_pixel_change -= current_pixel_change;
+ }
- let (current_target_flex, next_target_flex) =
- flex_changes(current_pixel_change, current_ix, 1, flexes.as_slice());
+ // todo!(schedule serialize)
+ // workspace.schedule_serialize(cx);
+ cx.notify();
+ }
- flexes[current_ix] = current_target_flex;
- flexes[current_ix + 1] = next_target_flex;
+ fn push_handle(
+ flexes: Arc<Mutex<Vec<f32>>>,
+ dragged_handle: Rc<RefCell<Option<usize>>>,
+ axis: Axis,
+ ix: usize,
+ pane_bounds: Bounds<Pixels>,
+ axis_bounds: Bounds<Pixels>,
+ cx: &mut WindowContext,
+ ) {
+ let handle_bounds = Bounds {
+ origin: pane_bounds.origin.apply_along(axis, |origin| {
+ origin + pane_bounds.size.along(axis) - px(HANDLE_HITBOX_SIZE / 2.)
+ }),
+ size: pane_bounds
+ .size
+ .apply_along(axis, |_| px(HANDLE_HITBOX_SIZE)),
+ };
+ let divider_bounds = Bounds {
+ origin: pane_bounds
+ .origin
+ .apply_along(axis, |origin| origin + pane_bounds.size.along(axis)),
+ size: pane_bounds.size.apply_along(axis, |_| px(DIVIDER_SIZE)),
+ };
- proposed_current_pixel_change -= current_pixel_change;
+ cx.with_z_index(3, |cx| {
+ let interactive_handle_bounds = InteractiveBounds {
+ bounds: handle_bounds,
+ stacking_order: cx.stacking_order().clone(),
+ };
+ if interactive_handle_bounds.visibly_contains(&cx.mouse_position(), cx) {
+ cx.set_cursor_style(match axis {
+ Axis::Vertical => CursorStyle::ResizeUpDown,
+ Axis::Horizontal => CursorStyle::ResizeLeftRight,
+ })
}
- workspace.schedule_serialize(cx);
- cx.notify();
- }
- }
- }
+ cx.add_opaque_layer(handle_bounds);
+ cx.paint_quad(gpui::fill(divider_bounds, cx.theme().colors().border));
- impl Extend<AnyElement<Workspace>> for PaneAxisElement {
- fn extend<T: IntoIterator<Item = AnyElement<Workspace>>>(&mut self, children: T) {
- self.children.extend(children);
+ cx.on_mouse_event({
+ let dragged_handle = dragged_handle.clone();
+ move |e: &MouseDownEvent, phase, _cx| {
+ if phase.bubble() && handle_bounds.contains(&e.position) {
+ dragged_handle.replace(Some(ix));
+ }
+ }
+ });
+ cx.on_mouse_event(move |e: &MouseMoveEvent, phase, cx| {
+ let dragged_handle = dragged_handle.borrow();
+ if phase.bubble() && *dragged_handle == Some(ix) {
+ Self::compute_resize(
+ &flexes,
+ e,
+ ix,
+ axis,
+ pane_bounds.origin,
+ axis_bounds.size,
+ cx,
+ )
+ }
+ });
+ });
}
}
- impl Element<Workspace> for PaneAxisElement {
- type LayoutState = f32;
- type PaintState = ();
-
- fn layout(
- &mut self,
- constraint: SizeConstraint,
- view: &mut Workspace,
- cx: &mut ViewContext<Workspace>,
- ) -> (Vector2F, Self::LayoutState) {
- debug_assert!(self.children.len() == self.flexes.borrow().len());
-
- let active_pane_magnification =
- settings::get::<WorkspaceSettings>(cx).active_pane_magnification;
-
- let mut remaining_flex = 0.;
-
- if active_pane_magnification != 1. {
- let active_pane_flex = self
- .active_pane_ix
- .map(|_| active_pane_magnification)
- .unwrap_or(1.);
- remaining_flex += self.children.len() as f32 - 1. + active_pane_flex;
- } else {
- for flex in self.flexes.borrow().iter() {
- remaining_flex += flex;
- }
- }
-
- let mut cross_axis_max: f32 = 0.0;
- let mut remaining_space = constraint.max_along(self.axis);
-
- if remaining_space.is_infinite() {
- panic!("flex contains flexible children but has an infinite constraint along the flex axis");
- }
+ impl IntoElement for PaneAxisElement {
+ type Element = Self;
- self.layout_children(
- active_pane_magnification,
- constraint,
- &mut remaining_space,
- &mut remaining_flex,
- &mut cross_axis_max,
- view,
- cx,
- );
-
- let mut size = match self.axis {
- Axis::Horizontal => vec2f(constraint.max.x() - remaining_space, cross_axis_max),
- Axis::Vertical => vec2f(cross_axis_max, constraint.max.y() - remaining_space),
- };
+ fn element_id(&self) -> Option<ui::prelude::ElementId> {
+ Some(self.basis.into())
+ }
- if constraint.min.x().is_finite() {
- size.set_x(size.x().max(constraint.min.x()));
- }
- if constraint.min.y().is_finite() {
- size.set_y(size.y().max(constraint.min.y()));
- }
+ fn into_element(self) -> Self::Element {
+ self
+ }
+ }
- if size.x() > constraint.max.x() {
- size.set_x(constraint.max.x());
- }
- if size.y() > constraint.max.y() {
- size.set_y(constraint.max.y());
- }
+ impl Element for PaneAxisElement {
+ type State = Rc<RefCell<Option<usize>>>;
- (size, remaining_space)
+ fn request_layout(
+ &mut self,
+ state: Option<Self::State>,
+ cx: &mut ui::prelude::WindowContext,
+ ) -> (gpui::LayoutId, Self::State) {
+ let mut style = Style::default();
+ style.flex_grow = 1.;
+ style.flex_shrink = 1.;
+ style.flex_basis = relative(0.).into();
+ style.size.width = relative(1.).into();
+ style.size.height = relative(1.).into();
+ let layout_id = cx.request_layout(&style, None);
+ let dragged_pane = state.unwrap_or_else(|| Rc::new(RefCell::new(None)));
+ (layout_id, dragged_pane)
}
fn paint(
&mut self,
- bounds: RectF,
- visible_bounds: RectF,
- remaining_space: &mut Self::LayoutState,
- view: &mut Workspace,
- cx: &mut ViewContext<Workspace>,
- ) -> Self::PaintState {
- let can_resize = settings::get::<WorkspaceSettings>(cx).active_pane_magnification == 1.;
- let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
-
- let overflowing = *remaining_space < 0.;
- if overflowing {
- cx.scene().push_layer(Some(visible_bounds));
- }
+ bounds: gpui::Bounds<ui::prelude::Pixels>,
+ state: &mut Self::State,
+ cx: &mut ui::prelude::WindowContext,
+ ) {
+ let flexes = self.flexes.lock().clone();
+ let len = self.children.len();
+ debug_assert!(flexes.len() == len);
+ debug_assert!(flex_values_in_bounds(flexes.as_slice()));
- let mut child_origin = bounds.origin();
+ let mut origin = bounds.origin;
+ let space_per_flex = bounds.size.along(self.axis) / len as f32;
- let mut bounding_boxes = self.bounding_boxes.borrow_mut();
+ let mut bounding_boxes = self.bounding_boxes.lock();
bounding_boxes.clear();
- let mut children_iter = self.children.iter_mut().enumerate().peekable();
- while let Some((ix, child)) = children_iter.next() {
- let child_start = child_origin.clone();
- child.paint(child_origin, visible_bounds, view, cx);
-
- bounding_boxes.push(Some(RectF::new(child_origin, child.size())));
-
- match self.axis {
- Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
- Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
- }
-
- if can_resize && children_iter.peek().is_some() {
- cx.scene().push_stacking_context(None, None);
-
- let handle_origin = match self.axis {
- Axis::Horizontal => child_origin - vec2f(HANDLE_HITBOX_SIZE / 2., 0.0),
- Axis::Vertical => child_origin - vec2f(0.0, HANDLE_HITBOX_SIZE / 2.),
- };
-
- let handle_bounds = match self.axis {
- Axis::Horizontal => RectF::new(
- handle_origin,
- vec2f(HANDLE_HITBOX_SIZE, visible_bounds.height()),
- ),
- Axis::Vertical => RectF::new(
- handle_origin,
- vec2f(visible_bounds.width(), HANDLE_HITBOX_SIZE),
- ),
- };
-
- let style = match self.axis {
- Axis::Horizontal => CursorStyle::ResizeLeftRight,
- Axis::Vertical => CursorStyle::ResizeUpDown,
- };
-
- cx.scene().push_cursor_region(CursorRegion {
- bounds: handle_bounds,
- style,
- });
-
- enum ResizeHandle {}
- let mut mouse_region = MouseRegion::new::<ResizeHandle>(
- cx.view_id(),
- self.basis + ix,
- handle_bounds,
- );
- mouse_region = mouse_region
- .on_drag(
- MouseButton::Left,
- Self::handle_resize(
- self.flexes.clone(),
- self.axis,
- ix,
- child_start,
- visible_bounds.clone(),
- ),
- )
- .on_click(MouseButton::Left, {
- let flexes = self.flexes.clone();
- move |e, v: &mut Workspace, cx| {
- if e.click_count >= 2 {
- let mut borrow = flexes.borrow_mut();
- *borrow = vec![1.; borrow.len()];
- v.schedule_serialize(cx);
- cx.notify();
- }
- }
- });
- cx.scene().push_mouse_region(mouse_region);
+ for (ix, child) in self.children.iter_mut().enumerate() {
+ //todo!(active_pane_magnification)
+ // If using active pane magnification, need to switch to using
+ // 1 for all non-active panes, and then the magnification for the
+ // active pane.
+ let child_size = bounds
+ .size
+ .apply_along(self.axis, |_| space_per_flex * flexes[ix]);
+
+ let child_bounds = Bounds {
+ origin,
+ size: child_size,
+ };
+ bounding_boxes.push(Some(child_bounds));
+ cx.with_z_index(0, |cx| {
+ child.draw(origin, child_size.into(), cx);
+ });
+ cx.with_z_index(1, |cx| {
+ if ix < len - 1 {
+ Self::push_handle(
+ self.flexes.clone(),
+ state.clone(),
+ self.axis,
+ ix,
+ child_bounds,
+ bounds,
+ cx,
+ );
+ }
+ });
- cx.scene().pop_stacking_context();
- }
+ origin = origin.apply_along(self.axis, |val| val + child_size.along(self.axis));
}
- if overflowing {
- cx.scene().pop_layer();
- }
+ cx.with_z_index(1, |cx| {
+ cx.on_mouse_event({
+ let state = state.clone();
+ move |_: &MouseUpEvent, phase, _cx| {
+ if phase.bubble() {
+ state.replace(None);
+ }
+ }
+ });
+ })
}
+ }
- fn rect_for_text_range(
- &self,
- range_utf16: Range<usize>,
- _: RectF,
- _: RectF,
- _: &Self::LayoutState,
- _: &Self::PaintState,
- view: &Workspace,
- cx: &ViewContext<Workspace>,
- ) -> Option<RectF> {
- self.children
- .iter()
- .find_map(|child| child.rect_for_text_range(range_utf16.clone(), view, cx))
+ impl ParentElement for PaneAxisElement {
+ fn children_mut(&mut self) -> &mut smallvec::SmallVec<[AnyElement; 2]> {
+ &mut self.children
}
+ }
- fn debug(
- &self,
- bounds: RectF,
- _: &Self::LayoutState,
- _: &Self::PaintState,
- view: &Workspace,
- cx: &ViewContext<Workspace>,
- ) -> json::Value {
- serde_json::json!({
- "type": "PaneAxis",
- "bounds": bounds.to_json(),
- "axis": self.axis.to_json(),
- "flexes": *self.flexes.borrow(),
- "children": self.children.iter().map(|child| child.debug(view, cx)).collect::<Vec<json::Value>>()
- })
- }
+ fn flex_values_in_bounds(flexes: &[f32]) -> bool {
+ (flexes.iter().copied().sum::<f32>() - flexes.len() as f32).abs() < 0.001
}
}
@@ -1,4 +1,4 @@
-#![allow(dead_code)]
+//#![allow(dead_code)]
pub mod model;
@@ -6,7 +6,7 @@ use std::path::Path;
use anyhow::{anyhow, bail, Context, Result};
use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
-use gpui::{platform::WindowBounds, Axis};
+use gpui::{Axis, WindowBounds};
use util::{unzip_option, ResultExt};
use uuid::Uuid;
@@ -403,7 +403,7 @@ impl WorkspaceDb {
.map(|(group_id, axis, pane_id, active, flexes)| {
if let Some((group_id, axis)) = group_id.zip(axis) {
let flexes = flexes
- .map(|flexes| serde_json::from_str::<Vec<f32>>(&flexes))
+ .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
.transpose()?;
Ok(SerializedPaneGroup::Group {
@@ -553,6 +553,7 @@ impl WorkspaceDb {
mod tests {
use super::*;
use db::open_test_db;
+ use gpui;
#[gpui::test]
async fn test_next_id_stability() {
@@ -612,13 +613,13 @@ mod tests {
conn.migrate(
"test_table",
&[sql!(
- CREATE TABLE test_table(
- text TEXT,
- workspace_id INTEGER,
- FOREIGN KEY(workspace_id)
- REFERENCES workspaces(workspace_id)
- ON DELETE CASCADE
- ) STRICT;)],
+ CREATE TABLE test_table(
+ text TEXT,
+ workspace_id INTEGER,
+ FOREIGN KEY(workspace_id)
+ REFERENCES workspaces(workspace_id)
+ ON DELETE CASCADE
+ ) STRICT;)],
)
})
.await
@@ -680,7 +681,7 @@ mod tests {
assert_eq!(test_text_1, "test-text-1");
}
- fn group(axis: gpui::Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
+ fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
SerializedPaneGroup::Group {
axis,
flexes: None,
@@ -700,10 +701,10 @@ mod tests {
// | 3,4 | |
// -----------------
let center_group = group(
- gpui::Axis::Horizontal,
+ Axis::Horizontal,
vec![
group(
- gpui::Axis::Vertical,
+ Axis::Vertical,
vec![
SerializedPaneGroup::Pane(SerializedPane::new(
vec![
@@ -859,10 +860,10 @@ mod tests {
// | 3,4 | |
// -----------------
let center_pane = group(
- gpui::Axis::Horizontal,
+ Axis::Horizontal,
vec![
group(
- gpui::Axis::Vertical,
+ Axis::Vertical,
vec![
SerializedPaneGroup::Pane(SerializedPane::new(
vec![
@@ -906,10 +907,10 @@ mod tests {
let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
let center_pane = group(
- gpui::Axis::Horizontal,
+ Axis::Horizontal,
vec![
group(
- gpui::Axis::Vertical,
+ Axis::Vertical,
vec![
SerializedPaneGroup::Pane(SerializedPane::new(
vec![
@@ -944,7 +945,7 @@ mod tests {
db.save_workspace(workspace.clone()).await;
workspace.center_group = group(
- gpui::Axis::Vertical,
+ Axis::Vertical,
vec![
SerializedPaneGroup::Pane(SerializedPane::new(
vec![
@@ -5,9 +5,7 @@ use db::sqlez::{
bindable::{Bind, Column, StaticColumnCount},
statement::Statement,
};
-use gpui::{
- platform::WindowBounds, AsyncAppContext, Axis, ModelHandle, Task, ViewHandle, WeakViewHandle,
-};
+use gpui::{AsyncWindowContext, Axis, Model, Task, View, WeakView, WindowBounds};
use project::Project;
use std::{
path::{Path, PathBuf},
@@ -151,15 +149,11 @@ impl SerializedPaneGroup {
#[async_recursion(?Send)]
pub(crate) async fn deserialize(
self,
- project: &ModelHandle<Project>,
+ project: &Model<Project>,
workspace_id: WorkspaceId,
- workspace: &WeakViewHandle<Workspace>,
- cx: &mut AsyncAppContext,
- ) -> Option<(
- Member,
- Option<ViewHandle<Pane>>,
- Vec<Option<Box<dyn ItemHandle>>>,
- )> {
+ workspace: WeakView<Workspace>,
+ cx: &mut AsyncWindowContext,
+ ) -> Option<(Member, Option<View<Pane>>, Vec<Option<Box<dyn ItemHandle>>>)> {
match self {
SerializedPaneGroup::Group {
axis,
@@ -171,7 +165,7 @@ impl SerializedPaneGroup {
let mut items = Vec::new();
for child in children {
if let Some((new_member, active_pane, new_items)) = child
- .deserialize(project, workspace_id, workspace, cx)
+ .deserialize(project, workspace_id, workspace.clone(), cx)
.await
{
members.push(new_member);
@@ -200,18 +194,15 @@ impl SerializedPaneGroup {
.log_err()?;
let active = serialized_pane.active;
let new_items = serialized_pane
- .deserialize_to(project, &pane, workspace_id, workspace, cx)
+ .deserialize_to(project, &pane, workspace_id, workspace.clone(), cx)
.await
.log_err()?;
- if pane
- .read_with(cx, |pane, _| pane.items_len() != 0)
- .log_err()?
- {
- let pane = pane.upgrade(cx)?;
+ if pane.update(cx, |pane, _| pane.items_len() != 0).log_err()? {
+ let pane = pane.upgrade()?;
Some((Member::Pane(pane.clone()), active.then(|| pane), new_items))
} else {
- let pane = pane.upgrade(cx)?;
+ let pane = pane.upgrade()?;
workspace
.update(cx, |workspace, cx| workspace.force_remove_pane(&pane, cx))
.log_err()?;
@@ -235,11 +226,11 @@ impl SerializedPane {
pub async fn deserialize_to(
&self,
- project: &ModelHandle<Project>,
- pane: &WeakViewHandle<Pane>,
+ project: &Model<Project>,
+ pane: &WeakView<Pane>,
workspace_id: WorkspaceId,
- workspace: &WeakViewHandle<Workspace>,
- cx: &mut AsyncAppContext,
+ workspace: WeakView<Workspace>,
+ cx: &mut AsyncWindowContext,
) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
let mut items = Vec::new();
let mut active_item_index = None;
@@ -284,7 +275,7 @@ impl SerializedPane {
pub type GroupId = i64;
pub type PaneId = i64;
-pub type ItemId = usize;
+pub type ItemId = u64;
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct SerializedItem {
@@ -1,12 +1,15 @@
use std::{any::Any, sync::Arc};
use gpui::{
- AnyViewHandle, AnyWeakViewHandle, AppContext, Subscription, Task, ViewContext, ViewHandle,
- WeakViewHandle, WindowContext,
+ AnyView, AppContext, EventEmitter, Subscription, Task, View, ViewContext, WeakView,
+ WindowContext,
};
use project::search::SearchQuery;
-use crate::{item::WeakItemHandle, Item, ItemHandle};
+use crate::{
+ item::{Item, WeakItemHandle},
+ ItemHandle,
+};
#[derive(Debug)]
pub enum SearchEvent {
@@ -29,7 +32,7 @@ pub struct SearchOptions {
pub replacement: bool,
}
-pub trait SearchableItem: Item {
+pub trait SearchableItem: Item + EventEmitter<SearchEvent> {
type Match: Any + Sync + Send + Clone;
fn supported_options() -> SearchOptions {
@@ -40,11 +43,7 @@ pub trait SearchableItem: Item {
replacement: true,
}
}
- fn to_search_event(
- &mut self,
- event: &Self::Event,
- cx: &mut ViewContext<Self>,
- ) -> Option<SearchEvent>;
+
fn clear_matches(&mut self, cx: &mut ViewContext<Self>);
fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>);
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String;
@@ -95,7 +94,7 @@ pub trait SearchableItemHandle: ItemHandle {
fn subscribe_to_search_events(
&self,
cx: &mut WindowContext,
- handler: Box<dyn Fn(SearchEvent, &mut WindowContext)>,
+ handler: Box<dyn Fn(&SearchEvent, &mut WindowContext) + Send>,
) -> Subscription;
fn clear_matches(&self, cx: &mut WindowContext);
fn update_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut WindowContext);
@@ -128,7 +127,8 @@ pub trait SearchableItemHandle: ItemHandle {
) -> Option<usize>;
}
-impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
+// todo!("here is where we need to use AnyWeakView");
+impl<T: SearchableItem> SearchableItemHandle for View<T> {
fn downgrade(&self) -> Box<dyn WeakSearchableItemHandle> {
Box::new(self.downgrade())
}
@@ -144,14 +144,9 @@ impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
fn subscribe_to_search_events(
&self,
cx: &mut WindowContext,
- handler: Box<dyn Fn(SearchEvent, &mut WindowContext)>,
+ handler: Box<dyn Fn(&SearchEvent, &mut WindowContext) + Send>,
) -> Subscription {
- cx.subscribe(self, move |handle, event, cx| {
- let search_event = handle.update(cx, |handle, cx| handle.to_search_event(event, cx));
- if let Some(search_event) = search_event {
- handler(search_event, cx)
- }
- })
+ cx.subscribe(self, move |_, event: &SearchEvent, cx| handler(event, cx))
}
fn clear_matches(&self, cx: &mut WindowContext) {
@@ -198,7 +193,7 @@ impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
cx: &mut WindowContext,
) -> Task<Vec<Box<dyn Any + Send>>> {
let matches = self.update(cx, |this, cx| this.find_matches(query, cx));
- cx.foreground().spawn(async {
+ cx.spawn(|_| async {
let matches = matches.await;
matches
.into_iter()
@@ -231,21 +226,21 @@ fn downcast_matches<T: Any + Clone>(matches: &Vec<Box<dyn Any + Send>>) -> Vec<T
)
}
-impl From<Box<dyn SearchableItemHandle>> for AnyViewHandle {
+impl From<Box<dyn SearchableItemHandle>> for AnyView {
fn from(this: Box<dyn SearchableItemHandle>) -> Self {
- this.as_any().clone()
+ this.to_any().clone()
}
}
-impl From<&Box<dyn SearchableItemHandle>> for AnyViewHandle {
+impl From<&Box<dyn SearchableItemHandle>> for AnyView {
fn from(this: &Box<dyn SearchableItemHandle>) -> Self {
- this.as_any().clone()
+ this.to_any().clone()
}
}
impl PartialEq for Box<dyn SearchableItemHandle> {
fn eq(&self, other: &Self) -> bool {
- self.id() == other.id() && self.window() == other.window()
+ self.item_id() == other.item_id()
}
}
@@ -254,22 +249,22 @@ impl Eq for Box<dyn SearchableItemHandle> {}
pub trait WeakSearchableItemHandle: WeakItemHandle {
fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
- fn into_any(self) -> AnyWeakViewHandle;
+ // fn into_any(self) -> AnyWeakView;
}
-impl<T: SearchableItem> WeakSearchableItemHandle for WeakViewHandle<T> {
- fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
- Some(Box::new(self.upgrade(cx)?))
+impl<T: SearchableItem> WeakSearchableItemHandle for WeakView<T> {
+ fn upgrade(&self, _cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
+ Some(Box::new(self.upgrade()?))
}
- fn into_any(self) -> AnyWeakViewHandle {
- self.into_any()
- }
+ // fn into_any(self) -> AnyView {
+ // self.into_any()
+ // }
}
impl PartialEq for Box<dyn WeakSearchableItemHandle> {
fn eq(&self, other: &Self) -> bool {
- self.id() == other.id() && self.window() == other.window()
+ self.id() == other.id()
}
}
@@ -277,6 +272,6 @@ impl Eq for Box<dyn WeakSearchableItemHandle> {}
impl std::hash::Hash for Box<dyn WeakSearchableItemHandle> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
- (self.id(), self.window().id()).hash(state)
+ self.id().hash(state)
}
}
@@ -7,16 +7,12 @@ use call::participant::{Frame, RemoteVideoTrack};
use client::{proto::PeerId, User};
use futures::StreamExt;
use gpui::{
- elements::*,
- geometry::{rect::RectF, vector::vec2f},
- platform::MouseButton,
- AppContext, Entity, Task, View, ViewContext,
-};
-use smallvec::SmallVec;
-use std::{
- borrow::Cow,
- sync::{Arc, Weak},
+ div, img, AppContext, Element, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
+ ParentElement, Render, SharedString, Styled, Task, View, ViewContext, VisualContext,
+ WindowContext,
};
+use std::sync::{Arc, Weak};
+use ui::{h_stack, prelude::*, Icon, IconElement, Label};
pub enum Event {
Close,
@@ -29,6 +25,7 @@ pub struct SharedScreen {
user: Arc<User>,
nav_history: Option<ItemNavHistory>,
_maintain_frame: Task<Result<()>>,
+ focus: FocusHandle,
}
impl SharedScreen {
@@ -38,6 +35,7 @@ impl SharedScreen {
user: Arc<User>,
cx: &mut ViewContext<Self>,
) -> Self {
+ cx.focus_handle();
let mut frames = track.frames();
Self {
track: Arc::downgrade(track),
@@ -55,77 +53,56 @@ impl SharedScreen {
this.update(&mut cx, |_, cx| cx.emit(Event::Close))?;
Ok(())
}),
+ focus: cx.focus_handle(),
}
}
}
-impl Entity for SharedScreen {
- type Event = Event;
-}
+impl EventEmitter<Event> for SharedScreen {}
-impl View for SharedScreen {
- fn ui_name() -> &'static str {
- "SharedScreen"
+impl FocusableView for SharedScreen {
+ fn focus_handle(&self, _: &AppContext) -> FocusHandle {
+ self.focus.clone()
}
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- enum Focus {}
-
- let frame = self.frame.clone();
- MouseEventHandler::new::<Focus, _>(0, cx, |_, cx| {
- Canvas::new(move |bounds, _, _, cx| {
- if let Some(frame) = frame.clone() {
- let size = constrain_size_preserving_aspect_ratio(
- bounds.size(),
- vec2f(frame.width() as f32, frame.height() as f32),
- );
- let origin = bounds.origin() + (bounds.size() / 2.) - size / 2.;
- cx.scene().push_surface(gpui::platform::mac::Surface {
- bounds: RectF::new(origin, size),
- image_buffer: frame.image(),
- });
- }
- })
- .contained()
- .with_style(theme::current(cx).shared_screen)
- })
- .on_down(MouseButton::Left, |_, _, cx| cx.focus_parent())
- .into_any()
+}
+impl Render for SharedScreen {
+ fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
+ div().track_focus(&self.focus).size_full().children(
+ self.frame
+ .as_ref()
+ .map(|frame| img(frame.image()).size_full()),
+ )
}
}
impl Item for SharedScreen {
- fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
+ type Event = Event;
+
+ fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
Some(format!("{}'s screen", self.user.github_login).into())
}
+
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
if let Some(nav_history) = self.nav_history.as_mut() {
nav_history.push::<()>(None, cx);
}
}
- fn tab_content<V: 'static>(
+ fn tab_content(
&self,
_: Option<usize>,
- style: &theme::Tab,
- _: &AppContext,
- ) -> gpui::AnyElement<V> {
- Flex::row()
- .with_child(
- Svg::new("icons/desktop.svg")
- .with_color(style.label.text.color)
- .constrained()
- .with_width(style.type_icon_width)
- .aligned()
- .contained()
- .with_margin_right(style.spacing),
- )
- .with_child(
- Label::new(
- format!("{}'s screen", self.user.github_login),
- style.label.clone(),
- )
- .aligned(),
+ selected: bool,
+ _: &WindowContext<'_>,
+ ) -> gpui::AnyElement {
+ h_stack()
+ .gap_1()
+ .child(IconElement::new(Icon::Screen))
+ .child(
+ Label::new(format!("{}'s screen", self.user.github_login)).color(if selected {
+ Color::Default
+ } else {
+ Color::Muted
+ }),
)
.into_any()
}
@@ -138,14 +115,14 @@ impl Item for SharedScreen {
&self,
_workspace_id: WorkspaceId,
cx: &mut ViewContext<Self>,
- ) -> Option<Self> {
+ ) -> Option<View<Self>> {
let track = self.track.upgrade()?;
- Some(Self::new(&track, self.peer_id, self.user.clone(), cx))
+ Some(cx.new_view(|cx| Self::new(&track, self.peer_id, self.user.clone(), cx)))
}
- fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
+ fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
match event {
- Event::Close => smallvec::smallvec!(ItemEvent::CloseItem),
+ Event::Close => f(ItemEvent::CloseItem),
}
}
}
@@ -1,18 +1,13 @@
-use std::ops::Range;
-
use crate::{ItemHandle, Pane};
use gpui::{
- elements::*,
- geometry::{
- rect::RectF,
- vector::{vec2f, Vector2F},
- },
- json::{json, ToJson},
- AnyElement, AnyViewHandle, Entity, SizeConstraint, Subscription, View, ViewContext, ViewHandle,
+ div, AnyView, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext,
WindowContext,
};
+use std::any::TypeId;
+use ui::{h_stack, prelude::*};
+use util::ResultExt;
-pub trait StatusItemView: View {
+pub trait StatusItemView: Render {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn crate::ItemHandle>,
@@ -20,63 +15,57 @@ pub trait StatusItemView: View {
);
}
-trait StatusItemViewHandle {
- fn as_any(&self) -> &AnyViewHandle;
+trait StatusItemViewHandle: Send {
+ fn to_any(&self) -> AnyView;
fn set_active_pane_item(
&self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut WindowContext,
);
- fn ui_name(&self) -> &'static str;
+ fn item_type(&self) -> TypeId;
}
pub struct StatusBar {
left_items: Vec<Box<dyn StatusItemViewHandle>>,
right_items: Vec<Box<dyn StatusItemViewHandle>>,
- active_pane: ViewHandle<Pane>,
+ active_pane: View<Pane>,
_observe_active_pane: Subscription,
}
-impl Entity for StatusBar {
- type Event = ();
+impl Render for StatusBar {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ div()
+ .py_0p5()
+ .px_1()
+ .flex()
+ .items_center()
+ .justify_between()
+ .w_full()
+ .h_8()
+ .bg(cx.theme().colors().status_bar_background)
+ .child(self.render_left_tools(cx))
+ .child(self.render_right_tools(cx))
+ }
}
-impl View for StatusBar {
- fn ui_name() -> &'static str {
- "StatusBar"
+impl StatusBar {
+ fn render_left_tools(&self, _: &mut ViewContext<Self>) -> impl IntoElement {
+ h_stack()
+ .items_center()
+ .gap_2()
+ .children(self.left_items.iter().map(|item| item.to_any()))
}
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let theme = &theme::current(cx).workspace.status_bar;
-
- StatusBarElement {
- left: Flex::row()
- .with_children(self.left_items.iter().map(|i| {
- ChildView::new(i.as_any(), cx)
- .aligned()
- .contained()
- .with_margin_right(theme.item_spacing)
- }))
- .into_any(),
- right: Flex::row()
- .with_children(self.right_items.iter().rev().map(|i| {
- ChildView::new(i.as_any(), cx)
- .aligned()
- .contained()
- .with_margin_left(theme.item_spacing)
- }))
- .into_any(),
- }
- .contained()
- .with_style(theme.container)
- .constrained()
- .with_height(theme.height)
- .into_any()
+ fn render_right_tools(&self, _: &mut ViewContext<Self>) -> impl IntoElement {
+ h_stack()
+ .items_center()
+ .gap_2()
+ .children(self.right_items.iter().rev().map(|item| item.to_any()))
}
}
impl StatusBar {
- pub fn new(active_pane: &ViewHandle<Pane>, cx: &mut ViewContext<Self>) -> Self {
+ pub fn new(active_pane: &View<Pane>, cx: &mut ViewContext<Self>) -> Self {
let mut this = Self {
left_items: Default::default(),
right_items: Default::default(),
@@ -88,19 +77,22 @@ impl StatusBar {
this
}
- pub fn add_left_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
+ pub fn add_left_item<T>(&mut self, item: View<T>, cx: &mut ViewContext<Self>)
where
T: 'static + StatusItemView,
{
+ let active_pane_item = self.active_pane.read(cx).active_item();
+ item.set_active_pane_item(active_pane_item.as_deref(), cx);
+
self.left_items.push(Box::new(item));
cx.notify();
}
- pub fn item_of_type<T: StatusItemView>(&self) -> Option<ViewHandle<T>> {
+ pub fn item_of_type<T: StatusItemView>(&self) -> Option<View<T>> {
self.left_items
.iter()
.chain(self.right_items.iter())
- .find_map(|item| item.as_any().clone().downcast())
+ .find_map(|item| item.to_any().clone().downcast().log_err())
}
pub fn position_of_item<T>(&self) -> Option<usize>
@@ -108,12 +100,12 @@ impl StatusBar {
T: StatusItemView,
{
for (index, item) in self.left_items.iter().enumerate() {
- if item.as_ref().ui_name() == T::ui_name() {
+ if item.item_type() == TypeId::of::<T>() {
return Some(index);
}
}
for (index, item) in self.right_items.iter().enumerate() {
- if item.as_ref().ui_name() == T::ui_name() {
+ if item.item_type() == TypeId::of::<T>() {
return Some(index + self.left_items.len());
}
}
@@ -123,11 +115,14 @@ impl StatusBar {
pub fn insert_item_after<T>(
&mut self,
position: usize,
- item: ViewHandle<T>,
+ item: View<T>,
cx: &mut ViewContext<Self>,
) where
T: 'static + StatusItemView,
{
+ let active_pane_item = self.active_pane.read(cx).active_item();
+ item.set_active_pane_item(active_pane_item.as_deref(), cx);
+
if position < self.left_items.len() {
self.left_items.insert(position + 1, Box::new(item))
} else {
@@ -146,15 +141,18 @@ impl StatusBar {
cx.notify();
}
- pub fn add_right_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
+ pub fn add_right_item<T>(&mut self, item: View<T>, cx: &mut ViewContext<Self>)
where
T: 'static + StatusItemView,
{
+ let active_pane_item = self.active_pane.read(cx).active_item();
+ item.set_active_pane_item(active_pane_item.as_deref(), cx);
+
self.right_items.push(Box::new(item));
cx.notify();
}
- pub fn set_active_pane(&mut self, active_pane: &ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
+ pub fn set_active_pane(&mut self, active_pane: &View<Pane>, cx: &mut ViewContext<Self>) {
self.active_pane = active_pane.clone();
self._observe_active_pane =
cx.observe(active_pane, |this, _, cx| this.update_active_pane_item(cx));
@@ -169,9 +167,9 @@ impl StatusBar {
}
}
-impl<T: StatusItemView> StatusItemViewHandle for ViewHandle<T> {
- fn as_any(&self) -> &AnyViewHandle {
- self
+impl<T: StatusItemView> StatusItemViewHandle for View<T> {
+ fn to_any(&self) -> AnyView {
+ self.clone().into()
}
fn set_active_pane_item(
@@ -184,88 +182,13 @@ impl<T: StatusItemView> StatusItemViewHandle for ViewHandle<T> {
});
}
- fn ui_name(&self) -> &'static str {
- T::ui_name()
+ fn item_type(&self) -> TypeId {
+ TypeId::of::<T>()
}
}
-impl From<&dyn StatusItemViewHandle> for AnyViewHandle {
+impl From<&dyn StatusItemViewHandle> for AnyView {
fn from(val: &dyn StatusItemViewHandle) -> Self {
- val.as_any().clone()
- }
-}
-
-struct StatusBarElement {
- left: AnyElement<StatusBar>,
- right: AnyElement<StatusBar>,
-}
-
-impl Element<StatusBar> for StatusBarElement {
- type LayoutState = ();
- type PaintState = ();
-
- fn layout(
- &mut self,
- mut constraint: SizeConstraint,
- view: &mut StatusBar,
- cx: &mut ViewContext<StatusBar>,
- ) -> (Vector2F, Self::LayoutState) {
- let max_width = constraint.max.x();
- constraint.min = vec2f(0., constraint.min.y());
-
- let right_size = self.right.layout(constraint, view, cx);
- let constraint = SizeConstraint::new(
- vec2f(0., constraint.min.y()),
- vec2f(max_width - right_size.x(), constraint.max.y()),
- );
-
- self.left.layout(constraint, view, cx);
-
- (vec2f(max_width, right_size.y()), ())
- }
-
- fn paint(
- &mut self,
- bounds: RectF,
- visible_bounds: RectF,
- _: &mut Self::LayoutState,
- view: &mut StatusBar,
- cx: &mut ViewContext<StatusBar>,
- ) -> Self::PaintState {
- let origin_y = bounds.upper_right().y();
- let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
-
- let left_origin = vec2f(bounds.lower_left().x(), origin_y);
- self.left.paint(left_origin, visible_bounds, view, cx);
-
- let right_origin = vec2f(bounds.upper_right().x() - self.right.size().x(), origin_y);
- self.right.paint(right_origin, visible_bounds, view, cx);
- }
-
- fn rect_for_text_range(
- &self,
- _: Range<usize>,
- _: RectF,
- _: RectF,
- _: &Self::LayoutState,
- _: &Self::PaintState,
- _: &StatusBar,
- _: &ViewContext<StatusBar>,
- ) -> Option<RectF> {
- None
- }
-
- fn debug(
- &self,
- bounds: RectF,
- _: &Self::LayoutState,
- _: &Self::PaintState,
- _: &StatusBar,
- _: &ViewContext<StatusBar>,
- ) -> serde_json::Value {
- json!({
- "type": "StatusBarElement",
- "bounds": bounds.to_json()
- })
+ val.to_any().clone()
}
}
@@ -1,38 +1,35 @@
use crate::ItemHandle;
use gpui::{
- elements::*, AnyElement, AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle,
+ AnyView, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View, ViewContext,
WindowContext,
};
+use ui::prelude::*;
+use ui::{h_stack, v_stack};
-pub trait ToolbarItemView: View {
+pub enum ToolbarItemEvent {
+ ChangeLocation(ToolbarItemLocation),
+}
+
+pub trait ToolbarItemView: Render + EventEmitter<ToolbarItemEvent> {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn crate::ItemHandle>,
cx: &mut ViewContext<Self>,
) -> ToolbarItemLocation;
- fn location_for_event(
- &self,
- _event: &Self::Event,
- current_location: ToolbarItemLocation,
- _cx: &AppContext,
- ) -> ToolbarItemLocation {
- current_location
- }
-
fn pane_focus_update(&mut self, _pane_focused: bool, _cx: &mut ViewContext<Self>) {}
/// Number of times toolbar's height will be repeated to get the effective height.
/// Useful when multiple rows one under each other are needed.
/// The rows have the same width and act as a whole when reacting to resizes and similar events.
- fn row_count(&self, _cx: &ViewContext<Self>) -> usize {
+ fn row_count(&self, _cx: &WindowContext) -> usize {
1
}
}
-trait ToolbarItemViewHandle {
- fn id(&self) -> usize;
- fn as_any(&self) -> &AnyViewHandle;
+trait ToolbarItemViewHandle: Send {
+ fn id(&self) -> EntityId;
+ fn to_any(&self) -> AnyView;
fn set_active_pane_item(
&self,
active_pane_item: Option<&dyn ItemHandle>,
@@ -45,8 +42,8 @@ trait ToolbarItemViewHandle {
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum ToolbarItemLocation {
Hidden,
- PrimaryLeft { flex: Option<(f32, bool)> },
- PrimaryRight { flex: Option<(f32, bool)> },
+ PrimaryLeft,
+ PrimaryRight,
Secondary,
}
@@ -57,140 +54,161 @@ pub struct Toolbar {
items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
}
-impl Entity for Toolbar {
- type Event = ();
-}
-
-impl View for Toolbar {
- fn ui_name() -> &'static str {
- "Toolbar"
+impl Toolbar {
+ fn has_any_visible_items(&self) -> bool {
+ self.items
+ .iter()
+ .any(|(_item, location)| *location != ToolbarItemLocation::Hidden)
}
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let theme = &theme::current(cx).workspace.toolbar;
-
- let mut primary_left_items = Vec::new();
- let mut primary_right_items = Vec::new();
- let mut secondary_item = None;
- let spacing = theme.item_spacing;
- let mut primary_items_row_count = 1;
-
- for (item, position) in &self.items {
- match *position {
- ToolbarItemLocation::Hidden => {}
-
- ToolbarItemLocation::PrimaryLeft { flex } => {
- primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
- let left_item = ChildView::new(item.as_any(), cx).aligned();
- if let Some((flex, expanded)) = flex {
- primary_left_items.push(left_item.flex(flex, expanded).into_any());
- } else {
- primary_left_items.push(left_item.into_any());
- }
- }
-
- ToolbarItemLocation::PrimaryRight { flex } => {
- primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
- let right_item = ChildView::new(item.as_any(), cx).aligned().flex_float();
- if let Some((flex, expanded)) = flex {
- primary_right_items.push(right_item.flex(flex, expanded).into_any());
- } else {
- primary_right_items.push(right_item.into_any());
- }
- }
-
- ToolbarItemLocation::Secondary => {
- secondary_item = Some(
- ChildView::new(item.as_any(), cx)
- .constrained()
- .with_height(theme.height * item.row_count(cx) as f32)
- .into_any(),
- );
- }
+ fn left_items(&self) -> impl Iterator<Item = &dyn ToolbarItemViewHandle> {
+ self.items.iter().filter_map(|(item, location)| {
+ if *location == ToolbarItemLocation::PrimaryLeft {
+ Some(item.as_ref())
+ } else {
+ None
}
- }
+ })
+ }
- let container_style = theme.container;
- let height = theme.height * primary_items_row_count as f32;
+ fn right_items(&self) -> impl Iterator<Item = &dyn ToolbarItemViewHandle> {
+ self.items.iter().filter_map(|(item, location)| {
+ if *location == ToolbarItemLocation::PrimaryRight {
+ Some(item.as_ref())
+ } else {
+ None
+ }
+ })
+ }
- let mut primary_items = Flex::row().with_spacing(spacing);
- primary_items.extend(primary_left_items);
- primary_items.extend(primary_right_items);
+ fn secondary_items(&self) -> impl Iterator<Item = &dyn ToolbarItemViewHandle> {
+ self.items.iter().filter_map(|(item, location)| {
+ if *location == ToolbarItemLocation::Secondary {
+ Some(item.as_ref())
+ } else {
+ None
+ }
+ })
+ }
+}
- let mut toolbar = Flex::column();
- if !primary_items.is_empty() {
- toolbar.add_child(primary_items.constrained().with_height(height));
- }
- if let Some(secondary_item) = secondary_item {
- toolbar.add_child(secondary_item);
+impl Render for Toolbar {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ if !self.has_any_visible_items() {
+ return div();
}
- if toolbar.is_empty() {
- toolbar.into_any_named("toolbar")
- } else {
- toolbar
- .contained()
- .with_style(container_style)
- .into_any_named("toolbar")
- }
+ let secondary_item = self.secondary_items().next().map(|item| item.to_any());
+
+ let has_left_items = self.left_items().count() > 0;
+ let has_right_items = self.right_items().count() > 0;
+
+ v_stack()
+ .p_2()
+ .when(has_left_items || has_right_items, |this| this.gap_2())
+ .border_b()
+ .border_color(cx.theme().colors().border_variant)
+ .bg(cx.theme().colors().toolbar_background)
+ .child(
+ h_stack()
+ .justify_between()
+ .when(has_left_items, |this| {
+ this.child(
+ h_stack()
+ .flex_1()
+ .justify_start()
+ .children(self.left_items().map(|item| item.to_any())),
+ )
+ })
+ .when(has_right_items, |this| {
+ this.child(
+ h_stack()
+ .flex_1()
+ .justify_end()
+ .children(self.right_items().map(|item| item.to_any())),
+ )
+ }),
+ )
+ .children(secondary_item)
}
}
-// <<<<<<< HEAD
-// =======
-// #[allow(clippy::too_many_arguments)]
-// fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>)>(
-// svg_path: &'static str,
-// style: theme::Interactive<theme::IconButton>,
-// nav_button_height: f32,
-// tooltip_style: TooltipStyle,
-// enabled: bool,
-// spacing: f32,
-// on_click: F,
-// tooltip_action: A,
-// action_name: &'static str,
-// cx: &mut ViewContext<Toolbar>,
-// ) -> AnyElement<Toolbar> {
-// MouseEventHandler::new::<A, _>(0, cx, |state, _| {
-// let style = if enabled {
-// style.style_for(state)
+// todo!()
+// impl View for Toolbar {
+// fn ui_name() -> &'static str {
+// "Toolbar"
+// }
+
+// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+// let theme = &theme::current(cx).workspace.toolbar;
+
+// let mut primary_left_items = Vec::new();
+// let mut primary_right_items = Vec::new();
+// let mut secondary_item = None;
+// let spacing = theme.item_spacing;
+// let mut primary_items_row_count = 1;
+
+// for (item, position) in &self.items {
+// match *position {
+// ToolbarItemLocation::Hidden => {}
+
+// ToolbarItemLocation::PrimaryLeft { flex } => {
+// primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
+// let left_item = ChildView::new(item.as_any(), cx).aligned();
+// if let Some((flex, expanded)) = flex {
+// primary_left_items.push(left_item.flex(flex, expanded).into_any());
+// } else {
+// primary_left_items.push(left_item.into_any());
+// }
+// }
+
+// ToolbarItemLocation::PrimaryRight { flex } => {
+// primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
+// let right_item = ChildView::new(item.as_any(), cx).aligned().flex_float();
+// if let Some((flex, expanded)) = flex {
+// primary_right_items.push(right_item.flex(flex, expanded).into_any());
+// } else {
+// primary_right_items.push(right_item.into_any());
+// }
+// }
+
+// ToolbarItemLocation::Secondary => {
+// secondary_item = Some(
+// ChildView::new(item.as_any(), cx)
+// .constrained()
+// .with_height(theme.height * item.row_count(cx) as f32)
+// .into_any(),
+// );
+// }
+// }
+// }
+
+// let container_style = theme.container;
+// let height = theme.height * primary_items_row_count as f32;
+
+// let mut primary_items = Flex::row().with_spacing(spacing);
+// primary_items.extend(primary_left_items);
+// primary_items.extend(primary_right_items);
+
+// let mut toolbar = Flex::column();
+// if !primary_items.is_empty() {
+// toolbar.add_child(primary_items.constrained().with_height(height));
+// }
+// if let Some(secondary_item) = secondary_item {
+// toolbar.add_child(secondary_item);
+// }
+
+// if toolbar.is_empty() {
+// toolbar.into_any_named("toolbar")
// } else {
-// style.disabled_style()
-// };
-// Svg::new(svg_path)
-// .with_color(style.color)
-// .constrained()
-// .with_width(style.icon_width)
-// .aligned()
-// .contained()
-// .with_style(style.container)
-// .constrained()
-// .with_width(style.button_width)
-// .with_height(nav_button_height)
-// .aligned()
-// .top()
-// })
-// .with_cursor_style(if enabled {
-// CursorStyle::PointingHand
-// } else {
-// CursorStyle::default()
-// })
-// .on_click(MouseButton::Left, move |_, toolbar, cx| {
-// on_click(toolbar, cx)
-// })
-// .with_tooltip::<A>(
-// 0,
-// action_name,
-// Some(Box::new(tooltip_action)),
-// tooltip_style,
-// cx,
-// )
-// .contained()
-// .with_margin_right(spacing)
-// .into_any_named("nav button")
+// toolbar
+// .contained()
+// .with_style(container_style)
+// .into_any_named("toolbar")
+// }
+// }
// }
-// >>>>>>> 139cbbfd3aebd0863a7d51b0c12d748764cf0b2e
impl Toolbar {
pub fn new() -> Self {
Self {
@@ -206,7 +224,7 @@ impl Toolbar {
cx.notify();
}
- pub fn add_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
+ pub fn add_item<T>(&mut self, item: View<T>, cx: &mut ViewContext<Self>)
where
T: 'static + ToolbarItemView,
{
@@ -215,12 +233,13 @@ impl Toolbar {
if let Some((_, current_location)) =
this.items.iter_mut().find(|(i, _)| i.id() == item.id())
{
- let new_location = item
- .read(cx)
- .location_for_event(event, *current_location, cx);
- if new_location != *current_location {
- *current_location = new_location;
- cx.notify();
+ match event {
+ ToolbarItemEvent::ChangeLocation(new_location) => {
+ if new_location != current_location {
+ *current_location = *new_location;
+ cx.notify();
+ }
+ }
}
}
})
@@ -252,10 +271,10 @@ impl Toolbar {
}
}
- pub fn item_of_type<T: ToolbarItemView>(&self) -> Option<ViewHandle<T>> {
+ pub fn item_of_type<T: ToolbarItemView>(&self) -> Option<View<T>> {
self.items
.iter()
- .find_map(|(item, _)| item.as_any().clone().downcast())
+ .find_map(|(item, _)| item.to_any().downcast().ok())
}
pub fn hidden(&self) -> bool {
@@ -263,13 +282,13 @@ impl Toolbar {
}
}
-impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
- fn id(&self) -> usize {
- self.id()
+impl<T: ToolbarItemView> ToolbarItemViewHandle for View<T> {
+ fn id(&self) -> EntityId {
+ self.entity_id()
}
- fn as_any(&self) -> &AnyViewHandle {
- self
+ fn to_any(&self) -> AnyView {
+ self.clone().into()
}
fn set_active_pane_item(
@@ -290,12 +309,13 @@ impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
}
fn row_count(&self, cx: &WindowContext) -> usize {
- self.read_with(cx, |this, cx| this.row_count(cx))
+ self.read(cx).row_count(cx)
}
}
-impl From<&dyn ToolbarItemViewHandle> for AnyViewHandle {
- fn from(val: &dyn ToolbarItemViewHandle) -> Self {
- val.as_any().clone()
- }
-}
+// todo!()
+// impl From<&dyn ToolbarItemViewHandle> for AnyViewHandle {
+// fn from(val: &dyn ToolbarItemViewHandle) -> Self {
+// val.as_any().clone()
+// }
+// }
@@ -1,5 +1,6 @@
pub mod dock;
pub mod item;
+mod modal_layer;
pub mod notifications;
pub mod pane;
pub mod pane_group;
@@ -10,111 +11,80 @@ mod status_bar;
mod toolbar;
mod workspace_settings;
-use anyhow::{anyhow, Context, Result};
+use anyhow::{anyhow, Context as _, Result};
use call::ActiveCall;
use client::{
proto::{self, PeerId},
Client, Status, TelemetrySettings, TypedEnvelope, UserStore,
};
use collections::{hash_map, HashMap, HashSet};
-use drag_and_drop::DragAndDrop;
+use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
use futures::{
channel::{mpsc, oneshot},
future::try_join_all,
- FutureExt, StreamExt,
+ Future, FutureExt, StreamExt,
};
use gpui::{
- actions,
- elements::*,
- geometry::{
- rect::RectF,
- vector::{vec2f, Vector2F},
- },
- impl_actions,
- platform::{
- CursorStyle, ModifiersChangedEvent, MouseButton, PathPromptOptions, Platform, PromptLevel,
- WindowBounds, WindowOptions,
- },
- AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity,
- ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
- WeakViewHandle, WindowContext, WindowHandle,
+ actions, canvas, div, impl_actions, point, size, Action, AnyElement, AnyModel, AnyView,
+ AnyWeakView, AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, BorrowWindow,
+ Bounds, Context, Div, DragMoveEvent, Element, Entity, EntityId, EventEmitter, FocusHandle,
+ FocusableView, GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId,
+ ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel,
+ Render, Size, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
+ WindowBounds, WindowContext, WindowHandle, WindowOptions,
};
-use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
+use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
use itertools::Itertools;
use language::{LanguageRegistry, Rope};
-use node_runtime::NodeRuntime;
-use std::{
- any::TypeId,
- borrow::Cow,
- cmp, env,
- future::Future,
- path::{Path, PathBuf},
- rc::Rc,
- str,
- sync::{atomic::AtomicUsize, Arc},
- time::Duration,
-};
-
-use crate::{
- notifications::NotificationTracker,
- persistence::model::{
- DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
- },
-};
-use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
use lazy_static::lazy_static;
-use notifications::{
- simple_message_notification::MessageNotification, NotificationHandle, NotifyResultExt,
-};
+pub use modal_layer::*;
+use node_runtime::NodeRuntime;
+use notifications::{simple_message_notification::MessageNotification, NotificationHandle};
pub use pane::*;
pub use pane_group::*;
-use persistence::{model::SerializedItem, DB};
+use persistence::DB;
pub use persistence::{
- model::{ItemId, WorkspaceLocation},
+ model::{ItemId, SerializedWorkspace, WorkspaceLocation},
WorkspaceDb, DB as WORKSPACE_DB,
};
-use postage::prelude::Stream;
+use postage::stream::Stream;
use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
use serde::Deserialize;
+use settings::Settings;
use shared_screen::SharedScreen;
use status_bar::StatusBar;
pub use status_bar::StatusItemView;
-use theme::{Theme, ThemeSettings};
-pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
+use std::{
+ any::TypeId,
+ borrow::Cow,
+ cmp, env,
+ path::{Path, PathBuf},
+ sync::{atomic::AtomicUsize, Arc},
+ time::Duration,
+};
+use theme::{ActiveTheme, ThemeSettings};
+pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
+pub use ui;
+use ui::Label;
use util::ResultExt;
-pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
+use uuid::Uuid;
+pub use workspace_settings::{AutosaveSetting, WorkspaceSettings};
+
+use crate::persistence::model::{
+ DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup,
+};
lazy_static! {
- static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
+ static ref ZED_WINDOW_SIZE: Option<Size<GlobalPixels>> = env::var("ZED_WINDOW_SIZE")
.ok()
.as_deref()
- .and_then(parse_pixel_position_env_var);
- static ref ZED_WINDOW_POSITION: Option<Vector2F> = env::var("ZED_WINDOW_POSITION")
+ .and_then(parse_pixel_size_env_var);
+ static ref ZED_WINDOW_POSITION: Option<Point<GlobalPixels>> = env::var("ZED_WINDOW_POSITION")
.ok()
.as_deref()
.and_then(parse_pixel_position_env_var);
}
-pub trait Modal: View {
- fn has_focus(&self) -> bool;
- fn dismiss_on_event(event: &Self::Event) -> bool;
-}
-
-trait ModalHandle {
- fn as_any(&self) -> &AnyViewHandle;
- fn has_focus(&self, cx: &WindowContext) -> bool;
-}
-
-impl<T: Modal> ModalHandle for ViewHandle<T> {
- fn as_any(&self) -> &AnyViewHandle {
- self
- }
-
- fn has_focus(&self, cx: &WindowContext) -> bool {
- self.read(cx).has_focus()
- }
-}
-
#[derive(Clone, PartialEq)]
pub struct RemoveWorktreeFromProject(pub WorktreeId);
@@ -183,6 +153,20 @@ pub struct CloseAllItemsAndPanes {
pub save_intent: Option<SaveIntent>,
}
+impl_actions!(
+ workspace,
+ [
+ ActivatePane,
+ ActivatePaneInDirection,
+ CloseAllItemsAndPanes,
+ NewFileInDirection,
+ OpenTerminal,
+ Save,
+ SaveAll,
+ SwapPaneInDirection,
+ ]
+);
+
#[derive(Deserialize)]
pub struct Toast {
id: usize,
@@ -228,243 +212,126 @@ impl Clone for Toast {
}
}
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Debug, Default, Clone, Deserialize, PartialEq)]
pub struct OpenTerminal {
pub working_directory: PathBuf,
}
-impl_actions!(
- workspace,
- [
- ActivatePane,
- ActivatePaneInDirection,
- SwapPaneInDirection,
- NewFileInDirection,
- Toast,
- OpenTerminal,
- SaveAll,
- Save,
- CloseAllItemsAndPanes,
- ]
-);
-
pub type WorkspaceId = i64;
pub fn init_settings(cx: &mut AppContext) {
- settings::register::<WorkspaceSettings>(cx);
- settings::register::<item::ItemSettings>(cx);
+ WorkspaceSettings::register(cx);
+ ItemSettings::register(cx);
}
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
init_settings(cx);
- pane::init(cx);
notifications::init(cx);
- cx.add_global_action({
+ cx.on_action(Workspace::close_global);
+ cx.on_action(restart);
+
+ cx.on_action({
let app_state = Arc::downgrade(&app_state);
move |_: &Open, cx: &mut AppContext| {
- let mut paths = cx.prompt_for_paths(PathPromptOptions {
+ let paths = cx.prompt_for_paths(PathPromptOptions {
files: true,
directories: true,
multiple: true,
});
if let Some(app_state) = app_state.upgrade() {
- cx.spawn(move |mut cx| async move {
- if let Some(paths) = paths.recv().await.flatten() {
+ cx.spawn(move |cx| async move {
+ if let Some(paths) = paths.await.log_err().flatten() {
cx.update(|cx| {
open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx)
- });
+ })
+ .ok();
}
})
.detach();
}
}
});
- cx.add_async_action(Workspace::open);
-
- cx.add_async_action(Workspace::follow_next_collaborator);
- cx.add_async_action(Workspace::close);
- cx.add_async_action(Workspace::close_inactive_items_and_panes);
- cx.add_async_action(Workspace::close_all_items_and_panes);
- cx.add_global_action(Workspace::close_global);
- cx.add_global_action(restart);
- cx.add_async_action(Workspace::save_all);
- cx.add_action(Workspace::add_folder_to_project);
-
- cx.add_action(
- |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
- let pane = workspace.active_pane().clone();
- workspace.unfollow(&pane, cx);
- },
- );
- cx.add_action(
- |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext<Workspace>| {
- workspace
- .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
- .detach_and_log_err(cx);
- },
- );
- cx.add_action(
- |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
- workspace
- .save_active_item(SaveIntent::SaveAs, cx)
- .detach_and_log_err(cx);
- },
- );
- cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
- workspace.activate_previous_pane(cx)
- });
- cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
- workspace.activate_next_pane(cx)
- });
-
- cx.add_action(
- |workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| {
- workspace.activate_pane_in_direction(action.0, cx)
- },
- );
-
- cx.add_action(
- |workspace: &mut Workspace, action: &SwapPaneInDirection, cx| {
- workspace.swap_pane_in_direction(action.0, cx)
- },
- );
-
- cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| {
- workspace.toggle_dock(DockPosition::Left, cx);
- });
- cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
- workspace.toggle_dock(DockPosition::Right, cx);
- });
- cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
- workspace.toggle_dock(DockPosition::Bottom, cx);
- });
- cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
- workspace.close_all_docks(cx);
- });
- cx.add_action(Workspace::activate_pane_at_index);
- cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
- workspace.reopen_closed_item(cx).detach();
- });
- cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| {
- workspace
- .go_back(workspace.active_pane().downgrade(), cx)
- .detach();
- });
- cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| {
- workspace
- .go_forward(workspace.active_pane().downgrade(), cx)
- .detach();
- });
-
- cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| {
- cx.spawn(|workspace, mut cx| async move {
- let err = install_cli::install_cli(&cx)
- .await
- .context("Failed to create CLI symlink");
-
- workspace.update(&mut cx, |workspace, cx| {
- if matches!(err, Err(_)) {
- err.notify_err(workspace, cx);
- } else {
- workspace.show_notification(1, cx, |cx| {
- cx.add_view(|_| {
- MessageNotification::new("Successfully installed the `zed` binary")
- })
- });
- }
- })
- })
- .detach();
- });
}
-type ProjectItemBuilders = HashMap<
- TypeId,
- fn(ModelHandle<Project>, AnyModelHandle, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
->;
+type ProjectItemBuilders =
+ HashMap<TypeId, fn(Model<Project>, AnyModel, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>>;
pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
- cx.update_default_global(|builders: &mut ProjectItemBuilders, _| {
- builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
- let item = model.downcast::<I::Item>().unwrap();
- Box::new(cx.add_view(|cx| I::for_project_item(project, item, cx)))
- });
+ let builders = cx.default_global::<ProjectItemBuilders>();
+ builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
+ let item = model.downcast::<I::Item>().unwrap();
+ Box::new(cx.new_view(|cx| I::for_project_item(project, item, cx)))
});
}
type FollowableItemBuilder = fn(
- ViewHandle<Pane>,
- ViewHandle<Workspace>,
+ View<Pane>,
+ View<Workspace>,
ViewId,
&mut Option<proto::view::Variant>,
- &mut AppContext,
+ &mut WindowContext,
) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
type FollowableItemBuilders = HashMap<
TypeId,
(
FollowableItemBuilder,
- fn(&AnyViewHandle) -> Box<dyn FollowableItemHandle>,
+ fn(&AnyView) -> Box<dyn FollowableItemHandle>,
),
>;
pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
- cx.update_default_global(|builders: &mut FollowableItemBuilders, _| {
- builders.insert(
- TypeId::of::<I>(),
- (
- |pane, workspace, id, state, cx| {
- I::from_state_proto(pane, workspace, id, state, cx).map(|task| {
- cx.foreground()
- .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
- })
- },
- |this| Box::new(this.clone().downcast::<I>().unwrap()),
- ),
- );
- });
+ let builders = cx.default_global::<FollowableItemBuilders>();
+ builders.insert(
+ TypeId::of::<I>(),
+ (
+ |pane, workspace, id, state, cx| {
+ I::from_state_proto(pane, workspace, id, state, cx).map(|task| {
+ cx.foreground_executor()
+ .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
+ })
+ },
+ |this| Box::new(this.clone().downcast::<I>().unwrap()),
+ ),
+ );
}
type ItemDeserializers = HashMap<
Arc<str>,
fn(
- ModelHandle<Project>,
- WeakViewHandle<Workspace>,
+ Model<Project>,
+ WeakView<Workspace>,
WorkspaceId,
ItemId,
&mut ViewContext<Pane>,
) -> Task<Result<Box<dyn ItemHandle>>>,
>;
pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
- cx.update_default_global(|deserializers: &mut ItemDeserializers, _cx| {
- if let Some(serialized_item_kind) = I::serialized_item_kind() {
- deserializers.insert(
- Arc::from(serialized_item_kind),
- |project, workspace, workspace_id, item_id, cx| {
- let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
- cx.foreground()
- .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
- },
- );
- }
- });
+ if let Some(serialized_item_kind) = I::serialized_item_kind() {
+ let deserializers = cx.default_global::<ItemDeserializers>();
+ deserializers.insert(
+ Arc::from(serialized_item_kind),
+ |project, workspace, workspace_id, item_id, cx| {
+ let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
+ cx.foreground_executor()
+ .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
+ },
+ );
+ }
}
pub struct AppState {
pub languages: Arc<LanguageRegistry>,
pub client: Arc<Client>,
- pub user_store: ModelHandle<UserStore>,
- pub workspace_store: ModelHandle<WorkspaceStore>,
+ pub user_store: Model<UserStore>,
+ pub workspace_store: Model<WorkspaceStore>,
pub fs: Arc<dyn fs::Fs>,
pub build_window_options:
- fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
- pub initialize_workspace:
- fn(WeakViewHandle<Workspace>, bool, Arc<AppState>, AsyncAppContext) -> Task<Result<()>>,
- pub background_actions: BackgroundActions,
+ fn(Option<WindowBounds>, Option<Uuid>, &mut AppContext) -> WindowOptions,
pub node_runtime: Arc<dyn NodeRuntime>,
}
pub struct WorkspaceStore {
- workspaces: HashSet<WeakViewHandle<Workspace>>,
+ workspaces: HashSet<WindowHandle<Workspace>>,
followers: Vec<Follower>,
client: Arc<Client>,
_subscriptions: Vec<client::Subscription>,
@@ -483,17 +350,18 @@ impl AppState {
use settings::SettingsStore;
if !cx.has_global::<SettingsStore>() {
- cx.set_global(SettingsStore::test(cx));
+ let settings_store = SettingsStore::test(cx);
+ cx.set_global(settings_store);
}
- let fs = fs::FakeFs::new(cx.background().clone());
+ let fs = fs::FakeFs::new(cx.background_executor().clone());
let languages = Arc::new(LanguageRegistry::test());
let http_client = util::http::FakeHttpClient::with_404_response();
let client = Client::new(http_client.clone(), cx);
- let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
- let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
+ let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
+ let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
- theme::init((), cx);
+ theme::init(theme::LoadThemes::JustBase, cx);
client::init(&client, cx);
crate::init_settings(cx);
@@ -502,12 +370,9 @@ impl AppState {
fs,
languages,
user_store,
- // channel_store,
workspace_store,
node_runtime: FakeNodeRuntime::new(),
- initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
build_window_options: |_, _, _| Default::default(),
- background_actions: || &[],
})
}
}
@@ -527,7 +392,7 @@ impl DelayedDebouncedEditAction {
fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Workspace>, func: F)
where
- F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> Task<Result<()>>,
+ F: 'static + Send + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> Task<Result<()>>,
{
if let Some(channel) = self.cancel_channel.take() {
_ = channel.send(());
@@ -537,8 +402,8 @@ impl DelayedDebouncedEditAction {
self.cancel_channel = Some(sender);
let previous_task = self.task.take();
- self.task = Some(cx.spawn(|workspace, mut cx| async move {
- let mut timer = cx.background().timer(delay).fuse();
+ self.task = Some(cx.spawn(move |workspace, mut cx| async move {
+ let mut timer = cx.background_executor().timer(delay).fuse();
if let Some(previous_task) = previous_task {
previous_task.await;
}
@@ -559,46 +424,46 @@ impl DelayedDebouncedEditAction {
}
pub enum Event {
- PaneAdded(ViewHandle<Pane>),
+ PaneAdded(View<Pane>),
ContactRequestedJoin(u64),
+ WorkspaceCreated(WeakView<Workspace>),
}
pub struct Workspace {
- weak_self: WeakViewHandle<Self>,
- modal: Option<ActiveModal>,
- zoomed: Option<AnyWeakViewHandle>,
+ weak_self: WeakView<Self>,
+ workspace_actions: Vec<Box<dyn Fn(Div, &mut ViewContext<Self>) -> Div>>,
+ zoomed: Option<AnyWeakView>,
zoomed_position: Option<DockPosition>,
center: PaneGroup,
- left_dock: ViewHandle<Dock>,
- bottom_dock: ViewHandle<Dock>,
- right_dock: ViewHandle<Dock>,
- panes: Vec<ViewHandle<Pane>>,
- panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
- active_pane: ViewHandle<Pane>,
- last_active_center_pane: Option<WeakViewHandle<Pane>>,
+ left_dock: View<Dock>,
+ bottom_dock: View<Dock>,
+ right_dock: View<Dock>,
+ panes: Vec<View<Pane>>,
+ panes_by_item: HashMap<EntityId, WeakView<Pane>>,
+ active_pane: View<Pane>,
+ last_active_center_pane: Option<WeakView<Pane>>,
last_active_view_id: Option<proto::ViewId>,
- status_bar: ViewHandle<StatusBar>,
- titlebar_item: Option<AnyViewHandle>,
+ status_bar: View<StatusBar>,
+ modal_layer: View<ModalLayer>,
+ titlebar_item: Option<AnyView>,
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
- project: ModelHandle<Project>,
- follower_states: HashMap<ViewHandle<Pane>, FollowerState>,
- last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
+ project: Model<Project>,
+ follower_states: HashMap<View<Pane>, FollowerState>,
+ last_leaders_by_pane: HashMap<WeakView<Pane>, PeerId>,
window_edited: bool,
- active_call: Option<(ModelHandle<ActiveCall>, Vec<Subscription>)>,
+ active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
database_id: WorkspaceId,
app_state: Arc<AppState>,
- subscriptions: Vec<Subscription>,
+ _subscriptions: Vec<Subscription>,
_apply_leader_updates: Task<Result<()>>,
_observe_current_user: Task<Result<()>>,
_schedule_serialize: Option<Task<()>>,
pane_history_timestamp: Arc<AtomicUsize>,
+ bounds: Bounds<Pixels>,
}
-struct ActiveModal {
- view: Box<dyn ModalHandle>,
- previously_focused_view_id: Option<usize>,
-}
+impl EventEmitter<Event> for Workspace {}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct ViewId {
@@ -613,12 +478,10 @@ struct FollowerState {
items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
}
-enum WorkspaceBounds {}
-
impl Workspace {
pub fn new(
workspace_id: WorkspaceId,
- project: ModelHandle<Project>,
+ project: Model<Project>,
app_state: Arc<AppState>,
cx: &mut ViewContext<Self>,
) -> Self {
@@ -640,7 +503,7 @@ impl Workspace {
project::Event::DisconnectedFromHost => {
this.update_window_edited(cx);
- cx.blur();
+ cx.disable_focus();
}
project::Event::Closed => {
@@ -656,7 +519,7 @@ impl Workspace {
}
project::Event::Notification(message) => this.show_notification(0, cx, |cx| {
- cx.add_view(|_| MessageNotification::new(message.clone()))
+ cx.new_view(|_| MessageNotification::new(message.clone()))
}),
_ => {}
@@ -665,31 +528,39 @@ impl Workspace {
})
.detach();
- let weak_handle = cx.weak_handle();
+ cx.on_blur_window(|this, cx| {
+ let focus_handle = this.focus_handle(cx);
+ cx.focus(&focus_handle);
+ })
+ .detach();
+
+ let weak_handle = cx.view().downgrade();
let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
- let center_pane = cx.add_view(|cx| {
+ let center_pane = cx.new_view(|cx| {
Pane::new(
weak_handle.clone(),
project.clone(),
- app_state.background_actions,
pane_history_timestamp.clone(),
+ None,
cx,
)
});
cx.subscribe(¢er_pane, Self::handle_pane_event).detach();
- cx.focus(¢er_pane);
+
+ cx.focus_view(¢er_pane);
cx.emit(Event::PaneAdded(center_pane.clone()));
+ let window_handle = cx.window_handle().downcast::<Workspace>().unwrap();
app_state.workspace_store.update(cx, |store, _| {
- store.workspaces.insert(weak_handle.clone());
+ store.workspaces.insert(window_handle);
});
let mut current_user = app_state.user_store.read(cx).watch_current_user();
let mut connection_status = app_state.client.status();
let _observe_current_user = cx.spawn(|this, mut cx| async move {
- current_user.recv().await;
- connection_status.recv().await;
+ current_user.next().await;
+ connection_status.next().await;
let mut stream =
Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
@@ -713,18 +584,15 @@ impl Workspace {
Ok(())
});
- cx.emit_global(WorkspaceCreated(weak_handle.clone()));
-
- let left_dock = cx.add_view(|_| Dock::new(DockPosition::Left));
- let bottom_dock = cx.add_view(|_| Dock::new(DockPosition::Bottom));
- let right_dock = cx.add_view(|_| Dock::new(DockPosition::Right));
- let left_dock_buttons =
- cx.add_view(|cx| PanelButtons::new(left_dock.clone(), weak_handle.clone(), cx));
- let bottom_dock_buttons =
- cx.add_view(|cx| PanelButtons::new(bottom_dock.clone(), weak_handle.clone(), cx));
- let right_dock_buttons =
- cx.add_view(|cx| PanelButtons::new(right_dock.clone(), weak_handle.clone(), cx));
- let status_bar = cx.add_view(|cx| {
+ cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
+
+ let left_dock = Dock::new(DockPosition::Left, cx);
+ let bottom_dock = Dock::new(DockPosition::Bottom, cx);
+ let right_dock = Dock::new(DockPosition::Right, cx);
+ let left_dock_buttons = cx.new_view(|cx| PanelButtons::new(left_dock.clone(), cx));
+ let bottom_dock_buttons = cx.new_view(|cx| PanelButtons::new(bottom_dock.clone(), cx));
+ let right_dock_buttons = cx.new_view(|cx| PanelButtons::new(right_dock.clone(), cx));
+ let status_bar = cx.new_view(|cx| {
let mut status_bar = StatusBar::new(¢er_pane.clone(), cx);
status_bar.add_left_item(left_dock_buttons, cx);
status_bar.add_right_item(right_dock_buttons, cx);
@@ -732,37 +600,35 @@ impl Workspace {
status_bar
});
- cx.update_default_global::<DragAndDrop<Workspace>, _, _>(|drag_and_drop, _| {
- drag_and_drop.register_container(weak_handle.clone());
- });
+ let modal_layer = cx.new_view(|_| ModalLayer::new());
let mut active_call = None;
- if cx.has_global::<ModelHandle<ActiveCall>>() {
- let call = cx.global::<ModelHandle<ActiveCall>>().clone();
+ if cx.has_global::<Model<ActiveCall>>() {
+ let call = cx.global::<Model<ActiveCall>>().clone();
let mut subscriptions = Vec::new();
subscriptions.push(cx.subscribe(&call, Self::on_active_call_event));
active_call = Some((call, subscriptions));
}
let subscriptions = vec![
- cx.observe_fullscreen(|_, _, cx| cx.notify()),
cx.observe_window_activation(Self::on_window_activation_changed),
- cx.observe_window_bounds(move |_, mut bounds, display, cx| {
- // Transform fixed bounds to be stored in terms of the containing display
- if let WindowBounds::Fixed(mut window_bounds) = bounds {
- if let Some(screen) = cx.platform().screen_by_id(display) {
- let screen_bounds = screen.bounds();
- window_bounds
- .set_origin_x(window_bounds.origin_x() - screen_bounds.origin_x());
- window_bounds
- .set_origin_y(window_bounds.origin_y() - screen_bounds.origin_y());
- bounds = WindowBounds::Fixed(window_bounds);
+ cx.observe_window_bounds(move |_, cx| {
+ if let Some(display) = cx.display() {
+ // Transform fixed bounds to be stored in terms of the containing display
+ let mut bounds = cx.window_bounds();
+ if let WindowBounds::Fixed(window_bounds) = &mut bounds {
+ let display_bounds = display.bounds();
+ window_bounds.origin.x -= display_bounds.origin.x;
+ window_bounds.origin.y -= display_bounds.origin.y;
}
- }
- cx.background()
- .spawn(DB.set_window_bounds(workspace_id, bounds, display))
- .detach_and_log_err(cx);
+ if let Some(display_uuid) = display.uuid().log_err() {
+ cx.background_executor()
+ .spawn(DB.set_window_bounds(workspace_id, bounds, display_uuid))
+ .detach_and_log_err(cx);
+ }
+ }
+ cx.notify();
}),
cx.observe(&left_dock, |this, _, cx| {
this.serialize_workspace(cx);
@@ -776,14 +642,31 @@ impl Workspace {
this.serialize_workspace(cx);
cx.notify();
}),
+ cx.on_release(|this, window, cx| {
+ this.app_state.workspace_store.update(cx, |store, _| {
+ let window = window.downcast::<Self>().unwrap();
+ debug_assert!(store.workspaces.remove(&window));
+ })
+ }),
];
cx.defer(|this, cx| {
this.update_window_title(cx);
+ // todo! @nate - these are useful for testing notifications
+ // this.show_error(
+ // &anyhow::anyhow!("what happens if this message is very very very very very long"),
+ // cx,
+ // );
+
+ // this.show_notification(1, cx, |cx| {
+ // cx.build_view(|_cx| {
+ // simple_message_notification::MessageNotification::new(format!("Error:"))
+ // .with_click_message("click here because!")
+ // })
+ // });
});
Workspace {
weak_self: weak_handle.clone(),
- modal: None,
zoomed: None,
zoomed_position: None,
center: PaneGroup::new(center_pane.clone()),
@@ -793,6 +676,7 @@ impl Workspace {
last_active_center_pane: Some(center_pane.downgrade()),
last_active_view_id: None,
status_bar,
+ modal_layer,
titlebar_item: None,
notifications: Default::default(),
left_dock,
@@ -809,8 +693,11 @@ impl Workspace {
_apply_leader_updates,
_schedule_serialize: None,
leader_updates_tx,
- subscriptions,
+ _subscriptions: subscriptions,
pane_history_timestamp,
+ workspace_actions: Default::default(),
+ // This data will be incorrect, but it will be overwritten by the time it needs to be used.
+ bounds: Default::default(),
}
}
@@ -819,10 +706,12 @@ impl Workspace {
app_state: Arc<AppState>,
requesting_window: Option<WindowHandle<Workspace>>,
cx: &mut AppContext,
- ) -> Task<(
- WeakViewHandle<Workspace>,
- Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
- )> {
+ ) -> Task<
+ anyhow::Result<(
+ WindowHandle<Workspace>,
+ Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
+ )>,
+ > {
let project_handle = Project::local(
app_state.client.clone(),
app_state.node_runtime.clone(),
@@ -833,7 +722,8 @@ impl Workspace {
);
cx.spawn(|mut cx| async move {
- let serialized_workspace = persistence::DB.workspace_for_roots(&abs_paths.as_slice());
+ let serialized_workspace: Option<SerializedWorkspace> =
+ persistence::DB.workspace_for_roots(&abs_paths.as_slice());
let paths_to_open = Arc::new(abs_paths);
@@ -845,11 +735,11 @@ impl Workspace {
if let Some((worktree, project_entry)) = cx
.update(|cx| {
Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
- })
+ })?
.await
.log_err()
{
- worktree_roots.insert(worktree.read_with(&mut cx, |tree, _| tree.abs_path()));
+ worktree_roots.extend(worktree.update(&mut cx, |tree, _| tree.abs_path()).ok());
project_paths.push((path, Some(project_entry)));
} else {
project_paths.push((path, None));
@@ -863,199 +753,105 @@ impl Workspace {
};
let window = if let Some(window) = requesting_window {
- window.replace_root(&mut cx, |cx| {
- Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
- });
+ cx.update_window(window.into(), |_, cx| {
+ cx.replace_root_view(|cx| {
+ Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
+ });
+ })?;
window
} else {
- {
- let window_bounds_override = window_bounds_env_override(&cx);
- let (bounds, display) = if let Some(bounds) = window_bounds_override {
- (Some(bounds), None)
- } else {
- serialized_workspace
- .as_ref()
- .and_then(|serialized_workspace| {
- let display = serialized_workspace.display?;
- let mut bounds = serialized_workspace.bounds?;
-
- // Stored bounds are relative to the containing display.
- // So convert back to global coordinates if that screen still exists
- if let WindowBounds::Fixed(mut window_bounds) = bounds {
- if let Some(screen) = cx.platform().screen_by_id(display) {
- let screen_bounds = screen.bounds();
- window_bounds.set_origin_x(
- window_bounds.origin_x() + screen_bounds.origin_x(),
- );
- window_bounds.set_origin_y(
- window_bounds.origin_y() + screen_bounds.origin_y(),
- );
- bounds = WindowBounds::Fixed(window_bounds);
- } else {
- // Screen no longer exists. Return none here.
- return None;
- }
- }
+ let window_bounds_override = window_bounds_env_override(&cx);
+ let (bounds, display) = if let Some(bounds) = window_bounds_override {
+ (Some(bounds), None)
+ } else {
+ serialized_workspace
+ .as_ref()
+ .and_then(|serialized_workspace| {
+ let serialized_display = serialized_workspace.display?;
+ let mut bounds = serialized_workspace.bounds?;
+
+ // Stored bounds are relative to the containing display.
+ // So convert back to global coordinates if that screen still exists
+ if let WindowBounds::Fixed(mut window_bounds) = bounds {
+ let screen = cx
+ .update(|cx| {
+ cx.displays().into_iter().find(|display| {
+ display.uuid().ok() == Some(serialized_display)
+ })
+ })
+ .ok()??;
+ let screen_bounds = screen.bounds();
+ window_bounds.origin.x += screen_bounds.origin.x;
+ window_bounds.origin.y += screen_bounds.origin.y;
+ bounds = WindowBounds::Fixed(window_bounds);
+ }
- Some((bounds, display))
- })
- .unzip()
- };
+ Some((bounds, serialized_display))
+ })
+ .unzip()
+ };
- // Use the serialized workspace to construct the new window
- cx.add_window(
- (app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
- |cx| {
- Workspace::new(
- workspace_id,
- project_handle.clone(),
- app_state.clone(),
- cx,
- )
- },
- )
- }
+ // Use the serialized workspace to construct the new window
+ let options =
+ cx.update(|cx| (app_state.build_window_options)(bounds, display, cx))?;
+
+ cx.open_window(options, {
+ let app_state = app_state.clone();
+ let workspace_id = workspace_id.clone();
+ let project_handle = project_handle.clone();
+ move |cx| {
+ cx.new_view(|cx| {
+ Workspace::new(workspace_id, project_handle, app_state, cx)
+ })
+ }
+ })?
};
- // We haven't yielded the main thread since obtaining the window handle,
- // so the window exists.
- let workspace = window.root(&cx).unwrap();
-
- (app_state.initialize_workspace)(
- workspace.downgrade(),
- serialized_workspace.is_some(),
- app_state.clone(),
- cx.clone(),
- )
- .await
- .log_err();
-
- window.update(&mut cx, |cx| cx.activate_window());
+ window
+ .update(&mut cx, |_, cx| cx.activate_window())
+ .log_err();
- let workspace = workspace.downgrade();
- notify_if_database_failed(&workspace, &mut cx);
- let opened_items = open_items(
- serialized_workspace,
- &workspace,
- project_paths,
- app_state,
- cx,
- )
- .await
- .unwrap_or_default();
+ notify_if_database_failed(window, &mut cx);
+ let opened_items = window
+ .update(&mut cx, |_workspace, cx| {
+ open_items(serialized_workspace, project_paths, app_state, cx)
+ })?
+ .await
+ .unwrap_or_default();
- (workspace, opened_items)
+ Ok((window, opened_items))
})
}
- pub fn weak_handle(&self) -> WeakViewHandle<Self> {
+ pub fn weak_handle(&self) -> WeakView<Self> {
self.weak_self.clone()
}
- pub fn left_dock(&self) -> &ViewHandle<Dock> {
+ pub fn left_dock(&self) -> &View<Dock> {
&self.left_dock
}
- pub fn bottom_dock(&self) -> &ViewHandle<Dock> {
+ pub fn bottom_dock(&self) -> &View<Dock> {
&self.bottom_dock
}
- pub fn right_dock(&self) -> &ViewHandle<Dock> {
+ pub fn right_dock(&self) -> &View<Dock> {
&self.right_dock
}
- pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>)
- where
- T::Event: std::fmt::Debug,
- {
- self.add_panel_with_extra_event_handler(panel, cx, |_, _, _, _| {})
- }
-
- pub fn add_panel_with_extra_event_handler<T: Panel, F>(
- &mut self,
- panel: ViewHandle<T>,
- cx: &mut ViewContext<Self>,
- handler: F,
- ) where
- T::Event: std::fmt::Debug,
- F: Fn(&mut Self, &ViewHandle<T>, &T::Event, &mut ViewContext<Self>) + 'static,
- {
+ pub fn add_panel<T: Panel>(&mut self, panel: View<T>, cx: &mut ViewContext<Self>) {
let dock = match panel.position(cx) {
DockPosition::Left => &self.left_dock,
DockPosition::Bottom => &self.bottom_dock,
DockPosition::Right => &self.right_dock,
};
- self.subscriptions.push(cx.subscribe(&panel, {
- let mut dock = dock.clone();
- let mut prev_position = panel.position(cx);
- move |this, panel, event, cx| {
- if T::should_change_position_on_event(event) {
- let new_position = panel.read(cx).position(cx);
- let mut was_visible = false;
- dock.update(cx, |dock, cx| {
- prev_position = new_position;
-
- was_visible = dock.is_open()
- && dock
- .visible_panel()
- .map_or(false, |active_panel| active_panel.id() == panel.id());
- dock.remove_panel(&panel, cx);
- });
-
- if panel.is_zoomed(cx) {
- this.zoomed_position = Some(new_position);
- }
-
- dock = match panel.read(cx).position(cx) {
- DockPosition::Left => &this.left_dock,
- DockPosition::Bottom => &this.bottom_dock,
- DockPosition::Right => &this.right_dock,
- }
- .clone();
- dock.update(cx, |dock, cx| {
- dock.add_panel(panel.clone(), cx);
- if was_visible {
- dock.set_open(true, cx);
- dock.activate_panel(dock.panels_len() - 1, cx);
- }
- });
- } else if T::should_zoom_in_on_event(event) {
- dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx));
- if !panel.has_focus(cx) {
- cx.focus(&panel);
- }
- this.zoomed = Some(panel.downgrade().into_any());
- this.zoomed_position = Some(panel.read(cx).position(cx));
- } else if T::should_zoom_out_on_event(event) {
- dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, false, cx));
- if this.zoomed_position == Some(prev_position) {
- this.zoomed = None;
- this.zoomed_position = None;
- }
- cx.notify();
- } else if T::is_focus_event(event) {
- let position = panel.read(cx).position(cx);
- this.dismiss_zoomed_items_to_reveal(Some(position), cx);
- if panel.is_zoomed(cx) {
- this.zoomed = Some(panel.downgrade().into_any());
- this.zoomed_position = Some(position);
- } else {
- this.zoomed = None;
- this.zoomed_position = None;
- }
- this.update_active_view_for_followers(cx);
- cx.notify();
- } else {
- handler(this, &panel, event, cx)
- }
- }
- }));
-
- dock.update(cx, |dock, cx| dock.add_panel(panel, cx));
+ dock.update(cx, |dock, cx| {
+ dock.add_panel(panel, self.weak_self.clone(), cx)
+ });
}
- pub fn status_bar(&self) -> &ViewHandle<StatusBar> {
+ pub fn status_bar(&self) -> &View<StatusBar> {
&self.status_bar
}
@@ -1,6 +1,6 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use settings::Setting;
+use settings::Settings;
#[derive(Deserialize)]
pub struct WorkspaceSettings {
@@ -41,7 +41,7 @@ pub enum GitGutterSetting {
Hide,
}
-impl Setting for WorkspaceSettings {
+impl Settings for WorkspaceSettings {
const KEY: Option<&'static str> = None;
type FileContent = WorkspaceSettingsContent;
@@ -49,7 +49,7 @@ impl Setting for WorkspaceSettings {
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
- _: &gpui::AppContext,
+ _: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
@@ -1,66 +0,0 @@
-[package]
-name = "workspace2"
-version = "0.1.0"
-edition = "2021"
-publish = false
-
-[lib]
-path = "src/workspace2.rs"
-doctest = false
-
-[features]
-test-support = [
- "call/test-support",
- "client/test-support",
- "project/test-support",
- "settings/test-support",
- "gpui/test-support",
- "fs/test-support"
-]
-
-[dependencies]
-db = { path = "../db2", package = "db2" }
-call = { path = "../call2", package = "call2" }
-client = { path = "../client2", package = "client2" }
-collections = { path = "../collections" }
-# context_menu = { path = "../context_menu" }
-fs = { path = "../fs2", package = "fs2" }
-gpui = { package = "gpui2", path = "../gpui2" }
-install_cli = { path = "../install_cli2", package = "install_cli2" }
-language = { path = "../language2", package = "language2" }
-#menu = { path = "../menu" }
-node_runtime = { path = "../node_runtime" }
-project = { path = "../project2", package = "project2" }
-settings = { path = "../settings2", package = "settings2" }
-terminal = { path = "../terminal2", package = "terminal2" }
-theme = { path = "../theme2", package = "theme2" }
-util = { path = "../util" }
-ui = { package = "ui2", path = "../ui2" }
-
-async-recursion = "1.0.0"
-itertools = "0.10"
-bincode = "1.2.1"
-anyhow.workspace = true
-futures.workspace = true
-lazy_static.workspace = true
-log.workspace = true
-parking_lot.workspace = true
-postage.workspace = true
-schemars.workspace = true
-serde.workspace = true
-serde_derive.workspace = true
-serde_json.workspace = true
-smallvec.workspace = true
-uuid.workspace = true
-
-[dev-dependencies]
-call = { path = "../call2", package = "call2", features = ["test-support"] }
-client = { path = "../client2", package = "client2", features = ["test-support"] }
-gpui = { path = "../gpui2", package = "gpui2", features = ["test-support"] }
-project = { path = "../project2", package = "project2", features = ["test-support"] }
-settings = { path = "../settings2", package = "settings2", features = ["test-support"] }
-fs = { path = "../fs2", package = "fs2", features = ["test-support"] }
-db = { path = "../db2", package = "db2", features = ["test-support"] }
-
-indoc.workspace = true
-env_logger.workspace = true
@@ -1,783 +0,0 @@
-use crate::DraggedDock;
-use crate::{status_bar::StatusItemView, Workspace};
-use gpui::{
- div, px, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Entity, EntityId,
- EventEmitter, FocusHandle, FocusableView, IntoElement, MouseButton, ParentElement, Render,
- SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView, WindowContext,
-};
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use std::sync::Arc;
-use ui::{h_stack, ContextMenu, IconButton, Tooltip};
-use ui::{prelude::*, right_click_menu};
-
-const RESIZE_HANDLE_SIZE: Pixels = Pixels(6.);
-
-pub enum PanelEvent {
- ChangePosition,
- ZoomIn,
- ZoomOut,
- Activate,
- Close,
- Focus,
-}
-
-pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
- fn persistent_name() -> &'static str;
- fn position(&self, cx: &WindowContext) -> DockPosition;
- fn position_is_valid(&self, position: DockPosition) -> bool;
- fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
- fn size(&self, cx: &WindowContext) -> Pixels;
- fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>);
- fn icon(&self, cx: &WindowContext) -> Option<ui::Icon>;
- fn icon_tooltip(&self, cx: &WindowContext) -> Option<&'static str>;
- fn toggle_action(&self) -> Box<dyn Action>;
- fn icon_label(&self, _: &WindowContext) -> Option<String> {
- None
- }
- fn is_zoomed(&self, _cx: &WindowContext) -> bool {
- false
- }
- fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {}
- fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
-}
-
-pub trait PanelHandle: Send + Sync {
- fn panel_id(&self) -> EntityId;
- fn persistent_name(&self) -> &'static str;
- fn position(&self, cx: &WindowContext) -> DockPosition;
- fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool;
- fn set_position(&self, position: DockPosition, cx: &mut WindowContext);
- fn is_zoomed(&self, cx: &WindowContext) -> bool;
- fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext);
- fn set_active(&self, active: bool, cx: &mut WindowContext);
- fn size(&self, cx: &WindowContext) -> Pixels;
- fn set_size(&self, size: Option<Pixels>, cx: &mut WindowContext);
- fn icon(&self, cx: &WindowContext) -> Option<ui::Icon>;
- fn icon_tooltip(&self, cx: &WindowContext) -> Option<&'static str>;
- fn toggle_action(&self, cx: &WindowContext) -> Box<dyn Action>;
- fn icon_label(&self, cx: &WindowContext) -> Option<String>;
- fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
- fn to_any(&self) -> AnyView;
-}
-
-impl<T> PanelHandle for View<T>
-where
- T: Panel,
-{
- fn panel_id(&self) -> EntityId {
- Entity::entity_id(self)
- }
-
- fn persistent_name(&self) -> &'static str {
- T::persistent_name()
- }
-
- fn position(&self, cx: &WindowContext) -> DockPosition {
- self.read(cx).position(cx)
- }
-
- fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool {
- self.read(cx).position_is_valid(position)
- }
-
- fn set_position(&self, position: DockPosition, cx: &mut WindowContext) {
- self.update(cx, |this, cx| this.set_position(position, cx))
- }
-
- fn is_zoomed(&self, cx: &WindowContext) -> bool {
- self.read(cx).is_zoomed(cx)
- }
-
- fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext) {
- self.update(cx, |this, cx| this.set_zoomed(zoomed, cx))
- }
-
- fn set_active(&self, active: bool, cx: &mut WindowContext) {
- self.update(cx, |this, cx| this.set_active(active, cx))
- }
-
- fn size(&self, cx: &WindowContext) -> Pixels {
- self.read(cx).size(cx)
- }
-
- fn set_size(&self, size: Option<Pixels>, cx: &mut WindowContext) {
- self.update(cx, |this, cx| this.set_size(size, cx))
- }
-
- fn icon(&self, cx: &WindowContext) -> Option<ui::Icon> {
- self.read(cx).icon(cx)
- }
-
- fn icon_tooltip(&self, cx: &WindowContext) -> Option<&'static str> {
- self.read(cx).icon_tooltip(cx)
- }
-
- fn toggle_action(&self, cx: &WindowContext) -> Box<dyn Action> {
- self.read(cx).toggle_action()
- }
-
- fn icon_label(&self, cx: &WindowContext) -> Option<String> {
- self.read(cx).icon_label(cx)
- }
-
- fn to_any(&self) -> AnyView {
- self.clone().into()
- }
-
- fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
- self.read(cx).focus_handle(cx).clone()
- }
-}
-
-impl From<&dyn PanelHandle> for AnyView {
- fn from(val: &dyn PanelHandle) -> Self {
- val.to_any()
- }
-}
-
-pub struct Dock {
- position: DockPosition,
- panel_entries: Vec<PanelEntry>,
- is_open: bool,
- active_panel_index: usize,
- focus_handle: FocusHandle,
- _focus_subscription: Subscription,
-}
-
-impl FocusableView for Dock {
- fn focus_handle(&self, _: &AppContext) -> FocusHandle {
- self.focus_handle.clone()
- }
-}
-
-#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
-#[serde(rename_all = "lowercase")]
-pub enum DockPosition {
- Left,
- Bottom,
- Right,
-}
-
-impl DockPosition {
- fn to_label(&self) -> &'static str {
- match self {
- Self::Left => "left",
- Self::Bottom => "bottom",
- Self::Right => "right",
- }
- }
-
- // todo!()
- // fn to_resize_handle_side(self) -> HandleSide {
- // match self {
- // Self::Left => HandleSide::Right,
- // Self::Bottom => HandleSide::Top,
- // Self::Right => HandleSide::Left,
- // }
- // }
-
- pub fn axis(&self) -> Axis {
- match self {
- Self::Left | Self::Right => Axis::Horizontal,
- Self::Bottom => Axis::Vertical,
- }
- }
-}
-
-struct PanelEntry {
- panel: Arc<dyn PanelHandle>,
- // todo!()
- // context_menu: View<ContextMenu>,
- _subscriptions: [Subscription; 2],
-}
-
-pub struct PanelButtons {
- dock: View<Dock>,
-}
-
-impl Dock {
- pub fn new(position: DockPosition, cx: &mut ViewContext<Workspace>) -> View<Self> {
- let focus_handle = cx.focus_handle();
-
- let dock = cx.new_view(|cx: &mut ViewContext<Self>| {
- let focus_subscription = cx.on_focus(&focus_handle, |dock, cx| {
- if let Some(active_entry) = dock.panel_entries.get(dock.active_panel_index) {
- active_entry.panel.focus_handle(cx).focus(cx)
- }
- });
- Self {
- position,
- panel_entries: Default::default(),
- active_panel_index: 0,
- is_open: false,
- focus_handle: focus_handle.clone(),
- _focus_subscription: focus_subscription,
- }
- });
-
- cx.observe(&dock, move |workspace, dock, cx| {
- if dock.read(cx).is_open() {
- if let Some(panel) = dock.read(cx).active_panel() {
- if panel.is_zoomed(cx) {
- workspace.zoomed = Some(panel.to_any().downgrade());
- workspace.zoomed_position = Some(position);
- return;
- }
- }
- }
- if workspace.zoomed_position == Some(position) {
- workspace.zoomed = None;
- workspace.zoomed_position = None;
- }
- })
- .detach();
-
- dock
- }
-
- pub fn position(&self) -> DockPosition {
- self.position
- }
-
- pub fn is_open(&self) -> bool {
- self.is_open
- }
-
- // todo!()
- // pub fn has_focus(&self, cx: &WindowContext) -> bool {
- // self.visible_panel()
- // .map_or(false, |panel| panel.has_focus(cx))
- // }
-
- pub fn panel<T: Panel>(&self) -> Option<View<T>> {
- self.panel_entries
- .iter()
- .find_map(|entry| entry.panel.to_any().clone().downcast().ok())
- }
-
- pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
- self.panel_entries
- .iter()
- .position(|entry| entry.panel.to_any().downcast::<T>().is_ok())
- }
-
- pub fn panel_index_for_persistent_name(
- &self,
- ui_name: &str,
- _cx: &AppContext,
- ) -> Option<usize> {
- self.panel_entries
- .iter()
- .position(|entry| entry.panel.persistent_name() == ui_name)
- }
-
- pub fn active_panel_index(&self) -> usize {
- self.active_panel_index
- }
-
- pub(crate) fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) {
- if open != self.is_open {
- self.is_open = open;
- if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
- active_panel.panel.set_active(open, cx);
- }
-
- cx.notify();
- }
- }
-
- pub fn set_panel_zoomed(&mut self, panel: &AnyView, zoomed: bool, cx: &mut ViewContext<Self>) {
- for entry in &mut self.panel_entries {
- if entry.panel.panel_id() == panel.entity_id() {
- if zoomed != entry.panel.is_zoomed(cx) {
- entry.panel.set_zoomed(zoomed, cx);
- }
- } else if entry.panel.is_zoomed(cx) {
- entry.panel.set_zoomed(false, cx);
- }
- }
-
- cx.notify();
- }
-
- pub fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
- for entry in &mut self.panel_entries {
- if entry.panel.is_zoomed(cx) {
- entry.panel.set_zoomed(false, cx);
- }
- }
- }
-
- pub(crate) fn add_panel<T: Panel>(
- &mut self,
- panel: View<T>,
- workspace: WeakView<Workspace>,
- cx: &mut ViewContext<Self>,
- ) {
- let subscriptions = [
- cx.observe(&panel, |_, _, cx| cx.notify()),
- cx.subscribe(&panel, move |this, panel, event, cx| match event {
- PanelEvent::ChangePosition => {
- let new_position = panel.read(cx).position(cx);
-
- let Ok(new_dock) = workspace.update(cx, |workspace, cx| {
- if panel.is_zoomed(cx) {
- workspace.zoomed_position = Some(new_position);
- }
- match new_position {
- DockPosition::Left => &workspace.left_dock,
- DockPosition::Bottom => &workspace.bottom_dock,
- DockPosition::Right => &workspace.right_dock,
- }
- .clone()
- }) else {
- return;
- };
-
- let was_visible = this.is_open()
- && this.visible_panel().map_or(false, |active_panel| {
- active_panel.panel_id() == Entity::entity_id(&panel)
- });
-
- this.remove_panel(&panel, cx);
-
- new_dock.update(cx, |new_dock, cx| {
- new_dock.add_panel(panel.clone(), workspace.clone(), cx);
- if was_visible {
- new_dock.set_open(true, cx);
- new_dock.activate_panel(new_dock.panels_len() - 1, cx);
- }
- });
- }
- PanelEvent::ZoomIn => {
- this.set_panel_zoomed(&panel.to_any(), true, cx);
- if !panel.focus_handle(cx).contains_focused(cx) {
- cx.focus_view(&panel);
- }
- workspace
- .update(cx, |workspace, cx| {
- workspace.zoomed = Some(panel.downgrade().into());
- workspace.zoomed_position = Some(panel.read(cx).position(cx));
- })
- .ok();
- }
- PanelEvent::ZoomOut => {
- this.set_panel_zoomed(&panel.to_any(), false, cx);
- workspace
- .update(cx, |workspace, cx| {
- if workspace.zoomed_position == Some(this.position) {
- workspace.zoomed = None;
- workspace.zoomed_position = None;
- }
- cx.notify();
- })
- .ok();
- }
- // todo!() we do not use this event in the production code (even in zed1), remove it
- PanelEvent::Activate => {
- if let Some(ix) = this
- .panel_entries
- .iter()
- .position(|entry| entry.panel.panel_id() == Entity::entity_id(&panel))
- {
- this.set_open(true, cx);
- this.activate_panel(ix, cx);
- cx.focus_view(&panel);
- }
- }
- PanelEvent::Close => {
- if this
- .visible_panel()
- .map_or(false, |p| p.panel_id() == Entity::entity_id(&panel))
- {
- this.set_open(false, cx);
- }
- }
- PanelEvent::Focus => {}
- }),
- ];
-
- // todo!()
- // let dock_view_id = cx.view_id();
- self.panel_entries.push(PanelEntry {
- panel: Arc::new(panel),
- // todo!()
- // context_menu: cx.add_view(|cx| {
- // let mut menu = ContextMenu::new(dock_view_id, cx);
- // menu.set_position_mode(OverlayPositionMode::Local);
- // menu
- // }),
- _subscriptions: subscriptions,
- });
- cx.notify()
- }
-
- pub fn remove_panel<T: Panel>(&mut self, panel: &View<T>, cx: &mut ViewContext<Self>) {
- if let Some(panel_ix) = self
- .panel_entries
- .iter()
- .position(|entry| entry.panel.panel_id() == Entity::entity_id(panel))
- {
- if panel_ix == self.active_panel_index {
- self.active_panel_index = 0;
- self.set_open(false, cx);
- } else if panel_ix < self.active_panel_index {
- self.active_panel_index -= 1;
- }
- self.panel_entries.remove(panel_ix);
- cx.notify();
- }
- }
-
- pub fn panels_len(&self) -> usize {
- self.panel_entries.len()
- }
-
- pub fn activate_panel(&mut self, panel_ix: usize, cx: &mut ViewContext<Self>) {
- if panel_ix != self.active_panel_index {
- if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
- active_panel.panel.set_active(false, cx);
- }
-
- self.active_panel_index = panel_ix;
- if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
- active_panel.panel.set_active(true, cx);
- }
-
- cx.notify();
- }
- }
-
- pub fn visible_panel(&self) -> Option<&Arc<dyn PanelHandle>> {
- let entry = self.visible_entry()?;
- Some(&entry.panel)
- }
-
- pub fn active_panel(&self) -> Option<&Arc<dyn PanelHandle>> {
- Some(&self.panel_entries.get(self.active_panel_index)?.panel)
- }
-
- fn visible_entry(&self) -> Option<&PanelEntry> {
- if self.is_open {
- self.panel_entries.get(self.active_panel_index)
- } else {
- None
- }
- }
-
- pub fn zoomed_panel(&self, cx: &WindowContext) -> Option<Arc<dyn PanelHandle>> {
- let entry = self.visible_entry()?;
- if entry.panel.is_zoomed(cx) {
- Some(entry.panel.clone())
- } else {
- None
- }
- }
-
- pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<Pixels> {
- self.panel_entries
- .iter()
- .find(|entry| entry.panel.panel_id() == panel.panel_id())
- .map(|entry| entry.panel.size(cx))
- }
-
- pub fn active_panel_size(&self, cx: &WindowContext) -> Option<Pixels> {
- if self.is_open {
- self.panel_entries
- .get(self.active_panel_index)
- .map(|entry| entry.panel.size(cx))
- } else {
- None
- }
- }
-
- pub fn resize_active_panel(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
- if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) {
- let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE));
- entry.panel.set_size(size, cx);
- cx.notify();
- }
- }
-
- pub fn toggle_action(&self) -> Box<dyn Action> {
- match self.position {
- DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
- DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
- DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
- }
- }
-}
-
-impl Render for Dock {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- if let Some(entry) = self.visible_entry() {
- let size = entry.panel.size(cx);
-
- let position = self.position;
- let mut handle = div()
- .id("resize-handle")
- .on_drag(DraggedDock(position), |dock, cx| {
- cx.stop_propagation();
- cx.new_view(|_| dock.clone())
- })
- .on_click(cx.listener(|v, e: &ClickEvent, cx| {
- if e.down.button == MouseButton::Left && e.down.click_count == 2 {
- v.resize_active_panel(None, cx);
- cx.stop_propagation();
- }
- }))
- .z_index(1)
- .block_mouse();
-
- match self.position() {
- DockPosition::Left => {
- handle = handle
- .absolute()
- .right(px(0.))
- .top(px(0.))
- .h_full()
- .w(RESIZE_HANDLE_SIZE)
- .cursor_col_resize();
- }
- DockPosition::Bottom => {
- handle = handle
- .absolute()
- .top(px(0.))
- .left(px(0.))
- .w_full()
- .h(RESIZE_HANDLE_SIZE)
- .cursor_row_resize();
- }
- DockPosition::Right => {
- handle = handle
- .absolute()
- .top(px(0.))
- .left(px(0.))
- .h_full()
- .w(RESIZE_HANDLE_SIZE)
- .cursor_col_resize();
- }
- }
-
- div()
- .flex()
- .bg(cx.theme().colors().panel_background)
- .border_color(cx.theme().colors().border)
- .overflow_hidden()
- .map(|this| match self.position().axis() {
- Axis::Horizontal => this.w(size).h_full().flex_row(),
- Axis::Vertical => this.h(size).w_full().flex_col(),
- })
- .map(|this| match self.position() {
- DockPosition::Left => this.border_r(),
- DockPosition::Right => this.border_l(),
- DockPosition::Bottom => this.border_t(),
- })
- .child(
- div()
- .map(|this| match self.position().axis() {
- Axis::Horizontal => this.min_w(size).h_full(),
- Axis::Vertical => this.min_h(size).w_full(),
- })
- .child(entry.panel.to_any()),
- )
- .child(handle)
- } else {
- div()
- }
- }
-}
-
-impl PanelButtons {
- pub fn new(dock: View<Dock>, cx: &mut ViewContext<Self>) -> Self {
- cx.observe(&dock, |_, _, cx| cx.notify()).detach();
- Self { dock }
- }
-}
-
-impl Render for PanelButtons {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- // todo!()
- let dock = self.dock.read(cx);
- let active_index = dock.active_panel_index;
- let is_open = dock.is_open;
- let dock_position = dock.position;
-
- let (menu_anchor, menu_attach) = match dock.position {
- DockPosition::Left => (AnchorCorner::BottomLeft, AnchorCorner::TopLeft),
- DockPosition::Bottom | DockPosition::Right => {
- (AnchorCorner::BottomRight, AnchorCorner::TopRight)
- }
- };
-
- let buttons = dock
- .panel_entries
- .iter()
- .enumerate()
- .filter_map(|(i, entry)| {
- let icon = entry.panel.icon(cx)?;
- let icon_tooltip = entry.panel.icon_tooltip(cx)?;
- let name = entry.panel.persistent_name();
- let panel = entry.panel.clone();
-
- let is_active_button = i == active_index && is_open;
-
- let (action, tooltip) = if is_active_button {
- let action = dock.toggle_action();
-
- let tooltip: SharedString =
- format!("Close {} dock", dock.position.to_label()).into();
-
- (action, tooltip)
- } else {
- let action = entry.panel.toggle_action(cx);
-
- (action, icon_tooltip.into())
- };
-
- Some(
- right_click_menu(name)
- .menu(move |cx| {
- const POSITIONS: [DockPosition; 3] = [
- DockPosition::Left,
- DockPosition::Right,
- DockPosition::Bottom,
- ];
-
- ContextMenu::build(cx, |mut menu, cx| {
- for position in POSITIONS {
- if position != dock_position
- && panel.position_is_valid(position, cx)
- {
- let panel = panel.clone();
- menu = menu.entry(position.to_label(), None, move |cx| {
- panel.set_position(position, cx);
- })
- }
- }
- menu
- })
- })
- .anchor(menu_anchor)
- .attach(menu_attach)
- .trigger(
- IconButton::new(name, icon)
- .icon_size(IconSize::Small)
- .selected(is_active_button)
- .on_click({
- let action = action.boxed_clone();
- move |_, cx| cx.dispatch_action(action.boxed_clone())
- })
- .tooltip(move |cx| {
- Tooltip::for_action(tooltip.clone(), &*action, cx)
- }),
- ),
- )
- });
-
- h_stack().gap_0p5().children(buttons)
- }
-}
-
-impl StatusItemView for PanelButtons {
- fn set_active_pane_item(
- &mut self,
- _active_pane_item: Option<&dyn crate::ItemHandle>,
- _cx: &mut ViewContext<Self>,
- ) {
- // Nothing to do, panel buttons don't depend on the active center item
- }
-}
-
-#[cfg(any(test, feature = "test-support"))]
-pub mod test {
- use super::*;
- use gpui::{actions, div, ViewContext, WindowContext};
-
- pub struct TestPanel {
- pub position: DockPosition,
- pub zoomed: bool,
- pub active: bool,
- pub focus_handle: FocusHandle,
- pub size: Pixels,
- }
- actions!(test, [ToggleTestPanel]);
-
- impl EventEmitter<PanelEvent> for TestPanel {}
-
- impl TestPanel {
- pub fn new(position: DockPosition, cx: &mut WindowContext) -> Self {
- Self {
- position,
- zoomed: false,
- active: false,
- focus_handle: cx.focus_handle(),
- size: px(300.),
- }
- }
- }
-
- impl Render for TestPanel {
- fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
- div()
- }
- }
-
- impl Panel for TestPanel {
- fn persistent_name() -> &'static str {
- "TestPanel"
- }
-
- fn position(&self, _: &gpui::WindowContext) -> super::DockPosition {
- self.position
- }
-
- fn position_is_valid(&self, _: super::DockPosition) -> bool {
- true
- }
-
- fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
- self.position = position;
- cx.emit(PanelEvent::ChangePosition);
- }
-
- fn size(&self, _: &WindowContext) -> Pixels {
- self.size
- }
-
- fn set_size(&mut self, size: Option<Pixels>, _: &mut ViewContext<Self>) {
- self.size = size.unwrap_or(px(300.));
- }
-
- fn icon(&self, _: &WindowContext) -> Option<ui::Icon> {
- None
- }
-
- fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
- None
- }
-
- fn toggle_action(&self) -> Box<dyn Action> {
- ToggleTestPanel.boxed_clone()
- }
-
- fn is_zoomed(&self, _: &WindowContext) -> bool {
- self.zoomed
- }
-
- fn set_zoomed(&mut self, zoomed: bool, _cx: &mut ViewContext<Self>) {
- self.zoomed = zoomed;
- }
-
- fn set_active(&mut self, active: bool, _cx: &mut ViewContext<Self>) {
- self.active = active;
- }
- }
-
- impl FocusableView for TestPanel {
- fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
- self.focus_handle.clone()
- }
- }
-}
@@ -1,1070 +0,0 @@
-use crate::{
- pane::{self, Pane},
- persistence::model::ItemId,
- searchable::SearchableItemHandle,
- workspace_settings::{AutosaveSetting, WorkspaceSettings},
- DelayedDebouncedEditAction, FollowableItemBuilders, ItemNavHistory, ToolbarItemLocation,
- ViewId, Workspace, WorkspaceId,
-};
-use anyhow::Result;
-use client::{
- proto::{self, PeerId},
- Client,
-};
-use gpui::{
- AnyElement, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, FocusableView,
- HighlightStyle, Model, Pixels, Point, SharedString, Task, View, ViewContext, WeakView,
- WindowContext,
-};
-use project::{Project, ProjectEntryId, ProjectPath};
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::Settings;
-use smallvec::SmallVec;
-use std::{
- any::{Any, TypeId},
- cell::RefCell,
- ops::Range,
- path::PathBuf,
- rc::Rc,
- sync::{
- atomic::{AtomicBool, Ordering},
- Arc,
- },
- time::Duration,
-};
-use theme::Theme;
-
-#[derive(Deserialize)]
-pub struct ItemSettings {
- pub git_status: bool,
- pub close_position: ClosePosition,
-}
-
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "lowercase")]
-pub enum ClosePosition {
- Left,
- #[default]
- Right,
-}
-
-impl ClosePosition {
- pub fn right(&self) -> bool {
- match self {
- ClosePosition::Left => false,
- ClosePosition::Right => true,
- }
- }
-}
-
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
-pub struct ItemSettingsContent {
- git_status: Option<bool>,
- close_position: Option<ClosePosition>,
-}
-
-impl Settings for ItemSettings {
- const KEY: Option<&'static str> = Some("tabs");
-
- type FileContent = ItemSettingsContent;
-
- fn load(
- default_value: &Self::FileContent,
- user_values: &[&Self::FileContent],
- _: &mut AppContext,
- ) -> Result<Self> {
- Self::load_via_json_merge(default_value, user_values)
- }
-}
-
-#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)]
-pub enum ItemEvent {
- CloseItem,
- UpdateTab,
- UpdateBreadcrumbs,
- Edit,
-}
-
-// TODO: Combine this with existing HighlightedText struct?
-pub struct BreadcrumbText {
- pub text: String,
- pub highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
-}
-
-pub trait Item: FocusableView + EventEmitter<Self::Event> {
- type Event;
-
- fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
- fn workspace_deactivated(&mut self, _: &mut ViewContext<Self>) {}
- fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
- false
- }
- fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
- None
- }
- fn tab_description(&self, _: usize, _: &AppContext) -> Option<SharedString> {
- None
- }
- fn tab_content(&self, detail: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement;
-
- /// (model id, Item)
- fn for_each_project_item(
- &self,
- _: &AppContext,
- _: &mut dyn FnMut(EntityId, &dyn project::Item),
- ) {
- }
- fn is_singleton(&self, _cx: &AppContext) -> bool {
- false
- }
- fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>) {}
- fn clone_on_split(
- &self,
- _workspace_id: WorkspaceId,
- _: &mut ViewContext<Self>,
- ) -> Option<View<Self>>
- where
- Self: Sized,
- {
- None
- }
- fn is_dirty(&self, _: &AppContext) -> bool {
- false
- }
- fn has_conflict(&self, _: &AppContext) -> bool {
- false
- }
- fn can_save(&self, _cx: &AppContext) -> bool {
- false
- }
- fn save(&mut self, _project: Model<Project>, _cx: &mut ViewContext<Self>) -> Task<Result<()>> {
- unimplemented!("save() must be implemented if can_save() returns true")
- }
- fn save_as(
- &mut self,
- _project: Model<Project>,
- _abs_path: PathBuf,
- _cx: &mut ViewContext<Self>,
- ) -> Task<Result<()>> {
- unimplemented!("save_as() must be implemented if can_save() returns true")
- }
- fn reload(
- &mut self,
- _project: Model<Project>,
- _cx: &mut ViewContext<Self>,
- ) -> Task<Result<()>> {
- unimplemented!("reload() must be implemented if can_save() returns true")
- }
-
- fn to_item_events(event: &Self::Event, f: impl FnMut(ItemEvent));
-
- fn act_as_type<'a>(
- &'a self,
- type_id: TypeId,
- self_handle: &'a View<Self>,
- _: &'a AppContext,
- ) -> Option<AnyView> {
- if TypeId::of::<Self>() == type_id {
- Some(self_handle.clone().into())
- } else {
- None
- }
- }
-
- fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
- None
- }
-
- fn breadcrumb_location(&self) -> ToolbarItemLocation {
- ToolbarItemLocation::Hidden
- }
-
- fn breadcrumbs(&self, _theme: &Theme, _cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
- None
- }
-
- fn added_to_workspace(&mut self, _workspace: &mut Workspace, _cx: &mut ViewContext<Self>) {}
-
- fn serialized_item_kind() -> Option<&'static str> {
- None
- }
-
- fn deserialize(
- _project: Model<Project>,
- _workspace: WeakView<Workspace>,
- _workspace_id: WorkspaceId,
- _item_id: ItemId,
- _cx: &mut ViewContext<Pane>,
- ) -> Task<Result<View<Self>>> {
- unimplemented!(
- "deserialize() must be implemented if serialized_item_kind() returns Some(_)"
- )
- }
- fn show_toolbar(&self) -> bool {
- true
- }
- fn pixel_position_of_cursor(&self, _: &AppContext) -> Option<Point<Pixels>> {
- None
- }
-}
-
-pub trait ItemHandle: 'static + Send {
- fn subscribe_to_item_events(
- &self,
- cx: &mut WindowContext,
- handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
- ) -> gpui::Subscription;
- fn focus_handle(&self, cx: &WindowContext) -> FocusHandle;
- fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString>;
- fn tab_description(&self, detail: usize, cx: &AppContext) -> Option<SharedString>;
- fn tab_content(&self, detail: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement;
- fn dragged_tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement;
- fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
- fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
- fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[EntityId; 3]>;
- fn for_each_project_item(
- &self,
- _: &AppContext,
- _: &mut dyn FnMut(EntityId, &dyn project::Item),
- );
- fn is_singleton(&self, cx: &AppContext) -> bool;
- fn boxed_clone(&self) -> Box<dyn ItemHandle>;
- fn clone_on_split(
- &self,
- workspace_id: WorkspaceId,
- cx: &mut WindowContext,
- ) -> Option<Box<dyn ItemHandle>>;
- fn added_to_pane(
- &self,
- workspace: &mut Workspace,
- pane: View<Pane>,
- cx: &mut ViewContext<Workspace>,
- );
- fn deactivated(&self, cx: &mut WindowContext);
- fn workspace_deactivated(&self, cx: &mut WindowContext);
- fn navigate(&self, data: Box<dyn Any>, cx: &mut WindowContext) -> bool;
- fn item_id(&self) -> EntityId;
- fn to_any(&self) -> AnyView;
- fn is_dirty(&self, cx: &AppContext) -> bool;
- fn has_conflict(&self, cx: &AppContext) -> bool;
- fn can_save(&self, cx: &AppContext) -> bool;
- fn save(&self, project: Model<Project>, cx: &mut WindowContext) -> Task<Result<()>>;
- fn save_as(
- &self,
- project: Model<Project>,
- abs_path: PathBuf,
- cx: &mut WindowContext,
- ) -> Task<Result<()>>;
- fn reload(&self, project: Model<Project>, cx: &mut WindowContext) -> Task<Result<()>>;
- fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyView>;
- fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
- fn on_release(
- &self,
- cx: &mut AppContext,
- callback: Box<dyn FnOnce(&mut AppContext) + Send>,
- ) -> gpui::Subscription;
- fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
- fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation;
- fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>>;
- fn serialized_item_kind(&self) -> Option<&'static str>;
- fn show_toolbar(&self, cx: &AppContext) -> bool;
- fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>>;
-}
-
-pub trait WeakItemHandle: Send + Sync {
- fn id(&self) -> EntityId;
- fn upgrade(&self) -> Option<Box<dyn ItemHandle>>;
-}
-
-impl dyn ItemHandle {
- pub fn downcast<V: 'static>(&self) -> Option<View<V>> {
- self.to_any().downcast().ok()
- }
-
- pub fn act_as<V: 'static>(&self, cx: &AppContext) -> Option<View<V>> {
- self.act_as_type(TypeId::of::<V>(), cx)
- .and_then(|t| t.downcast().ok())
- }
-}
-
-impl<T: Item> ItemHandle for View<T> {
- fn subscribe_to_item_events(
- &self,
- cx: &mut WindowContext,
- handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
- ) -> gpui::Subscription {
- cx.subscribe(self, move |_, event, cx| {
- T::to_item_events(event, |item_event| handler(item_event, cx));
- })
- }
-
- fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
- self.focus_handle(cx)
- }
-
- fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
- self.read(cx).tab_tooltip_text(cx)
- }
-
- fn tab_description(&self, detail: usize, cx: &AppContext) -> Option<SharedString> {
- self.read(cx).tab_description(detail, cx)
- }
-
- fn tab_content(&self, detail: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement {
- self.read(cx).tab_content(detail, selected, cx)
- }
-
- fn dragged_tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement {
- self.read(cx).tab_content(detail, true, cx)
- }
-
- fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
- let this = self.read(cx);
- let mut result = None;
- if this.is_singleton(cx) {
- this.for_each_project_item(cx, &mut |_, item| {
- result = item.project_path(cx);
- });
- }
- result
- }
-
- fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
- let mut result = SmallVec::new();
- self.read(cx).for_each_project_item(cx, &mut |_, item| {
- if let Some(id) = item.entry_id(cx) {
- result.push(id);
- }
- });
- result
- }
-
- fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[EntityId; 3]> {
- let mut result = SmallVec::new();
- self.read(cx).for_each_project_item(cx, &mut |id, _| {
- result.push(id);
- });
- result
- }
-
- fn for_each_project_item(
- &self,
- cx: &AppContext,
- f: &mut dyn FnMut(EntityId, &dyn project::Item),
- ) {
- self.read(cx).for_each_project_item(cx, f)
- }
-
- fn is_singleton(&self, cx: &AppContext) -> bool {
- self.read(cx).is_singleton(cx)
- }
-
- fn boxed_clone(&self) -> Box<dyn ItemHandle> {
- Box::new(self.clone())
- }
-
- fn clone_on_split(
- &self,
- workspace_id: WorkspaceId,
- cx: &mut WindowContext,
- ) -> Option<Box<dyn ItemHandle>> {
- self.update(cx, |item, cx| item.clone_on_split(workspace_id, cx))
- .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
- }
-
- fn added_to_pane(
- &self,
- workspace: &mut Workspace,
- pane: View<Pane>,
- cx: &mut ViewContext<Workspace>,
- ) {
- let weak_item = self.downgrade();
- let history = pane.read(cx).nav_history_for_item(self);
- self.update(cx, |this, cx| {
- this.set_nav_history(history, cx);
- this.added_to_workspace(workspace, cx);
- });
-
- if let Some(followed_item) = self.to_followable_item_handle(cx) {
- if let Some(message) = followed_item.to_state_proto(cx) {
- workspace.update_followers(
- followed_item.is_project_item(cx),
- proto::update_followers::Variant::CreateView(proto::View {
- id: followed_item
- .remote_id(&workspace.app_state.client, cx)
- .map(|id| id.to_proto()),
- variant: Some(message),
- leader_id: workspace.leader_for_pane(&pane),
- }),
- cx,
- );
- }
- }
-
- if workspace
- .panes_by_item
- .insert(self.item_id(), pane.downgrade())
- .is_none()
- {
- let mut pending_autosave = DelayedDebouncedEditAction::new();
- let pending_update = Rc::new(RefCell::new(None));
- let pending_update_scheduled = Arc::new(AtomicBool::new(false));
-
- let mut event_subscription =
- Some(cx.subscribe(self, move |workspace, item, event, cx| {
- let pane = if let Some(pane) = workspace
- .panes_by_item
- .get(&item.item_id())
- .and_then(|pane| pane.upgrade())
- {
- pane
- } else {
- log::error!("unexpected item event after pane was dropped");
- return;
- };
-
- if let Some(item) = item.to_followable_item_handle(cx) {
- let is_project_item = item.is_project_item(cx);
- let leader_id = workspace.leader_for_pane(&pane);
-
- let follow_event = item.to_follow_event(event);
- if leader_id.is_some()
- && matches!(follow_event, Some(FollowEvent::Unfollow))
- {
- workspace.unfollow(&pane, cx);
- }
-
- if item.add_event_to_update_proto(
- event,
- &mut *pending_update.borrow_mut(),
- cx,
- ) && !pending_update_scheduled.load(Ordering::SeqCst)
- {
- pending_update_scheduled.store(true, Ordering::SeqCst);
- cx.on_next_frame({
- let pending_update = pending_update.clone();
- let pending_update_scheduled = pending_update_scheduled.clone();
- move |this, cx| {
- pending_update_scheduled.store(false, Ordering::SeqCst);
- this.update_followers(
- is_project_item,
- proto::update_followers::Variant::UpdateView(
- proto::UpdateView {
- id: item
- .remote_id(&this.app_state.client, cx)
- .map(|id| id.to_proto()),
- variant: pending_update.borrow_mut().take(),
- leader_id,
- },
- ),
- cx,
- );
- }
- });
- }
- }
-
- T::to_item_events(event, |event| match event {
- ItemEvent::CloseItem => {
- pane.update(cx, |pane, cx| {
- pane.close_item_by_id(item.item_id(), crate::SaveIntent::Close, cx)
- })
- .detach_and_log_err(cx);
- return;
- }
-
- ItemEvent::UpdateTab => {
- pane.update(cx, |_, cx| {
- cx.emit(pane::Event::ChangeItemTitle);
- cx.notify();
- });
- }
-
- ItemEvent::Edit => {
- let autosave = WorkspaceSettings::get_global(cx).autosave;
- if let AutosaveSetting::AfterDelay { milliseconds } = autosave {
- let delay = Duration::from_millis(milliseconds);
- let item = item.clone();
- pending_autosave.fire_new(delay, cx, move |workspace, cx| {
- Pane::autosave_item(&item, workspace.project().clone(), cx)
- });
- }
- }
-
- _ => {}
- });
- }));
-
- cx.on_blur(&self.focus_handle(cx), move |workspace, cx| {
- if WorkspaceSettings::get_global(cx).autosave == AutosaveSetting::OnFocusChange {
- if let Some(item) = weak_item.upgrade() {
- Pane::autosave_item(&item, workspace.project.clone(), cx)
- .detach_and_log_err(cx);
- }
- }
- })
- .detach();
-
- let item_id = self.item_id();
- cx.observe_release(self, move |workspace, _, _| {
- workspace.panes_by_item.remove(&item_id);
- event_subscription.take();
- })
- .detach();
- }
-
- cx.defer(|workspace, cx| {
- workspace.serialize_workspace(cx);
- });
- }
-
- fn deactivated(&self, cx: &mut WindowContext) {
- self.update(cx, |this, cx| this.deactivated(cx));
- }
-
- fn workspace_deactivated(&self, cx: &mut WindowContext) {
- self.update(cx, |this, cx| this.workspace_deactivated(cx));
- }
-
- fn navigate(&self, data: Box<dyn Any>, cx: &mut WindowContext) -> bool {
- self.update(cx, |this, cx| this.navigate(data, cx))
- }
-
- fn item_id(&self) -> EntityId {
- self.entity_id()
- }
-
- fn to_any(&self) -> AnyView {
- self.clone().into()
- }
-
- fn is_dirty(&self, cx: &AppContext) -> bool {
- self.read(cx).is_dirty(cx)
- }
-
- fn has_conflict(&self, cx: &AppContext) -> bool {
- self.read(cx).has_conflict(cx)
- }
-
- fn can_save(&self, cx: &AppContext) -> bool {
- self.read(cx).can_save(cx)
- }
-
- fn save(&self, project: Model<Project>, cx: &mut WindowContext) -> Task<Result<()>> {
- self.update(cx, |item, cx| item.save(project, cx))
- }
-
- fn save_as(
- &self,
- project: Model<Project>,
- abs_path: PathBuf,
- cx: &mut WindowContext,
- ) -> Task<anyhow::Result<()>> {
- self.update(cx, |item, cx| item.save_as(project, abs_path, cx))
- }
-
- fn reload(&self, project: Model<Project>, cx: &mut WindowContext) -> Task<Result<()>> {
- self.update(cx, |item, cx| item.reload(project, cx))
- }
-
- fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a AppContext) -> Option<AnyView> {
- self.read(cx).act_as_type(type_id, self, cx)
- }
-
- fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>> {
- if cx.has_global::<FollowableItemBuilders>() {
- let builders = cx.global::<FollowableItemBuilders>();
- let item = self.to_any();
- Some(builders.get(&item.entity_type())?.1(&item))
- } else {
- None
- }
- }
-
- fn on_release(
- &self,
- cx: &mut AppContext,
- callback: Box<dyn FnOnce(&mut AppContext) + Send>,
- ) -> gpui::Subscription {
- cx.observe_release(self, move |_, cx| callback(cx))
- }
-
- fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
- self.read(cx).as_searchable(self)
- }
-
- fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation {
- self.read(cx).breadcrumb_location()
- }
-
- fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
- self.read(cx).breadcrumbs(theme, cx)
- }
-
- fn serialized_item_kind(&self) -> Option<&'static str> {
- T::serialized_item_kind()
- }
-
- fn show_toolbar(&self, cx: &AppContext) -> bool {
- self.read(cx).show_toolbar()
- }
-
- fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
- self.read(cx).pixel_position_of_cursor(cx)
- }
-}
-
-impl From<Box<dyn ItemHandle>> for AnyView {
- fn from(val: Box<dyn ItemHandle>) -> Self {
- val.to_any()
- }
-}
-
-impl From<&Box<dyn ItemHandle>> for AnyView {
- fn from(val: &Box<dyn ItemHandle>) -> Self {
- val.to_any()
- }
-}
-
-impl Clone for Box<dyn ItemHandle> {
- fn clone(&self) -> Box<dyn ItemHandle> {
- self.boxed_clone()
- }
-}
-
-impl<T: Item> WeakItemHandle for WeakView<T> {
- fn id(&self) -> EntityId {
- self.entity_id()
- }
-
- fn upgrade(&self) -> Option<Box<dyn ItemHandle>> {
- self.upgrade().map(|v| Box::new(v) as Box<dyn ItemHandle>)
- }
-}
-
-pub trait ProjectItem: Item {
- type Item: project::Item;
-
- fn for_project_item(
- project: Model<Project>,
- item: Model<Self::Item>,
- cx: &mut ViewContext<Self>,
- ) -> Self
- where
- Self: Sized;
-}
-
-pub enum FollowEvent {
- Unfollow,
-}
-
-pub trait FollowableItem: Item {
- fn remote_id(&self) -> Option<ViewId>;
- fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant>;
- fn from_state_proto(
- pane: View<Pane>,
- project: View<Workspace>,
- id: ViewId,
- state: &mut Option<proto::view::Variant>,
- cx: &mut WindowContext,
- ) -> Option<Task<Result<View<Self>>>>;
- fn to_follow_event(event: &Self::Event) -> Option<FollowEvent>;
- fn add_event_to_update_proto(
- &self,
- event: &Self::Event,
- update: &mut Option<proto::update_view::Variant>,
- cx: &WindowContext,
- ) -> bool;
- fn apply_update_proto(
- &mut self,
- project: &Model<Project>,
- message: proto::update_view::Variant,
- cx: &mut ViewContext<Self>,
- ) -> Task<Result<()>>;
- fn is_project_item(&self, cx: &WindowContext) -> bool;
- fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>);
-}
-
-pub trait FollowableItemHandle: ItemHandle {
- fn remote_id(&self, client: &Arc<Client>, cx: &WindowContext) -> Option<ViewId>;
- fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext);
- fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant>;
- fn add_event_to_update_proto(
- &self,
- event: &dyn Any,
- update: &mut Option<proto::update_view::Variant>,
- cx: &WindowContext,
- ) -> bool;
- fn to_follow_event(&self, event: &dyn Any) -> Option<FollowEvent>;
- fn apply_update_proto(
- &self,
- project: &Model<Project>,
- message: proto::update_view::Variant,
- cx: &mut WindowContext,
- ) -> Task<Result<()>>;
- fn is_project_item(&self, cx: &WindowContext) -> bool;
-}
-
-impl<T: FollowableItem> FollowableItemHandle for View<T> {
- fn remote_id(&self, client: &Arc<Client>, cx: &WindowContext) -> Option<ViewId> {
- self.read(cx).remote_id().or_else(|| {
- client.peer_id().map(|creator| ViewId {
- creator,
- id: self.item_id().as_u64(),
- })
- })
- }
-
- fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext) {
- self.update(cx, |this, cx| this.set_leader_peer_id(leader_peer_id, cx))
- }
-
- fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
- self.read(cx).to_state_proto(cx)
- }
-
- fn add_event_to_update_proto(
- &self,
- event: &dyn Any,
- update: &mut Option<proto::update_view::Variant>,
- cx: &WindowContext,
- ) -> bool {
- if let Some(event) = event.downcast_ref() {
- self.read(cx).add_event_to_update_proto(event, update, cx)
- } else {
- false
- }
- }
-
- fn to_follow_event(&self, event: &dyn Any) -> Option<FollowEvent> {
- T::to_follow_event(event.downcast_ref()?)
- }
-
- fn apply_update_proto(
- &self,
- project: &Model<Project>,
- message: proto::update_view::Variant,
- cx: &mut WindowContext,
- ) -> Task<Result<()>> {
- self.update(cx, |this, cx| this.apply_update_proto(project, message, cx))
- }
-
- fn is_project_item(&self, cx: &WindowContext) -> bool {
- self.read(cx).is_project_item(cx)
- }
-}
-
-#[cfg(any(test, feature = "test-support"))]
-pub mod test {
- use super::{Item, ItemEvent};
- use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId};
- use gpui::{
- AnyElement, AppContext, Context as _, EntityId, EventEmitter, FocusableView,
- InteractiveElement, IntoElement, Model, Render, SharedString, Task, View, ViewContext,
- VisualContext, WeakView,
- };
- use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
- use std::{any::Any, cell::Cell, path::Path};
-
- pub struct TestProjectItem {
- pub entry_id: Option<ProjectEntryId>,
- pub project_path: Option<ProjectPath>,
- }
-
- pub struct TestItem {
- pub workspace_id: WorkspaceId,
- pub state: String,
- pub label: String,
- pub save_count: usize,
- pub save_as_count: usize,
- pub reload_count: usize,
- pub is_dirty: bool,
- pub is_singleton: bool,
- pub has_conflict: bool,
- pub project_items: Vec<Model<TestProjectItem>>,
- pub nav_history: Option<ItemNavHistory>,
- pub tab_descriptions: Option<Vec<&'static str>>,
- pub tab_detail: Cell<Option<usize>>,
- focus_handle: gpui::FocusHandle,
- }
-
- impl project::Item for TestProjectItem {
- fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
- self.entry_id
- }
-
- fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
- self.project_path.clone()
- }
- }
-
- pub enum TestItemEvent {
- Edit,
- }
-
- // impl Clone for TestItem {
- // fn clone(&self) -> Self {
- // Self {
- // state: self.state.clone(),
- // label: self.label.clone(),
- // save_count: self.save_count,
- // save_as_count: self.save_as_count,
- // reload_count: self.reload_count,
- // is_dirty: self.is_dirty,
- // is_singleton: self.is_singleton,
- // has_conflict: self.has_conflict,
- // project_items: self.project_items.clone(),
- // nav_history: None,
- // tab_descriptions: None,
- // tab_detail: Default::default(),
- // workspace_id: self.workspace_id,
- // focus_handle: self.focus_handle.clone(),
- // }
- // }
- // }
-
- impl TestProjectItem {
- pub fn new(id: u64, path: &str, cx: &mut AppContext) -> Model<Self> {
- let entry_id = Some(ProjectEntryId::from_proto(id));
- let project_path = Some(ProjectPath {
- worktree_id: WorktreeId::from_usize(0),
- path: Path::new(path).into(),
- });
- cx.new_model(|_| Self {
- entry_id,
- project_path,
- })
- }
-
- pub fn new_untitled(cx: &mut AppContext) -> Model<Self> {
- cx.new_model(|_| Self {
- project_path: None,
- entry_id: None,
- })
- }
- }
-
- impl TestItem {
- pub fn new(cx: &mut ViewContext<Self>) -> Self {
- Self {
- state: String::new(),
- label: String::new(),
- save_count: 0,
- save_as_count: 0,
- reload_count: 0,
- is_dirty: false,
- has_conflict: false,
- project_items: Vec::new(),
- is_singleton: true,
- nav_history: None,
- tab_descriptions: None,
- tab_detail: Default::default(),
- workspace_id: 0,
- focus_handle: cx.focus_handle(),
- }
- }
-
- pub fn new_deserialized(id: WorkspaceId, cx: &mut ViewContext<Self>) -> Self {
- let mut this = Self::new(cx);
- this.workspace_id = id;
- this
- }
-
- pub fn with_label(mut self, state: &str) -> Self {
- self.label = state.to_string();
- self
- }
-
- pub fn with_singleton(mut self, singleton: bool) -> Self {
- self.is_singleton = singleton;
- self
- }
-
- pub fn with_dirty(mut self, dirty: bool) -> Self {
- self.is_dirty = dirty;
- self
- }
-
- pub fn with_conflict(mut self, has_conflict: bool) -> Self {
- self.has_conflict = has_conflict;
- self
- }
-
- pub fn with_project_items(mut self, items: &[Model<TestProjectItem>]) -> Self {
- self.project_items.clear();
- self.project_items.extend(items.iter().cloned());
- self
- }
-
- pub fn set_state(&mut self, state: String, cx: &mut ViewContext<Self>) {
- self.push_to_nav_history(cx);
- self.state = state;
- }
-
- fn push_to_nav_history(&mut self, cx: &mut ViewContext<Self>) {
- if let Some(history) = &mut self.nav_history {
- history.push(Some(Box::new(self.state.clone())), cx);
- }
- }
- }
-
- impl Render for TestItem {
- fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
- gpui::div().track_focus(&self.focus_handle)
- }
- }
-
- impl EventEmitter<ItemEvent> for TestItem {}
-
- impl FocusableView for TestItem {
- fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
- self.focus_handle.clone()
- }
- }
-
- impl Item for TestItem {
- type Event = ItemEvent;
-
- fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
- f(*event)
- }
-
- fn tab_description(&self, detail: usize, _: &AppContext) -> Option<SharedString> {
- self.tab_descriptions.as_ref().and_then(|descriptions| {
- let description = *descriptions.get(detail).or_else(|| descriptions.last())?;
- Some(description.into())
- })
- }
-
- fn tab_content(
- &self,
- detail: Option<usize>,
- _selected: bool,
- _cx: &ui::prelude::WindowContext,
- ) -> AnyElement {
- self.tab_detail.set(detail);
- gpui::div().into_any_element()
- }
-
- fn for_each_project_item(
- &self,
- cx: &AppContext,
- f: &mut dyn FnMut(EntityId, &dyn project::Item),
- ) {
- self.project_items
- .iter()
- .for_each(|item| f(item.entity_id(), item.read(cx)))
- }
-
- fn is_singleton(&self, _: &AppContext) -> bool {
- self.is_singleton
- }
-
- fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
- self.nav_history = Some(history);
- }
-
- fn navigate(&mut self, state: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
- let state = *state.downcast::<String>().unwrap_or_default();
- if state != self.state {
- self.state = state;
- true
- } else {
- false
- }
- }
-
- fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
- self.push_to_nav_history(cx);
- }
-
- fn clone_on_split(
- &self,
- _workspace_id: WorkspaceId,
- cx: &mut ViewContext<Self>,
- ) -> Option<View<Self>>
- where
- Self: Sized,
- {
- Some(cx.new_view(|cx| Self {
- state: self.state.clone(),
- label: self.label.clone(),
- save_count: self.save_count,
- save_as_count: self.save_as_count,
- reload_count: self.reload_count,
- is_dirty: self.is_dirty,
- is_singleton: self.is_singleton,
- has_conflict: self.has_conflict,
- project_items: self.project_items.clone(),
- nav_history: None,
- tab_descriptions: None,
- tab_detail: Default::default(),
- workspace_id: self.workspace_id,
- focus_handle: cx.focus_handle(),
- }))
- }
-
- fn is_dirty(&self, _: &AppContext) -> bool {
- self.is_dirty
- }
-
- fn has_conflict(&self, _: &AppContext) -> bool {
- self.has_conflict
- }
-
- fn can_save(&self, cx: &AppContext) -> bool {
- !self.project_items.is_empty()
- && self
- .project_items
- .iter()
- .all(|item| item.read(cx).entry_id.is_some())
- }
-
- fn save(
- &mut self,
- _: Model<Project>,
- _: &mut ViewContext<Self>,
- ) -> Task<anyhow::Result<()>> {
- self.save_count += 1;
- self.is_dirty = false;
- Task::ready(Ok(()))
- }
-
- fn save_as(
- &mut self,
- _: Model<Project>,
- _: std::path::PathBuf,
- _: &mut ViewContext<Self>,
- ) -> Task<anyhow::Result<()>> {
- self.save_as_count += 1;
- self.is_dirty = false;
- Task::ready(Ok(()))
- }
-
- fn reload(
- &mut self,
- _: Model<Project>,
- _: &mut ViewContext<Self>,
- ) -> Task<anyhow::Result<()>> {
- self.reload_count += 1;
- self.is_dirty = false;
- Task::ready(Ok(()))
- }
-
- fn serialized_item_kind() -> Option<&'static str> {
- Some("TestItem")
- }
-
- fn deserialize(
- _project: Model<Project>,
- _workspace: WeakView<Workspace>,
- workspace_id: WorkspaceId,
- _item_id: ItemId,
- cx: &mut ViewContext<Pane>,
- ) -> Task<anyhow::Result<View<Self>>> {
- let view = cx.new_view(|cx| Self::new_deserialized(workspace_id, cx));
- Task::Ready(Some(anyhow::Ok(view)))
- }
- }
-}
@@ -1,395 +0,0 @@
-use crate::{Toast, Workspace};
-use collections::HashMap;
-use gpui::{
- AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, Render,
- View, ViewContext, VisualContext,
-};
-use std::{any::TypeId, ops::DerefMut};
-
-pub fn init(cx: &mut AppContext) {
- cx.set_global(NotificationTracker::new());
- // todo!()
- // simple_message_notification::init(cx);
-}
-
-pub trait Notification: EventEmitter<DismissEvent> + Render {}
-
-impl<V: EventEmitter<DismissEvent> + Render> Notification for V {}
-
-pub trait NotificationHandle: Send {
- fn id(&self) -> EntityId;
- fn to_any(&self) -> AnyView;
-}
-
-impl<T: Notification> NotificationHandle for View<T> {
- fn id(&self) -> EntityId {
- self.entity_id()
- }
-
- fn to_any(&self) -> AnyView {
- self.clone().into()
- }
-}
-
-impl From<&dyn NotificationHandle> for AnyView {
- fn from(val: &dyn NotificationHandle) -> Self {
- val.to_any()
- }
-}
-
-pub(crate) struct NotificationTracker {
- notifications_sent: HashMap<TypeId, Vec<usize>>,
-}
-
-impl std::ops::Deref for NotificationTracker {
- type Target = HashMap<TypeId, Vec<usize>>;
-
- fn deref(&self) -> &Self::Target {
- &self.notifications_sent
- }
-}
-
-impl DerefMut for NotificationTracker {
- fn deref_mut(&mut self) -> &mut Self::Target {
- &mut self.notifications_sent
- }
-}
-
-impl NotificationTracker {
- fn new() -> Self {
- Self {
- notifications_sent: Default::default(),
- }
- }
-}
-
-impl Workspace {
- pub fn has_shown_notification_once<V: Notification>(
- &self,
- id: usize,
- cx: &ViewContext<Self>,
- ) -> bool {
- cx.global::<NotificationTracker>()
- .get(&TypeId::of::<V>())
- .map(|ids| ids.contains(&id))
- .unwrap_or(false)
- }
-
- pub fn show_notification_once<V: Notification>(
- &mut self,
- id: usize,
- cx: &mut ViewContext<Self>,
- build_notification: impl FnOnce(&mut ViewContext<Self>) -> View<V>,
- ) {
- if !self.has_shown_notification_once::<V>(id, cx) {
- let tracker = cx.global_mut::<NotificationTracker>();
- let entry = tracker.entry(TypeId::of::<V>()).or_default();
- entry.push(id);
- self.show_notification::<V>(id, cx, build_notification)
- }
- }
-
- pub fn show_notification<V: Notification>(
- &mut self,
- id: usize,
- cx: &mut ViewContext<Self>,
- build_notification: impl FnOnce(&mut ViewContext<Self>) -> View<V>,
- ) {
- let type_id = TypeId::of::<V>();
- if self
- .notifications
- .iter()
- .all(|(existing_type_id, existing_id, _)| {
- (*existing_type_id, *existing_id) != (type_id, id)
- })
- {
- let notification = build_notification(cx);
- cx.subscribe(¬ification, move |this, _, _: &DismissEvent, cx| {
- this.dismiss_notification_internal(type_id, id, cx);
- })
- .detach();
- self.notifications
- .push((type_id, id, Box::new(notification)));
- cx.notify();
- }
- }
-
- pub fn show_error<E>(&mut self, err: &E, cx: &mut ViewContext<Self>)
- where
- E: std::fmt::Debug,
- {
- self.show_notification(0, cx, |cx| {
- cx.new_view(|_cx| {
- simple_message_notification::MessageNotification::new(format!("Error: {err:?}"))
- })
- });
- }
-
- pub fn dismiss_notification<V: Notification>(&mut self, id: usize, cx: &mut ViewContext<Self>) {
- let type_id = TypeId::of::<V>();
-
- self.dismiss_notification_internal(type_id, id, cx)
- }
-
- pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
- self.dismiss_notification::<simple_message_notification::MessageNotification>(toast.id, cx);
- self.show_notification(toast.id, cx, |cx| {
- cx.new_view(|_cx| match toast.on_click.as_ref() {
- Some((click_msg, on_click)) => {
- let on_click = on_click.clone();
- simple_message_notification::MessageNotification::new(toast.msg.clone())
- .with_click_message(click_msg.clone())
- .on_click(move |cx| on_click(cx))
- }
- None => simple_message_notification::MessageNotification::new(toast.msg.clone()),
- })
- })
- }
-
- pub fn dismiss_toast(&mut self, id: usize, cx: &mut ViewContext<Self>) {
- self.dismiss_notification::<simple_message_notification::MessageNotification>(id, cx);
- }
-
- fn dismiss_notification_internal(
- &mut self,
- type_id: TypeId,
- id: usize,
- cx: &mut ViewContext<Self>,
- ) {
- self.notifications
- .retain(|(existing_type_id, existing_id, _)| {
- if (*existing_type_id, *existing_id) == (type_id, id) {
- cx.notify();
- false
- } else {
- true
- }
- });
- }
-}
-
-pub mod simple_message_notification {
- use gpui::{
- div, DismissEvent, EventEmitter, InteractiveElement, ParentElement, Render, SharedString,
- StatefulInteractiveElement, Styled, ViewContext,
- };
- use std::sync::Arc;
- use ui::prelude::*;
- use ui::{h_stack, v_stack, Button, Icon, IconElement, Label, StyledExt};
-
- pub struct MessageNotification {
- message: SharedString,
- on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
- click_message: Option<SharedString>,
- }
-
- impl EventEmitter<DismissEvent> for MessageNotification {}
-
- impl MessageNotification {
- pub fn new<S>(message: S) -> MessageNotification
- where
- S: Into<SharedString>,
- {
- Self {
- message: message.into(),
- on_click: None,
- click_message: None,
- }
- }
-
- pub fn with_click_message<S>(mut self, message: S) -> Self
- where
- S: Into<SharedString>,
- {
- self.click_message = Some(message.into());
- self
- }
-
- pub fn on_click<F>(mut self, on_click: F) -> Self
- where
- F: 'static + Fn(&mut ViewContext<Self>),
- {
- self.on_click = Some(Arc::new(on_click));
- self
- }
-
- pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
- cx.emit(DismissEvent);
- }
- }
-
- impl Render for MessageNotification {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- v_stack()
- .elevation_3(cx)
- .p_4()
- .child(
- h_stack()
- .justify_between()
- .child(div().max_w_80().child(Label::new(self.message.clone())))
- .child(
- div()
- .id("cancel")
- .child(IconElement::new(Icon::Close))
- .cursor_pointer()
- .on_click(cx.listener(|this, _, cx| this.dismiss(cx))),
- ),
- )
- .children(self.click_message.iter().map(|message| {
- Button::new(message.clone(), message.clone()).on_click(cx.listener(
- |this, _, cx| {
- if let Some(on_click) = this.on_click.as_ref() {
- (on_click)(cx)
- };
- this.dismiss(cx)
- },
- ))
- }))
- }
- }
- // todo!()
- // impl View for MessageNotification {
- // fn ui_name() -> &'static str {
- // "MessageNotification"
- // }
-
- // fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
- // let theme = theme2::current(cx).clone();
- // let theme = &theme.simple_message_notification;
-
- // enum MessageNotificationTag {}
-
- // let click_message = self.click_message.clone();
- // let message = match &self.message {
- // NotificationMessage::Text(text) => {
- // Text::new(text.to_owned(), theme.message.text.clone()).into_any()
- // }
- // NotificationMessage::Element(e) => e(theme.message.text.clone(), cx),
- // };
- // let on_click = self.on_click.clone();
- // let has_click_action = on_click.is_some();
-
- // Flex::column()
- // .with_child(
- // Flex::row()
- // .with_child(
- // message
- // .contained()
- // .with_style(theme.message.container)
- // .aligned()
- // .top()
- // .left()
- // .flex(1., true),
- // )
- // .with_child(
- // MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| {
- // let style = theme.dismiss_button.style_for(state);
- // Svg::new("icons/x.svg")
- // .with_color(style.color)
- // .constrained()
- // .with_width(style.icon_width)
- // .aligned()
- // .contained()
- // .with_style(style.container)
- // .constrained()
- // .with_width(style.button_width)
- // .with_height(style.button_width)
- // })
- // .with_padding(Padding::uniform(5.))
- // .on_click(MouseButton::Left, move |_, this, cx| {
- // this.dismiss(&Default::default(), cx);
- // })
- // .with_cursor_style(CursorStyle::PointingHand)
- // .aligned()
- // .constrained()
- // .with_height(cx.font_cache().line_height(theme.message.text.font_size))
- // .aligned()
- // .top()
- // .flex_float(),
- // ),
- // )
- // .with_children({
- // click_message
- // .map(|click_message| {
- // MouseEventHandler::new::<MessageNotificationTag, _>(
- // 0,
- // cx,
- // |state, _| {
- // let style = theme.action_message.style_for(state);
-
- // Flex::row()
- // .with_child(
- // Text::new(click_message, style.text.clone())
- // .contained()
- // .with_style(style.container),
- // )
- // .contained()
- // },
- // )
- // .on_click(MouseButton::Left, move |_, this, cx| {
- // if let Some(on_click) = on_click.as_ref() {
- // on_click(cx);
- // this.dismiss(&Default::default(), cx);
- // }
- // })
- // // Since we're not using a proper overlay, we have to capture these extra events
- // .on_down(MouseButton::Left, |_, _, _| {})
- // .on_up(MouseButton::Left, |_, _, _| {})
- // .with_cursor_style(if has_click_action {
- // CursorStyle::PointingHand
- // } else {
- // CursorStyle::Arrow
- // })
- // })
- // .into_iter()
- // })
- // .into_any()
- // }
- // }
-}
-
-pub trait NotifyResultExt {
- type Ok;
-
- fn notify_err(
- self,
- workspace: &mut Workspace,
- cx: &mut ViewContext<Workspace>,
- ) -> Option<Self::Ok>;
-
- fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<Self::Ok>;
-}
-
-impl<T, E> NotifyResultExt for Result<T, E>
-where
- E: std::fmt::Debug,
-{
- type Ok = T;
-
- fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<T> {
- match self {
- Ok(value) => Some(value),
- Err(err) => {
- log::error!("TODO {err:?}");
- workspace.show_error(&err, cx);
- None
- }
- }
- }
-
- fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<T> {
- match self {
- Ok(value) => Some(value),
- Err(err) => {
- log::error!("TODO {err:?}");
- cx.update(|view, cx| {
- if let Ok(workspace) = view.downcast::<Workspace>() {
- workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
- }
- })
- .ok();
- None
- }
- }
- }
-}
@@ -1,2759 +0,0 @@
-use crate::{
- item::{ClosePosition, Item, ItemHandle, ItemSettings, WeakItemHandle},
- toolbar::Toolbar,
- workspace_settings::{AutosaveSetting, WorkspaceSettings},
- NewCenterTerminal, NewFile, NewSearch, SplitDirection, ToggleZoom, Workspace,
-};
-use anyhow::Result;
-use collections::{HashMap, HashSet, VecDeque};
-use gpui::{
- actions, impl_actions, overlay, prelude::*, Action, AnchorCorner, AnyElement, AppContext,
- AsyncWindowContext, DismissEvent, Div, DragMoveEvent, EntityId, EventEmitter, FocusHandle,
- FocusableView, Model, MouseButton, NavigationDirection, Pixels, Point, PromptLevel, Render,
- ScrollHandle, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
-};
-use parking_lot::Mutex;
-use project::{Project, ProjectEntryId, ProjectPath};
-use serde::Deserialize;
-use settings::Settings;
-use std::{
- any::Any,
- cmp, fmt, mem,
- path::{Path, PathBuf},
- rc::Rc,
- sync::{
- atomic::{AtomicUsize, Ordering},
- Arc,
- },
-};
-use theme::ThemeSettings;
-
-use ui::{
- prelude::*, right_click_menu, ButtonSize, Color, Icon, IconButton, IconSize, Indicator, Label,
- Tab, TabBar, TabPosition, Tooltip,
-};
-use ui::{v_stack, ContextMenu};
-use util::{maybe, truncate_and_remove_front, ResultExt};
-
-#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-pub enum SaveIntent {
- /// write all files (even if unchanged)
- /// prompt before overwriting on-disk changes
- Save,
- /// write any files that have local changes
- /// prompt before overwriting on-disk changes
- SaveAll,
- /// always prompt for a new path
- SaveAs,
- /// prompt "you have unsaved changes" before writing
- Close,
- /// write all dirty files, don't prompt on conflict
- Overwrite,
- /// skip all save-related behavior
- Skip,
-}
-
-#[derive(Clone, Deserialize, PartialEq, Debug)]
-pub struct ActivateItem(pub usize);
-
-// #[derive(Clone, PartialEq)]
-// pub struct CloseItemById {
-// pub item_id: usize,
-// pub pane: WeakView<Pane>,
-// }
-
-// #[derive(Clone, PartialEq)]
-// pub struct CloseItemsToTheLeftById {
-// pub item_id: usize,
-// pub pane: WeakView<Pane>,
-// }
-
-// #[derive(Clone, PartialEq)]
-// pub struct CloseItemsToTheRightById {
-// pub item_id: usize,
-// pub pane: WeakView<Pane>,
-// }
-
-#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
-#[serde(rename_all = "camelCase")]
-pub struct CloseActiveItem {
- pub save_intent: Option<SaveIntent>,
-}
-
-#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
-#[serde(rename_all = "camelCase")]
-pub struct CloseAllItems {
- pub save_intent: Option<SaveIntent>,
-}
-
-#[derive(Clone, PartialEq, Debug, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct RevealInProjectPanel {
- pub entry_id: u64,
-}
-
-impl_actions!(
- pane,
- [
- CloseAllItems,
- CloseActiveItem,
- ActivateItem,
- RevealInProjectPanel
- ]
-);
-
-actions!(
- pane,
- [
- ActivatePrevItem,
- ActivateNextItem,
- ActivateLastItem,
- CloseInactiveItems,
- CloseCleanItems,
- CloseItemsToTheLeft,
- CloseItemsToTheRight,
- GoBack,
- GoForward,
- ReopenClosedItem,
- SplitLeft,
- SplitUp,
- SplitRight,
- SplitDown,
- ]
-);
-
-const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
-
-pub enum Event {
- AddItem { item: Box<dyn ItemHandle> },
- ActivateItem { local: bool },
- Remove,
- RemoveItem { item_id: EntityId },
- Split(SplitDirection),
- ChangeItemTitle,
- Focus,
- ZoomIn,
- ZoomOut,
-}
-
-impl fmt::Debug for Event {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Event::AddItem { item } => f
- .debug_struct("AddItem")
- .field("item", &item.item_id())
- .finish(),
- Event::ActivateItem { local } => f
- .debug_struct("ActivateItem")
- .field("local", local)
- .finish(),
- Event::Remove => f.write_str("Remove"),
- Event::RemoveItem { item_id } => f
- .debug_struct("RemoveItem")
- .field("item_id", item_id)
- .finish(),
- Event::Split(direction) => f
- .debug_struct("Split")
- .field("direction", direction)
- .finish(),
- Event::ChangeItemTitle => f.write_str("ChangeItemTitle"),
- Event::Focus => f.write_str("Focus"),
- Event::ZoomIn => f.write_str("ZoomIn"),
- Event::ZoomOut => f.write_str("ZoomOut"),
- }
- }
-}
-
-pub struct Pane {
- focus_handle: FocusHandle,
- items: Vec<Box<dyn ItemHandle>>,
- activation_history: Vec<EntityId>,
- zoomed: bool,
- was_focused: bool,
- active_item_index: usize,
- last_focused_view_by_item: HashMap<EntityId, FocusHandle>,
- nav_history: NavHistory,
- toolbar: View<Toolbar>,
- new_item_menu: Option<View<ContextMenu>>,
- split_item_menu: Option<View<ContextMenu>>,
- // tab_context_menu: View<ContextMenu>,
- workspace: WeakView<Workspace>,
- project: Model<Project>,
- drag_split_direction: Option<SplitDirection>,
- can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut WindowContext) -> bool>>,
- can_split: bool,
- render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement>,
- _subscriptions: Vec<Subscription>,
- tab_bar_scroll_handle: ScrollHandle,
- display_nav_history_buttons: bool,
-}
-
-pub struct ItemNavHistory {
- history: NavHistory,
- item: Arc<dyn WeakItemHandle>,
-}
-
-#[derive(Clone)]
-pub struct NavHistory(Arc<Mutex<NavHistoryState>>);
-
-struct NavHistoryState {
- mode: NavigationMode,
- backward_stack: VecDeque<NavigationEntry>,
- forward_stack: VecDeque<NavigationEntry>,
- closed_stack: VecDeque<NavigationEntry>,
- paths_by_item: HashMap<EntityId, (ProjectPath, Option<PathBuf>)>,
- pane: WeakView<Pane>,
- next_timestamp: Arc<AtomicUsize>,
-}
-
-#[derive(Copy, Clone)]
-pub enum NavigationMode {
- Normal,
- GoingBack,
- GoingForward,
- ClosingItem,
- ReopeningClosedItem,
- Disabled,
-}
-
-impl Default for NavigationMode {
- fn default() -> Self {
- Self::Normal
- }
-}
-
-pub struct NavigationEntry {
- pub item: Arc<dyn WeakItemHandle>,
- pub data: Option<Box<dyn Any + Send>>,
- pub timestamp: usize,
-}
-
-#[derive(Clone)]
-pub struct DraggedTab {
- pub pane: View<Pane>,
- pub ix: usize,
- pub item_id: EntityId,
- pub detail: usize,
- pub is_active: bool,
-}
-
-// pub struct DraggedItem {
-// pub handle: Box<dyn ItemHandle>,
-// pub pane: WeakView<Pane>,
-// }
-
-// pub enum ReorderBehavior {
-// None,
-// MoveAfterActive,
-// MoveToIndex(usize),
-// }
-
-// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
-// enum TabBarContextMenuKind {
-// New,
-// Split,
-// }
-
-// struct TabBarContextMenu {
-// kind: TabBarContextMenuKind,
-// handle: View<ContextMenu>,
-// }
-
-// impl TabBarContextMenu {
-// fn handle_if_kind(&self, kind: TabBarContextMenuKind) -> Option<View<ContextMenu>> {
-// if self.kind == kind {
-// return Some(self.handle.clone());
-// }
-// None
-// }
-// }
-
-// #[allow(clippy::too_many_arguments)]
-// fn nav_button<A: Action, F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>)>(
-// svg_path: &'static str,
-// style: theme2::Interactive<theme2::IconButton>,
-// nav_button_height: f32,
-// tooltip_style: TooltipStyle,
-// enabled: bool,
-// on_click: F,
-// tooltip_action: A,
-// action_name: &str,
-// cx: &mut ViewContext<Pane>,
-// ) -> AnyElement<Pane> {
-// MouseEventHandler::new::<A, _>(0, cx, |state, _| {
-// let style = if enabled {
-// style.style_for(state)
-// } else {
-// style.disabled_style()
-// };
-// Svg::new(svg_path)
-// .with_color(style.color)
-// .constrained()
-// .with_width(style.icon_width)
-// .aligned()
-// .contained()
-// .with_style(style.container)
-// .constrained()
-// .with_width(style.button_width)
-// .with_height(nav_button_height)
-// .aligned()
-// .top()
-// })
-// .with_cursor_style(if enabled {
-// CursorStyle::PointingHand
-// } else {
-// CursorStyle::default()
-// })
-// .on_click(MouseButton::Left, move |_, toolbar, cx| {
-// on_click(toolbar, cx)
-// })
-// .with_tooltip::<A>(
-// 0,
-// action_name.to_string(),
-// Some(Box::new(tooltip_action)),
-// tooltip_style,
-// cx,
-// )
-// .contained()
-// .into_any_named("nav button")
-// }
-
-impl EventEmitter<Event> for Pane {}
-
-impl Pane {
- pub fn new(
- workspace: WeakView<Workspace>,
- project: Model<Project>,
- next_timestamp: Arc<AtomicUsize>,
- can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut WindowContext) -> bool + 'static>>,
- cx: &mut ViewContext<Self>,
- ) -> Self {
- // todo!("context menu")
- // let pane_view_id = cx.view_id();
- // let context_menu = cx.build_view(|cx| ContextMenu::new(pane_view_id, cx));
- // context_menu.update(cx, |menu, _| {
- // menu.set_position_mode(OverlayPositionMode::Local)
- // });
- //
- let focus_handle = cx.focus_handle();
-
- let subscriptions = vec![
- cx.on_focus_in(&focus_handle, move |this, cx| this.focus_in(cx)),
- cx.on_focus_out(&focus_handle, move |this, cx| this.focus_out(cx)),
- ];
-
- let handle = cx.view().downgrade();
- Self {
- focus_handle,
- items: Vec::new(),
- activation_history: Vec::new(),
- was_focused: false,
- zoomed: false,
- active_item_index: 0,
- last_focused_view_by_item: Default::default(),
- nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState {
- mode: NavigationMode::Normal,
- backward_stack: Default::default(),
- forward_stack: Default::default(),
- closed_stack: Default::default(),
- paths_by_item: Default::default(),
- pane: handle.clone(),
- next_timestamp,
- }))),
- toolbar: cx.new_view(|_| Toolbar::new()),
- new_item_menu: None,
- split_item_menu: None,
- tab_bar_scroll_handle: ScrollHandle::new(),
- drag_split_direction: None,
- // tab_bar_context_menu: TabBarContextMenu {
- // kind: TabBarContextMenuKind::New,
- // handle: context_menu,
- // },
- // tab_context_menu: cx.build_view(|_| ContextMenu::new(pane_view_id, cx)),
- workspace,
- project,
- can_drop_predicate,
- can_split: true,
- render_tab_bar_buttons: Rc::new(move |pane, cx| {
- h_stack()
- .gap_2()
- .child(
- IconButton::new("plus", Icon::Plus)
- .icon_size(IconSize::Small)
- .icon_color(Color::Muted)
- .on_click(cx.listener(|pane, _, cx| {
- let menu = ContextMenu::build(cx, |menu, _| {
- menu.action("New File", NewFile.boxed_clone())
- .action("New Terminal", NewCenterTerminal.boxed_clone())
- .action("New Search", NewSearch.boxed_clone())
- });
- cx.subscribe(&menu, |pane, _, _: &DismissEvent, cx| {
- pane.focus(cx);
- pane.new_item_menu = None;
- })
- .detach();
- pane.new_item_menu = Some(menu);
- }))
- .tooltip(|cx| Tooltip::text("New...", cx)),
- )
- .when_some(pane.new_item_menu.as_ref(), |el, new_item_menu| {
- el.child(Self::render_menu_overlay(new_item_menu))
- })
- .child(
- IconButton::new("split", Icon::Split)
- .icon_size(IconSize::Small)
- .icon_color(Color::Muted)
- .on_click(cx.listener(|pane, _, cx| {
- let menu = ContextMenu::build(cx, |menu, _| {
- menu.action("Split Right", SplitRight.boxed_clone())
- .action("Split Left", SplitLeft.boxed_clone())
- .action("Split Up", SplitUp.boxed_clone())
- .action("Split Down", SplitDown.boxed_clone())
- });
- cx.subscribe(&menu, |pane, _, _: &DismissEvent, cx| {
- pane.focus(cx);
- pane.split_item_menu = None;
- })
- .detach();
- pane.split_item_menu = Some(menu);
- }))
- .tooltip(|cx| Tooltip::text("Split Pane", cx)),
- )
- .child({
- let zoomed = pane.is_zoomed();
- IconButton::new("toggle_zoom", Icon::Maximize)
- .icon_size(IconSize::Small)
- .icon_color(Color::Muted)
- .selected(zoomed)
- .selected_icon(Icon::Minimize)
- .on_click(cx.listener(|pane, _, cx| {
- pane.toggle_zoom(&crate::ToggleZoom, cx);
- }))
- .tooltip(move |cx| {
- Tooltip::text(if zoomed { "Zoom Out" } else { "Zoom In" }, cx)
- })
- })
- .when_some(pane.split_item_menu.as_ref(), |el, split_item_menu| {
- el.child(Self::render_menu_overlay(split_item_menu))
- })
- .into_any_element()
- }),
- display_nav_history_buttons: true,
- _subscriptions: subscriptions,
- }
- }
-
- pub fn has_focus(&self, cx: &WindowContext) -> bool {
- // todo!(); // inline this manually
- self.focus_handle.contains_focused(cx)
- }
-
- fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
- if !self.was_focused {
- self.was_focused = true;
- cx.emit(Event::Focus);
- cx.notify();
- }
-
- self.toolbar.update(cx, |toolbar, cx| {
- toolbar.focus_changed(true, cx);
- });
-
- if let Some(active_item) = self.active_item() {
- if self.focus_handle.is_focused(cx) {
- // Pane was focused directly. We need to either focus a view inside the active item,
- // or focus the active item itself
- if let Some(weak_last_focused_view) =
- self.last_focused_view_by_item.get(&active_item.item_id())
- {
- weak_last_focused_view.focus(cx);
- return;
- }
-
- active_item.focus_handle(cx).focus(cx);
- } else if let Some(focused) = cx.focused() {
- if !self.context_menu_focused(cx) {
- self.last_focused_view_by_item
- .insert(active_item.item_id(), focused);
- }
- }
- }
- }
-
- fn context_menu_focused(&self, cx: &mut ViewContext<Self>) -> bool {
- self.new_item_menu
- .as_ref()
- .or(self.split_item_menu.as_ref())
- .map_or(false, |menu| menu.focus_handle(cx).is_focused(cx))
- }
-
- fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
- self.was_focused = false;
- self.toolbar.update(cx, |toolbar, cx| {
- toolbar.focus_changed(false, cx);
- });
- cx.notify();
- }
-
- pub fn active_item_index(&self) -> usize {
- self.active_item_index
- }
-
- // pub fn on_can_drop<F>(&mut self, can_drop: F)
- // where
- // F: 'static + Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool,
- // {
- // self.can_drop = Rc::new(can_drop);
- // }
-
- pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
- self.can_split = can_split;
- cx.notify();
- }
-
- pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
- self.toolbar.update(cx, |toolbar, cx| {
- toolbar.set_can_navigate(can_navigate, cx);
- });
- cx.notify();
- }
-
- pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
- where
- F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement,
- {
- self.render_tab_bar_buttons = Rc::new(render);
- cx.notify();
- }
-
- pub fn nav_history_for_item<T: Item>(&self, item: &View<T>) -> ItemNavHistory {
- ItemNavHistory {
- history: self.nav_history.clone(),
- item: Arc::new(item.downgrade()),
- }
- }
-
- pub fn nav_history(&self) -> &NavHistory {
- &self.nav_history
- }
-
- pub fn nav_history_mut(&mut self) -> &mut NavHistory {
- &mut self.nav_history
- }
-
- pub fn disable_history(&mut self) {
- self.nav_history.disable();
- }
-
- pub fn enable_history(&mut self) {
- self.nav_history.enable();
- }
-
- pub fn can_navigate_backward(&self) -> bool {
- !self.nav_history.0.lock().backward_stack.is_empty()
- }
-
- pub fn can_navigate_forward(&self) -> bool {
- !self.nav_history.0.lock().forward_stack.is_empty()
- }
-
- fn navigate_backward(&mut self, cx: &mut ViewContext<Self>) {
- if let Some(workspace) = self.workspace.upgrade() {
- let pane = cx.view().downgrade();
- cx.window_context().defer(move |cx| {
- workspace.update(cx, |workspace, cx| {
- workspace.go_back(pane, cx).detach_and_log_err(cx)
- })
- })
- }
- }
-
- fn navigate_forward(&mut self, cx: &mut ViewContext<Self>) {
- if let Some(workspace) = self.workspace.upgrade() {
- let pane = cx.view().downgrade();
- cx.window_context().defer(move |cx| {
- workspace.update(cx, |workspace, cx| {
- workspace.go_forward(pane, cx).detach_and_log_err(cx)
- })
- })
- }
- }
-
- fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
- self.toolbar.update(cx, |_, cx| cx.notify());
- }
-
- pub(crate) fn open_item(
- &mut self,
- project_entry_id: Option<ProjectEntryId>,
- focus_item: bool,
- cx: &mut ViewContext<Self>,
- build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
- ) -> Box<dyn ItemHandle> {
- let mut existing_item = None;
- if let Some(project_entry_id) = project_entry_id {
- for (index, item) in self.items.iter().enumerate() {
- if item.is_singleton(cx)
- && item.project_entry_ids(cx).as_slice() == [project_entry_id]
- {
- let item = item.boxed_clone();
- existing_item = Some((index, item));
- break;
- }
- }
- }
-
- if let Some((index, existing_item)) = existing_item {
- self.activate_item(index, focus_item, focus_item, cx);
- existing_item
- } else {
- let new_item = build_item(cx);
- self.add_item(new_item.clone(), true, focus_item, None, cx);
- new_item
- }
- }
-
- pub fn add_item(
- &mut self,
- item: Box<dyn ItemHandle>,
- activate_pane: bool,
- focus_item: bool,
- destination_index: Option<usize>,
- cx: &mut ViewContext<Self>,
- ) {
- if item.is_singleton(cx) {
- if let Some(&entry_id) = item.project_entry_ids(cx).get(0) {
- let project = self.project.read(cx);
- if let Some(project_path) = project.path_for_entry(entry_id, cx) {
- let abs_path = project.absolute_path(&project_path, cx);
- self.nav_history
- .0
- .lock()
- .paths_by_item
- .insert(item.item_id(), (project_path, abs_path));
- }
- }
- }
- // If no destination index is specified, add or move the item after the active item.
- let mut insertion_index = {
- cmp::min(
- if let Some(destination_index) = destination_index {
- destination_index
- } else {
- self.active_item_index + 1
- },
- self.items.len(),
- )
- };
-
- // Does the item already exist?
- let project_entry_id = if item.is_singleton(cx) {
- item.project_entry_ids(cx).get(0).copied()
- } else {
- None
- };
-
- let existing_item_index = self.items.iter().position(|existing_item| {
- if existing_item.item_id() == item.item_id() {
- true
- } else if existing_item.is_singleton(cx) {
- existing_item
- .project_entry_ids(cx)
- .get(0)
- .map_or(false, |existing_entry_id| {
- Some(existing_entry_id) == project_entry_id.as_ref()
- })
- } else {
- false
- }
- });
-
- if let Some(existing_item_index) = existing_item_index {
- // If the item already exists, move it to the desired destination and activate it
-
- if existing_item_index != insertion_index {
- let existing_item_is_active = existing_item_index == self.active_item_index;
-
- // If the caller didn't specify a destination and the added item is already
- // the active one, don't move it
- if existing_item_is_active && destination_index.is_none() {
- insertion_index = existing_item_index;
- } else {
- self.items.remove(existing_item_index);
- if existing_item_index < self.active_item_index {
- self.active_item_index -= 1;
- }
- insertion_index = insertion_index.min(self.items.len());
-
- self.items.insert(insertion_index, item.clone());
-
- if existing_item_is_active {
- self.active_item_index = insertion_index;
- } else if insertion_index <= self.active_item_index {
- self.active_item_index += 1;
- }
- }
-
- cx.notify();
- }
-
- self.activate_item(insertion_index, activate_pane, focus_item, cx);
- } else {
- self.items.insert(insertion_index, item.clone());
- if insertion_index <= self.active_item_index {
- self.active_item_index += 1;
- }
-
- self.activate_item(insertion_index, activate_pane, focus_item, cx);
- cx.notify();
- }
-
- cx.emit(Event::AddItem { item });
- }
-
- pub fn items_len(&self) -> usize {
- self.items.len()
- }
-
- pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> + DoubleEndedIterator {
- self.items.iter()
- }
-
- pub fn items_of_type<T: Render>(&self) -> impl '_ + Iterator<Item = View<T>> {
- self.items
- .iter()
- .filter_map(|item| item.to_any().downcast().ok())
- }
-
- pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
- self.items.get(self.active_item_index).cloned()
- }
-
- pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
- self.items
- .get(self.active_item_index)?
- .pixel_position_of_cursor(cx)
- }
-
- pub fn item_for_entry(
- &self,
- entry_id: ProjectEntryId,
- cx: &AppContext,
- ) -> Option<Box<dyn ItemHandle>> {
- self.items.iter().find_map(|item| {
- if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
- Some(item.boxed_clone())
- } else {
- None
- }
- })
- }
-
- pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
- self.items
- .iter()
- .position(|i| i.item_id() == item.item_id())
- }
-
- pub fn item_for_index(&self, ix: usize) -> Option<&dyn ItemHandle> {
- self.items.get(ix).map(|i| i.as_ref())
- }
-
- pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
- if self.zoomed {
- cx.emit(Event::ZoomOut);
- } else if !self.items.is_empty() {
- if !self.focus_handle.contains_focused(cx) {
- cx.focus_self();
- }
- cx.emit(Event::ZoomIn);
- }
- }
-
- pub fn activate_item(
- &mut self,
- index: usize,
- activate_pane: bool,
- focus_item: bool,
- cx: &mut ViewContext<Self>,
- ) {
- use NavigationMode::{GoingBack, GoingForward};
-
- if index < self.items.len() {
- let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
- if prev_active_item_ix != self.active_item_index
- || matches!(self.nav_history.mode(), GoingBack | GoingForward)
- {
- if let Some(prev_item) = self.items.get(prev_active_item_ix) {
- prev_item.deactivated(cx);
- }
-
- cx.emit(Event::ActivateItem {
- local: activate_pane,
- });
- }
-
- if let Some(newly_active_item) = self.items.get(index) {
- self.activation_history
- .retain(|&previously_active_item_id| {
- previously_active_item_id != newly_active_item.item_id()
- });
- self.activation_history.push(newly_active_item.item_id());
- }
-
- self.update_toolbar(cx);
- self.update_status_bar(cx);
-
- if focus_item {
- self.focus_active_item(cx);
- }
-
- self.tab_bar_scroll_handle.scroll_to_item(index);
- cx.notify();
- }
- }
-
- pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
- let mut index = self.active_item_index;
- if index > 0 {
- index -= 1;
- } else if !self.items.is_empty() {
- index = self.items.len() - 1;
- }
- self.activate_item(index, activate_pane, activate_pane, cx);
- }
-
- pub fn activate_next_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
- let mut index = self.active_item_index;
- if index + 1 < self.items.len() {
- index += 1;
- } else {
- index = 0;
- }
- self.activate_item(index, activate_pane, activate_pane, cx);
- }
-
- pub fn close_active_item(
- &mut self,
- action: &CloseActiveItem,
- cx: &mut ViewContext<Self>,
- ) -> Option<Task<Result<()>>> {
- if self.items.is_empty() {
- return None;
- }
- let active_item_id = self.items[self.active_item_index].item_id();
- Some(self.close_item_by_id(
- active_item_id,
- action.save_intent.unwrap_or(SaveIntent::Close),
- cx,
- ))
- }
-
- pub fn close_item_by_id(
- &mut self,
- item_id_to_close: EntityId,
- save_intent: SaveIntent,
- cx: &mut ViewContext<Self>,
- ) -> Task<Result<()>> {
- self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close)
- }
-
- pub fn close_inactive_items(
- &mut self,
- _: &CloseInactiveItems,
- cx: &mut ViewContext<Self>,
- ) -> Option<Task<Result<()>>> {
- if self.items.is_empty() {
- return None;
- }
-
- let active_item_id = self.items[self.active_item_index].item_id();
- Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
- item_id != active_item_id
- }))
- }
-
- pub fn close_clean_items(
- &mut self,
- _: &CloseCleanItems,
- cx: &mut ViewContext<Self>,
- ) -> Option<Task<Result<()>>> {
- let item_ids: Vec<_> = self
- .items()
- .filter(|item| !item.is_dirty(cx))
- .map(|item| item.item_id())
- .collect();
- Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
- item_ids.contains(&item_id)
- }))
- }
-
- pub fn close_items_to_the_left(
- &mut self,
- _: &CloseItemsToTheLeft,
- cx: &mut ViewContext<Self>,
- ) -> Option<Task<Result<()>>> {
- if self.items.is_empty() {
- return None;
- }
- let active_item_id = self.items[self.active_item_index].item_id();
- Some(self.close_items_to_the_left_by_id(active_item_id, cx))
- }
-
- pub fn close_items_to_the_left_by_id(
- &mut self,
- item_id: EntityId,
- cx: &mut ViewContext<Self>,
- ) -> Task<Result<()>> {
- let item_ids: Vec<_> = self
- .items()
- .take_while(|item| item.item_id() != item_id)
- .map(|item| item.item_id())
- .collect();
- self.close_items(cx, SaveIntent::Close, move |item_id| {
- item_ids.contains(&item_id)
- })
- }
-
- pub fn close_items_to_the_right(
- &mut self,
- _: &CloseItemsToTheRight,
- cx: &mut ViewContext<Self>,
- ) -> Option<Task<Result<()>>> {
- if self.items.is_empty() {
- return None;
- }
- let active_item_id = self.items[self.active_item_index].item_id();
- Some(self.close_items_to_the_right_by_id(active_item_id, cx))
- }
-
- pub fn close_items_to_the_right_by_id(
- &mut self,
- item_id: EntityId,
- cx: &mut ViewContext<Self>,
- ) -> Task<Result<()>> {
- let item_ids: Vec<_> = self
- .items()
- .rev()
- .take_while(|item| item.item_id() != item_id)
- .map(|item| item.item_id())
- .collect();
- self.close_items(cx, SaveIntent::Close, move |item_id| {
- item_ids.contains(&item_id)
- })
- }
-
- pub fn close_all_items(
- &mut self,
- action: &CloseAllItems,
- cx: &mut ViewContext<Self>,
- ) -> Option<Task<Result<()>>> {
- if self.items.is_empty() {
- return None;
- }
-
- Some(
- self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| {
- true
- }),
- )
- }
-
- pub(super) fn file_names_for_prompt(
- items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
- all_dirty_items: usize,
- cx: &AppContext,
- ) -> String {
- /// Quantity of item paths displayed in prompt prior to cutoff..
- const FILE_NAMES_CUTOFF_POINT: usize = 10;
- let mut file_names: Vec<_> = items
- .filter_map(|item| {
- item.project_path(cx).and_then(|project_path| {
- project_path
- .path
- .file_name()
- .and_then(|name| name.to_str().map(ToOwned::to_owned))
- })
- })
- .take(FILE_NAMES_CUTOFF_POINT)
- .collect();
- let should_display_followup_text =
- all_dirty_items > FILE_NAMES_CUTOFF_POINT || file_names.len() != all_dirty_items;
- if should_display_followup_text {
- let not_shown_files = all_dirty_items - file_names.len();
- if not_shown_files == 1 {
- file_names.push(".. 1 file not shown".into());
- } else {
- file_names.push(format!(".. {} files not shown", not_shown_files).into());
- }
- }
- let file_names = file_names.join("\n");
- format!(
- "Do you want to save changes to the following {} files?\n{file_names}",
- all_dirty_items
- )
- }
-
- pub fn close_items(
- &mut self,
- cx: &mut ViewContext<Pane>,
- mut save_intent: SaveIntent,
- should_close: impl Fn(EntityId) -> bool,
- ) -> Task<Result<()>> {
- // Find the items to close.
- let mut items_to_close = Vec::new();
- let mut dirty_items = Vec::new();
- for item in &self.items {
- if should_close(item.item_id()) {
- items_to_close.push(item.boxed_clone());
- if item.is_dirty(cx) {
- dirty_items.push(item.boxed_clone());
- }
- }
- }
-
- // If a buffer is open both in a singleton editor and in a multibuffer, make sure
- // to focus the singleton buffer when prompting to save that buffer, as opposed
- // to focusing the multibuffer, because this gives the user a more clear idea
- // of what content they would be saving.
- items_to_close.sort_by_key(|item| !item.is_singleton(cx));
-
- let workspace = self.workspace.clone();
- cx.spawn(|pane, mut cx| async move {
- if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
- let answer = pane.update(&mut cx, |_, cx| {
- let prompt =
- Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx);
- cx.prompt(
- PromptLevel::Warning,
- &prompt,
- &["Save all", "Discard all", "Cancel"],
- )
- })?;
- match answer.await {
- Ok(0) => save_intent = SaveIntent::SaveAll,
- Ok(1) => save_intent = SaveIntent::Skip,
- _ => {}
- }
- }
- let mut saved_project_items_ids = HashSet::default();
- for item in items_to_close.clone() {
- // Find the item's current index and its set of project item models. Avoid
- // storing these in advance, in case they have changed since this task
- // was started.
- let (item_ix, mut project_item_ids) = pane.update(&mut cx, |pane, cx| {
- (pane.index_for_item(&*item), item.project_item_model_ids(cx))
- })?;
- let item_ix = if let Some(ix) = item_ix {
- ix
- } else {
- continue;
- };
-
- // Check if this view has any project items that are not open anywhere else
- // in the workspace, AND that the user has not already been prompted to save.
- // If there are any such project entries, prompt the user to save this item.
- let project = workspace.update(&mut cx, |workspace, cx| {
- for item in workspace.items(cx) {
- if !items_to_close
- .iter()
- .any(|item_to_close| item_to_close.item_id() == item.item_id())
- {
- let other_project_item_ids = item.project_item_model_ids(cx);
- project_item_ids.retain(|id| !other_project_item_ids.contains(id));
- }
- }
- workspace.project().clone()
- })?;
- let should_save = project_item_ids
- .iter()
- .any(|id| saved_project_items_ids.insert(*id));
-
- if should_save
- && !Self::save_item(
- project.clone(),
- &pane,
- item_ix,
- &*item,
- save_intent,
- &mut cx,
- )
- .await?
- {
- break;
- }
-
- // Remove the item from the pane.
- pane.update(&mut cx, |pane, cx| {
- if let Some(item_ix) = pane
- .items
- .iter()
- .position(|i| i.item_id() == item.item_id())
- {
- pane.remove_item(item_ix, false, cx);
- }
- })
- .ok();
- }
-
- pane.update(&mut cx, |_, cx| cx.notify()).ok();
- Ok(())
- })
- }
-
- pub fn remove_item(
- &mut self,
- item_index: usize,
- activate_pane: bool,
- cx: &mut ViewContext<Self>,
- ) {
- self.activation_history
- .retain(|&history_entry| history_entry != self.items[item_index].item_id());
-
- if item_index == self.active_item_index {
- let index_to_activate = self
- .activation_history
- .pop()
- .and_then(|last_activated_item| {
- self.items.iter().enumerate().find_map(|(index, item)| {
- (item.item_id() == last_activated_item).then_some(index)
- })
- })
- // We didn't have a valid activation history entry, so fallback
- // to activating the item to the left
- .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
-
- let should_activate = activate_pane || self.has_focus(cx);
- if self.items.len() == 1 && should_activate {
- self.focus_handle.focus(cx);
- } else {
- self.activate_item(index_to_activate, should_activate, should_activate, cx);
- }
- }
-
- let item = self.items.remove(item_index);
-
- cx.emit(Event::RemoveItem {
- item_id: item.item_id(),
- });
- if self.items.is_empty() {
- item.deactivated(cx);
- self.update_toolbar(cx);
- cx.emit(Event::Remove);
- }
-
- if item_index < self.active_item_index {
- self.active_item_index -= 1;
- }
-
- self.nav_history.set_mode(NavigationMode::ClosingItem);
- item.deactivated(cx);
- self.nav_history.set_mode(NavigationMode::Normal);
-
- if let Some(path) = item.project_path(cx) {
- let abs_path = self
- .nav_history
- .0
- .lock()
- .paths_by_item
- .get(&item.item_id())
- .and_then(|(_, abs_path)| abs_path.clone());
-
- self.nav_history
- .0
- .lock()
- .paths_by_item
- .insert(item.item_id(), (path, abs_path));
- } else {
- self.nav_history
- .0
- .lock()
- .paths_by_item
- .remove(&item.item_id());
- }
-
- if self.items.is_empty() && self.zoomed {
- cx.emit(Event::ZoomOut);
- }
-
- cx.notify();
- }
-
- pub async fn save_item(
- project: Model<Project>,
- pane: &WeakView<Pane>,
- item_ix: usize,
- item: &dyn ItemHandle,
- save_intent: SaveIntent,
- cx: &mut AsyncWindowContext,
- ) -> Result<bool> {
- const CONFLICT_MESSAGE: &str =
- "This file has changed on disk since you started editing it. Do you want to overwrite it?";
-
- if save_intent == SaveIntent::Skip {
- return Ok(true);
- }
-
- let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.update(|_, cx| {
- (
- item.has_conflict(cx),
- item.is_dirty(cx),
- item.can_save(cx),
- item.is_singleton(cx),
- )
- })?;
-
- // when saving a single buffer, we ignore whether or not it's dirty.
- if save_intent == SaveIntent::Save {
- is_dirty = true;
- }
-
- if save_intent == SaveIntent::SaveAs {
- is_dirty = true;
- has_conflict = false;
- can_save = false;
- }
-
- if save_intent == SaveIntent::Overwrite {
- has_conflict = false;
- }
-
- if has_conflict && can_save {
- let answer = pane.update(cx, |pane, cx| {
- pane.activate_item(item_ix, true, true, cx);
- cx.prompt(
- PromptLevel::Warning,
- CONFLICT_MESSAGE,
- &["Overwrite", "Discard", "Cancel"],
- )
- })?;
- match answer.await {
- Ok(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?,
- Ok(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
- _ => return Ok(false),
- }
- } else if is_dirty && (can_save || can_save_as) {
- if save_intent == SaveIntent::Close {
- let will_autosave = cx.update(|_, cx| {
- matches!(
- WorkspaceSettings::get_global(cx).autosave,
- AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
- ) && Self::can_autosave_item(&*item, cx)
- })?;
- if !will_autosave {
- let answer = pane.update(cx, |pane, cx| {
- pane.activate_item(item_ix, true, true, cx);
- let prompt = dirty_message_for(item.project_path(cx));
- cx.prompt(
- PromptLevel::Warning,
- &prompt,
- &["Save", "Don't Save", "Cancel"],
- )
- })?;
- match answer.await {
- Ok(0) => {}
- Ok(1) => return Ok(true), // Don't save this file
- _ => return Ok(false), // Cancel
- }
- }
- }
-
- if can_save {
- pane.update(cx, |_, cx| item.save(project, cx))?.await?;
- } else if can_save_as {
- let start_abs_path = project
- .update(cx, |project, cx| {
- let worktree = project.visible_worktrees(cx).next()?;
- Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
- })?
- .unwrap_or_else(|| Path::new("").into());
-
- let abs_path = cx.update(|_, cx| cx.prompt_for_new_path(&start_abs_path))?;
- if let Some(abs_path) = abs_path.await.ok().flatten() {
- pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))?
- .await?;
- } else {
- return Ok(false);
- }
- }
- }
- Ok(true)
- }
-
- fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
- let is_deleted = item.project_entry_ids(cx).is_empty();
- item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
- }
-
- pub fn autosave_item(
- item: &dyn ItemHandle,
- project: Model<Project>,
- cx: &mut WindowContext,
- ) -> Task<Result<()>> {
- if Self::can_autosave_item(item, cx) {
- item.save(project, cx)
- } else {
- Task::ready(Ok(()))
- }
- }
-
- pub fn focus(&mut self, cx: &mut ViewContext<Pane>) {
- cx.focus(&self.focus_handle);
- }
-
- pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
- if let Some(active_item) = self.active_item() {
- let focus_handle = active_item.focus_handle(cx);
- cx.focus(&focus_handle);
- }
- }
-
- pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
- cx.emit(Event::Split(direction));
- }
-
- // fn deploy_split_menu(&mut self, cx: &mut ViewContext<Self>) {
- // self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
- // menu.toggle(
- // Default::default(),
- // AnchorCorner::TopRight,
- // vec![
- // ContextMenuItem::action("Split Right", SplitRight),
- // ContextMenuItem::action("Split Left", SplitLeft),
- // ContextMenuItem::action("Split Up", SplitUp),
- // ContextMenuItem::action("Split Down", SplitDown),
- // ],
- // cx,
- // );
- // });
-
- // self.tab_bar_context_menu.kind = TabBarContextMenuKind::Split;
- // }
-
- // fn deploy_new_menu(&mut self, cx: &mut ViewContext<Self>) {
- // self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
- // menu.toggle(
- // Default::default(),
- // AnchorCorner::TopRight,
- // vec![
- // ContextMenuItem::action("New File", NewFile),
- // ContextMenuItem::action("New Terminal", NewCenterTerminal),
- // ContextMenuItem::action("New Search", NewSearch),
- // ],
- // cx,
- // );
- // });
-
- // self.tab_bar_context_menu.kind = TabBarContextMenuKind::New;
- // }
-
- // fn deploy_tab_context_menu(
- // &mut self,
- // position: Vector2F,
- // target_item_id: usize,
- // cx: &mut ViewContext<Self>,
- // ) {
- // let active_item_id = self.items[self.active_item_index].id();
- // let is_active_item = target_item_id == active_item_id;
- // let target_pane = cx.weak_handle();
-
- // // The `CloseInactiveItems` action should really be called "CloseOthers" and the behaviour should be dynamically based on the tab the action is ran on. Currently, this is a weird action because you can run it on a non-active tab and it will close everything by the actual active tab
-
- // self.tab_context_menu.update(cx, |menu, cx| {
- // menu.show(
- // position,
- // AnchorCorner::TopLeft,
- // if is_active_item {
- // vec![
- // ContextMenuItem::action(
- // "Close Active Item",
- // CloseActiveItem { save_intent: None },
- // ),
- // ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
- // ContextMenuItem::action("Close Clean Items", CloseCleanItems),
- // ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft),
- // ContextMenuItem::action("Close Items To The Right", CloseItemsToTheRight),
- // ContextMenuItem::action(
- // "Close All Items",
- // CloseAllItems { save_intent: None },
- // ),
- // ]
- // } else {
- // // In the case of the user right clicking on a non-active tab, for some item-closing commands, we need to provide the id of the tab, for the others, we can reuse the existing command.
- // vec![
- // ContextMenuItem::handler("Close Inactive Item", {
- // let pane = target_pane.clone();
- // move |cx| {
- // if let Some(pane) = pane.upgrade(cx) {
- // pane.update(cx, |pane, cx| {
- // pane.close_item_by_id(
- // target_item_id,
- // SaveIntent::Close,
- // cx,
- // )
- // .detach_and_log_err(cx);
- // })
- // }
- // }
- // }),
- // ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
- // ContextMenuItem::action("Close Clean Items", CloseCleanItems),
- // ContextMenuItem::handler("Close Items To The Left", {
- // let pane = target_pane.clone();
- // move |cx| {
- // if let Some(pane) = pane.upgrade(cx) {
- // pane.update(cx, |pane, cx| {
- // pane.close_items_to_the_left_by_id(target_item_id, cx)
- // .detach_and_log_err(cx);
- // })
- // }
- // }
- // }),
- // ContextMenuItem::handler("Close Items To The Right", {
- // let pane = target_pane.clone();
- // move |cx| {
- // if let Some(pane) = pane.upgrade(cx) {
- // pane.update(cx, |pane, cx| {
- // pane.close_items_to_the_right_by_id(target_item_id, cx)
- // .detach_and_log_err(cx);
- // })
- // }
- // }
- // }),
- // ContextMenuItem::action(
- // "Close All Items",
- // CloseAllItems { save_intent: None },
- // ),
- // ]
- // },
- // cx,
- // );
- // });
- // }
-
- pub fn toolbar(&self) -> &View<Toolbar> {
- &self.toolbar
- }
-
- pub fn handle_deleted_project_item(
- &mut self,
- entry_id: ProjectEntryId,
- cx: &mut ViewContext<Pane>,
- ) -> Option<()> {
- let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
- if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
- Some((i, item.item_id()))
- } else {
- None
- }
- })?;
-
- self.remove_item(item_index_to_delete, false, cx);
- self.nav_history.remove_item(item_id);
-
- Some(())
- }
-
- fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
- let active_item = self
- .items
- .get(self.active_item_index)
- .map(|item| item.as_ref());
- self.toolbar.update(cx, |toolbar, cx| {
- toolbar.set_active_item(active_item, cx);
- });
- }
-
- fn update_status_bar(&mut self, cx: &mut ViewContext<Self>) {
- let workspace = self.workspace.clone();
- let pane = cx.view().clone();
-
- cx.window_context().defer(move |cx| {
- let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone())
- else {
- return;
- };
-
- status_bar.update(cx, move |status_bar, cx| {
- status_bar.set_active_pane(&pane, cx);
- });
- });
- }
-
- fn render_tab(
- &self,
- ix: usize,
- item: &Box<dyn ItemHandle>,
- detail: usize,
- cx: &mut ViewContext<'_, Pane>,
- ) -> impl IntoElement {
- let is_active = ix == self.active_item_index;
-
- let label = item.tab_content(Some(detail), is_active, cx);
- let close_side = &ItemSettings::get_global(cx).close_position;
-
- let indicator = maybe!({
- let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
- (true, _) => Color::Warning,
- (_, true) => Color::Accent,
- (false, false) => return None,
- };
-
- Some(Indicator::dot().color(indicator_color))
- });
-
- let item_id = item.item_id();
- let is_first_item = ix == 0;
- let is_last_item = ix == self.items.len() - 1;
- let position_relative_to_active_item = ix.cmp(&self.active_item_index);
-
- let tab = Tab::new(ix)
- .position(if is_first_item {
- TabPosition::First
- } else if is_last_item {
- TabPosition::Last
- } else {
- TabPosition::Middle(position_relative_to_active_item)
- })
- .close_side(match close_side {
- ClosePosition::Left => ui::TabCloseSide::Start,
- ClosePosition::Right => ui::TabCloseSide::End,
- })
- .selected(is_active)
- .on_click(
- cx.listener(move |pane: &mut Self, _, cx| pane.activate_item(ix, true, true, cx)),
- )
- // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener.
- .on_mouse_down(
- MouseButton::Middle,
- cx.listener(move |pane, _event, cx| {
- pane.close_item_by_id(item_id, SaveIntent::Close, cx)
- .detach_and_log_err(cx);
- }),
- )
- .on_drag(
- DraggedTab {
- pane: cx.view().clone(),
- detail,
- item_id,
- is_active,
- ix,
- },
- |tab, cx| cx.new_view(|_| tab.clone()),
- )
- .drag_over::<DraggedTab>(|tab| tab.bg(cx.theme().colors().drop_target_background))
- .drag_over::<ProjectEntryId>(|tab| tab.bg(cx.theme().colors().drop_target_background))
- .when_some(self.can_drop_predicate.clone(), |this, p| {
- this.can_drop(move |a, cx| p(a, cx))
- })
- .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
- this.drag_split_direction = None;
- this.handle_tab_drop(dragged_tab, ix, cx)
- }))
- .on_drop(cx.listener(move |this, entry_id: &ProjectEntryId, cx| {
- this.drag_split_direction = None;
- this.handle_project_entry_drop(entry_id, cx)
- }))
- .when_some(item.tab_tooltip_text(cx), |tab, text| {
- tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
- })
- .start_slot::<Indicator>(indicator)
- .end_slot(
- IconButton::new("close tab", Icon::Close)
- .icon_color(Color::Muted)
- .size(ButtonSize::None)
- .icon_size(IconSize::XSmall)
- .on_click(cx.listener(move |pane, _, cx| {
- pane.close_item_by_id(item_id, SaveIntent::Close, cx)
- .detach_and_log_err(cx);
- })),
- )
- .child(label);
-
- let single_entry_to_resolve = {
- let item_entries = self.items[ix].project_entry_ids(cx);
- if item_entries.len() == 1 {
- Some(item_entries[0])
- } else {
- None
- }
- };
-
- let pane = cx.view().downgrade();
- right_click_menu(ix).trigger(tab).menu(move |cx| {
- let pane = pane.clone();
- ContextMenu::build(cx, move |mut menu, cx| {
- if let Some(pane) = pane.upgrade() {
- menu = menu
- .entry(
- "Close",
- Some(Box::new(CloseActiveItem { save_intent: None })),
- cx.handler_for(&pane, move |pane, cx| {
- pane.close_item_by_id(item_id, SaveIntent::Close, cx)
- .detach_and_log_err(cx);
- }),
- )
- .entry(
- "Close Others",
- Some(Box::new(CloseInactiveItems)),
- cx.handler_for(&pane, move |pane, cx| {
- pane.close_items(cx, SaveIntent::Close, |id| id != item_id)
- .detach_and_log_err(cx);
- }),
- )
- .separator()
- .entry(
- "Close Left",
- Some(Box::new(CloseItemsToTheLeft)),
- cx.handler_for(&pane, move |pane, cx| {
- pane.close_items_to_the_left_by_id(item_id, cx)
- .detach_and_log_err(cx);
- }),
- )
- .entry(
- "Close Right",
- Some(Box::new(CloseItemsToTheRight)),
- cx.handler_for(&pane, move |pane, cx| {
- pane.close_items_to_the_right_by_id(item_id, cx)
- .detach_and_log_err(cx);
- }),
- )
- .separator()
- .entry(
- "Close Clean",
- Some(Box::new(CloseCleanItems)),
- cx.handler_for(&pane, move |pane, cx| {
- pane.close_clean_items(&CloseCleanItems, cx)
- .map(|task| task.detach_and_log_err(cx));
- }),
- )
- .entry(
- "Close All",
- Some(Box::new(CloseAllItems { save_intent: None })),
- cx.handler_for(&pane, |pane, cx| {
- pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
- .map(|task| task.detach_and_log_err(cx));
- }),
- );
-
- if let Some(entry) = single_entry_to_resolve {
- let entry_id = entry.to_proto();
- menu = menu.separator().entry(
- "Reveal In Project Panel",
- Some(Box::new(RevealInProjectPanel { entry_id })),
- cx.handler_for(&pane, move |pane, cx| {
- pane.project.update(cx, |_, cx| {
- cx.emit(project::Event::RevealInProjectPanel(
- ProjectEntryId::from_proto(entry_id),
- ))
- });
- }),
- );
- }
- }
-
- menu
- })
- })
- }
-
- fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
- TabBar::new("tab_bar")
- .track_scroll(self.tab_bar_scroll_handle.clone())
- .when(self.display_nav_history_buttons, |tab_bar| {
- tab_bar.start_child(
- h_stack()
- .gap_2()
- .child(
- IconButton::new("navigate_backward", Icon::ArrowLeft)
- .icon_size(IconSize::Small)
- .on_click({
- let view = cx.view().clone();
- move |_, cx| view.update(cx, Self::navigate_backward)
- })
- .disabled(!self.can_navigate_backward())
- .tooltip(|cx| Tooltip::for_action("Go Back", &GoBack, cx)),
- )
- .child(
- IconButton::new("navigate_forward", Icon::ArrowRight)
- .icon_size(IconSize::Small)
- .on_click({
- let view = cx.view().clone();
- move |_, cx| view.update(cx, Self::navigate_backward)
- })
- .disabled(!self.can_navigate_forward())
- .tooltip(|cx| Tooltip::for_action("Go Forward", &GoForward, cx)),
- ),
- )
- })
- .when(self.was_focused || self.has_focus(cx), |tab_bar| {
- tab_bar.end_child({
- let render_tab_buttons = self.render_tab_bar_buttons.clone();
- render_tab_buttons(self, cx)
- })
- })
- .children(
- self.items
- .iter()
- .enumerate()
- .zip(self.tab_details(cx))
- .map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)),
- )
- .child(
- div()
- .min_w_6()
- // HACK: This empty child is currently necessary to force the drop traget to appear
- // despite us setting a min width above.
- .child("")
- .h_full()
- .flex_grow()
- .drag_over::<DraggedTab>(|bar| {
- bar.bg(cx.theme().colors().drop_target_background)
- })
- .drag_over::<ProjectEntryId>(|bar| {
- bar.bg(cx.theme().colors().drop_target_background)
- })
- .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
- this.drag_split_direction = None;
- this.handle_tab_drop(dragged_tab, this.items.len(), cx)
- }))
- .on_drop(cx.listener(move |this, entry_id: &ProjectEntryId, cx| {
- this.drag_split_direction = None;
- this.handle_project_entry_drop(entry_id, cx)
- })),
- )
- }
-
- fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
- div()
- .absolute()
- .z_index(1)
- .bottom_0()
- .right_0()
- .size_0()
- .child(overlay().anchor(AnchorCorner::TopRight).child(menu.clone()))
- }
-
- fn tab_details(&self, cx: &AppContext) -> Vec<usize> {
- let mut tab_details = self.items.iter().map(|_| 0).collect::<Vec<_>>();
-
- let mut tab_descriptions = HashMap::default();
- let mut done = false;
- while !done {
- done = true;
-
- // Store item indices by their tab description.
- for (ix, (item, detail)) in self.items.iter().zip(&tab_details).enumerate() {
- if let Some(description) = item.tab_description(*detail, cx) {
- if *detail == 0
- || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
- {
- tab_descriptions
- .entry(description)
- .or_insert(Vec::new())
- .push(ix);
- }
- }
- }
-
- // If two or more items have the same tab description, increase eir level
- // of detail and try again.
- for (_, item_ixs) in tab_descriptions.drain() {
- if item_ixs.len() > 1 {
- done = false;
- for ix in item_ixs {
- tab_details[ix] += 1;
- }
- }
- }
- }
-
- tab_details
- }
-
- pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
- self.zoomed = zoomed;
- cx.notify();
- }
-
- pub fn is_zoomed(&self) -> bool {
- self.zoomed
- }
-
- fn handle_drag_move<T>(&mut self, event: &DragMoveEvent<T>, cx: &mut ViewContext<Self>) {
- if !self.can_split {
- return;
- }
-
- let edge_width = cx.rem_size() * 8;
- let cursor = event.event.position;
- let direction = if cursor.x < event.bounds.left() + edge_width {
- Some(SplitDirection::Left)
- } else if cursor.x > event.bounds.right() - edge_width {
- Some(SplitDirection::Right)
- } else if cursor.y < event.bounds.top() + edge_width {
- Some(SplitDirection::Up)
- } else if cursor.y > event.bounds.bottom() - edge_width {
- Some(SplitDirection::Down)
- } else {
- None
- };
-
- if direction != self.drag_split_direction {
- self.drag_split_direction = direction;
- }
- }
-
- fn handle_tab_drop(
- &mut self,
- dragged_tab: &DraggedTab,
- ix: usize,
- cx: &mut ViewContext<'_, Pane>,
- ) {
- let mut to_pane = cx.view().clone();
- let split_direction = self.drag_split_direction;
- let item_id = dragged_tab.item_id;
- let from_pane = dragged_tab.pane.clone();
- self.workspace
- .update(cx, |_, cx| {
- cx.defer(move |workspace, cx| {
- if let Some(split_direction) = split_direction {
- to_pane = workspace.split_pane(to_pane, split_direction, cx);
- }
- workspace.move_item(from_pane, to_pane, item_id, ix, cx);
- });
- })
- .log_err();
- }
-
- fn handle_project_entry_drop(
- &mut self,
- project_entry_id: &ProjectEntryId,
- cx: &mut ViewContext<'_, Pane>,
- ) {
- let mut to_pane = cx.view().clone();
- let split_direction = self.drag_split_direction;
- let project_entry_id = *project_entry_id;
- self.workspace
- .update(cx, |_, cx| {
- cx.defer(move |workspace, cx| {
- if let Some(path) = workspace
- .project()
- .read(cx)
- .path_for_entry(project_entry_id, cx)
- {
- if let Some(split_direction) = split_direction {
- to_pane = workspace.split_pane(to_pane, split_direction, cx);
- }
- workspace
- .open_path(path, Some(to_pane.downgrade()), true, cx)
- .detach_and_log_err(cx);
- }
- });
- })
- .log_err();
- }
-
- pub fn display_nav_history_buttons(&mut self, display: bool) {
- self.display_nav_history_buttons = display;
- }
-}
-
-impl FocusableView for Pane {
- fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
- self.focus_handle.clone()
- }
-}
-
-impl Render for Pane {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- v_stack()
- .key_context("Pane")
- .track_focus(&self.focus_handle)
- .size_full()
- .flex_none()
- .overflow_hidden()
- .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
- .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
- .on_action(
- cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
- )
- .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
- .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
- .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
- .on_action(cx.listener(Pane::toggle_zoom))
- .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
- pane.activate_item(action.0, true, true, cx);
- }))
- .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
- pane.activate_item(pane.items.len() - 1, true, true, cx);
- }))
- .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
- pane.activate_prev_item(true, cx);
- }))
- .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
- pane.activate_next_item(true, cx);
- }))
- .on_action(
- cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
- pane.close_active_item(action, cx)
- .map(|task| task.detach_and_log_err(cx));
- }),
- )
- .on_action(
- cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
- pane.close_inactive_items(action, cx)
- .map(|task| task.detach_and_log_err(cx));
- }),
- )
- .on_action(
- cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
- pane.close_clean_items(action, cx)
- .map(|task| task.detach_and_log_err(cx));
- }),
- )
- .on_action(
- cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
- pane.close_items_to_the_left(action, cx)
- .map(|task| task.detach_and_log_err(cx));
- }),
- )
- .on_action(
- cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
- pane.close_items_to_the_right(action, cx)
- .map(|task| task.detach_and_log_err(cx));
- }),
- )
- .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
- pane.close_all_items(action, cx)
- .map(|task| task.detach_and_log_err(cx));
- }))
- .on_action(
- cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
- pane.close_active_item(action, cx)
- .map(|task| task.detach_and_log_err(cx));
- }),
- )
- .on_action(
- cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
- pane.project.update(cx, |_, cx| {
- cx.emit(project::Event::RevealInProjectPanel(
- ProjectEntryId::from_proto(action.entry_id),
- ))
- })
- }),
- )
- .when(self.active_item().is_some(), |pane| {
- pane.child(self.render_tab_bar(cx))
- })
- .child({
- let has_worktrees = self.project.read(cx).worktrees().next().is_some();
- // main content
- div()
- .flex_1()
- .relative()
- .group("")
- .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
- .on_drag_move::<ProjectEntryId>(cx.listener(Self::handle_drag_move))
- .map(|div| {
- if let Some(item) = self.active_item() {
- div.v_flex()
- .child(self.toolbar.clone())
- .child(item.to_any())
- } else {
- let placeholder = div.h_flex().size_full().justify_center();
- if has_worktrees {
- placeholder
- } else {
- placeholder.child(
- Label::new("Open a file or project to get started.")
- .color(Color::Muted),
- )
- }
- }
- })
- .child(
- // drag target
- div()
- .z_index(1)
- .invisible()
- .absolute()
- .bg(theme::color_alpha(
- cx.theme().colors().drop_target_background,
- 0.75,
- ))
- .group_drag_over::<DraggedTab>("", |style| style.visible())
- .group_drag_over::<ProjectEntryId>("", |style| style.visible())
- .when_some(self.can_drop_predicate.clone(), |this, p| {
- this.can_drop(move |a, cx| p(a, cx))
- })
- .on_drop(cx.listener(move |this, dragged_tab, cx| {
- this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
- }))
- .on_drop(cx.listener(move |this, entry_id, cx| {
- this.handle_project_entry_drop(entry_id, cx)
- }))
- .map(|div| match self.drag_split_direction {
- None => div.top_0().left_0().right_0().bottom_0(),
- Some(SplitDirection::Up) => div.top_0().left_0().right_0().h_32(),
- Some(SplitDirection::Down) => {
- div.left_0().bottom_0().right_0().h_32()
- }
- Some(SplitDirection::Left) => {
- div.top_0().left_0().bottom_0().w_32()
- }
- Some(SplitDirection::Right) => {
- div.top_0().bottom_0().right_0().w_32()
- }
- }),
- )
- })
- .on_mouse_down(
- MouseButton::Navigate(NavigationDirection::Back),
- cx.listener(|pane, _, cx| {
- if let Some(workspace) = pane.workspace.upgrade() {
- let pane = cx.view().downgrade();
- cx.window_context().defer(move |cx| {
- workspace.update(cx, |workspace, cx| {
- workspace.go_back(pane, cx).detach_and_log_err(cx)
- })
- })
- }
- }),
- )
- .on_mouse_down(
- MouseButton::Navigate(NavigationDirection::Forward),
- cx.listener(|pane, _, cx| {
- if let Some(workspace) = pane.workspace.upgrade() {
- let pane = cx.view().downgrade();
- cx.window_context().defer(move |cx| {
- workspace.update(cx, |workspace, cx| {
- workspace.go_forward(pane, cx).detach_and_log_err(cx)
- })
- })
- }
- }),
- )
- }
-}
-
-impl ItemNavHistory {
- pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
- self.history.push(data, self.item.clone(), cx);
- }
-
- pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
- self.history.pop(NavigationMode::GoingBack, cx)
- }
-
- pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
- self.history.pop(NavigationMode::GoingForward, cx)
- }
-}
-
-impl NavHistory {
- pub fn for_each_entry(
- &self,
- cx: &AppContext,
- mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
- ) {
- let borrowed_history = self.0.lock();
- borrowed_history
- .forward_stack
- .iter()
- .chain(borrowed_history.backward_stack.iter())
- .chain(borrowed_history.closed_stack.iter())
- .for_each(|entry| {
- if let Some(project_and_abs_path) =
- borrowed_history.paths_by_item.get(&entry.item.id())
- {
- f(entry, project_and_abs_path.clone());
- } else if let Some(item) = entry.item.upgrade() {
- if let Some(path) = item.project_path(cx) {
- f(entry, (path, None));
- }
- }
- })
- }
-
- pub fn set_mode(&mut self, mode: NavigationMode) {
- self.0.lock().mode = mode;
- }
-
- pub fn mode(&self) -> NavigationMode {
- self.0.lock().mode
- }
-
- pub fn disable(&mut self) {
- self.0.lock().mode = NavigationMode::Disabled;
- }
-
- pub fn enable(&mut self) {
- self.0.lock().mode = NavigationMode::Normal;
- }
-
- pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
- let mut state = self.0.lock();
- let entry = match mode {
- NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
- return None
- }
- NavigationMode::GoingBack => &mut state.backward_stack,
- NavigationMode::GoingForward => &mut state.forward_stack,
- NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
- }
- .pop_back();
- if entry.is_some() {
- state.did_update(cx);
- }
- entry
- }
-
- pub fn push<D: 'static + Send + Any>(
- &mut self,
- data: Option<D>,
- item: Arc<dyn WeakItemHandle>,
- cx: &mut WindowContext,
- ) {
- let state = &mut *self.0.lock();
- match state.mode {
- NavigationMode::Disabled => {}
- NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
- if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
- state.backward_stack.pop_front();
- }
- state.backward_stack.push_back(NavigationEntry {
- item,
- data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
- timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
- });
- state.forward_stack.clear();
- }
- NavigationMode::GoingBack => {
- if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
- state.forward_stack.pop_front();
- }
- state.forward_stack.push_back(NavigationEntry {
- item,
- data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
- timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
- });
- }
- NavigationMode::GoingForward => {
- if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
- state.backward_stack.pop_front();
- }
- state.backward_stack.push_back(NavigationEntry {
- item,
- data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
- timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
- });
- }
- NavigationMode::ClosingItem => {
- if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
- state.closed_stack.pop_front();
- }
- state.closed_stack.push_back(NavigationEntry {
- item,
- data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
- timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
- });
- }
- }
- state.did_update(cx);
- }
-
- pub fn remove_item(&mut self, item_id: EntityId) {
- let mut state = self.0.lock();
- state.paths_by_item.remove(&item_id);
- state
- .backward_stack
- .retain(|entry| entry.item.id() != item_id);
- state
- .forward_stack
- .retain(|entry| entry.item.id() != item_id);
- state
- .closed_stack
- .retain(|entry| entry.item.id() != item_id);
- }
-
- pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
- self.0.lock().paths_by_item.get(&item_id).cloned()
- }
-}
-
-impl NavHistoryState {
- pub fn did_update(&self, cx: &mut WindowContext) {
- if let Some(pane) = self.pane.upgrade() {
- cx.defer(move |cx| {
- pane.update(cx, |pane, cx| pane.history_updated(cx));
- });
- }
- }
-}
-
-fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
- let path = buffer_path
- .as_ref()
- .and_then(|p| p.path.to_str())
- .unwrap_or(&"This buffer");
- let path = truncate_and_remove_front(path, 80);
- format!("{path} contains unsaved edits. Do you want to save it?")
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::item::test::{TestItem, TestProjectItem};
- use gpui::{TestAppContext, VisualTestContext};
- use project::FakeFs;
- use settings::SettingsStore;
- use theme::LoadThemes;
-
- #[gpui::test]
- async fn test_remove_active_empty(cx: &mut TestAppContext) {
- init_test(cx);
- let fs = FakeFs::new(cx.executor());
-
- let project = Project::test(fs, None, cx).await;
- let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
- let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
-
- pane.update(cx, |pane, cx| {
- assert!(pane
- .close_active_item(&CloseActiveItem { save_intent: None }, cx)
- .is_none())
- });
- }
-
- #[gpui::test]
- async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
- init_test(cx);
- let fs = FakeFs::new(cx.executor());
-
- let project = Project::test(fs, None, cx).await;
- let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
- let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
-
- // 1. Add with a destination index
- // a. Add before the active item
- set_labeled_items(&pane, ["A", "B*", "C"], cx);
- pane.update(cx, |pane, cx| {
- pane.add_item(
- Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
- false,
- false,
- Some(0),
- cx,
- );
- });
- assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
-
- // b. Add after the active item
- set_labeled_items(&pane, ["A", "B*", "C"], cx);
- pane.update(cx, |pane, cx| {
- pane.add_item(
- Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
- false,
- false,
- Some(2),
- cx,
- );
- });
- assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
-
- // c. Add at the end of the item list (including off the length)
- set_labeled_items(&pane, ["A", "B*", "C"], cx);
- pane.update(cx, |pane, cx| {
- pane.add_item(
- Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
- false,
- false,
- Some(5),
- cx,
- );
- });
- assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
-
- // 2. Add without a destination index
- // a. Add with active item at the start of the item list
- set_labeled_items(&pane, ["A*", "B", "C"], cx);
- pane.update(cx, |pane, cx| {
- pane.add_item(
- Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
- false,
- false,
- None,
- cx,
- );
- });
- set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
-
- // b. Add with active item at the end of the item list
- set_labeled_items(&pane, ["A", "B", "C*"], cx);
- pane.update(cx, |pane, cx| {
- pane.add_item(
- Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))),
- false,
- false,
- None,
- cx,
- );
- });
- assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
- }
-
- #[gpui::test]
- async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
- init_test(cx);
- let fs = FakeFs::new(cx.executor());
-
- let project = Project::test(fs, None, cx).await;
- let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
- let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
-
- // 1. Add with a destination index
- // 1a. Add before the active item
- let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
- pane.update(cx, |pane, cx| {
- pane.add_item(d, false, false, Some(0), cx);
- });
- assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
-
- // 1b. Add after the active item
- let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
- pane.update(cx, |pane, cx| {
- pane.add_item(d, false, false, Some(2), cx);
- });
- assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
-
- // 1c. Add at the end of the item list (including off the length)
- let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
- pane.update(cx, |pane, cx| {
- pane.add_item(a, false, false, Some(5), cx);
- });
- assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
-
- // 1d. Add same item to active index
- let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
- pane.update(cx, |pane, cx| {
- pane.add_item(b, false, false, Some(1), cx);
- });
- assert_item_labels(&pane, ["A", "B*", "C"], cx);
-
- // 1e. Add item to index after same item in last position
- let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
- pane.update(cx, |pane, cx| {
- pane.add_item(c, false, false, Some(2), cx);
- });
- assert_item_labels(&pane, ["A", "B", "C*"], cx);
-
- // 2. Add without a destination index
- // 2a. Add with active item at the start of the item list
- let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
- pane.update(cx, |pane, cx| {
- pane.add_item(d, false, false, None, cx);
- });
- assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
-
- // 2b. Add with active item at the end of the item list
- let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
- pane.update(cx, |pane, cx| {
- pane.add_item(a, false, false, None, cx);
- });
- assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
-
- // 2c. Add active item to active item at end of list
- let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
- pane.update(cx, |pane, cx| {
- pane.add_item(c, false, false, None, cx);
- });
- assert_item_labels(&pane, ["A", "B", "C*"], cx);
-
- // 2d. Add active item to active item at start of list
- let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
- pane.update(cx, |pane, cx| {
- pane.add_item(a, false, false, None, cx);
- });
- assert_item_labels(&pane, ["A*", "B", "C"], cx);
- }
-
- #[gpui::test]
- async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
- init_test(cx);
- let fs = FakeFs::new(cx.executor());
-
- let project = Project::test(fs, None, cx).await;
- let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
- let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
-
- // singleton view
- pane.update(cx, |pane, cx| {
- pane.add_item(
- Box::new(cx.new_view(|cx| {
- TestItem::new(cx)
- .with_singleton(true)
- .with_label("buffer 1")
- .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
- })),
- false,
- false,
- None,
- cx,
- );
- });
- assert_item_labels(&pane, ["buffer 1*"], cx);
-
- // new singleton view with the same project entry
- pane.update(cx, |pane, cx| {
- pane.add_item(
- Box::new(cx.new_view(|cx| {
- TestItem::new(cx)
- .with_singleton(true)
- .with_label("buffer 1")
- .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
- })),
- false,
- false,
- None,
- cx,
- );
- });
- assert_item_labels(&pane, ["buffer 1*"], cx);
-
- // new singleton view with different project entry
- pane.update(cx, |pane, cx| {
- pane.add_item(
- Box::new(cx.new_view(|cx| {
- TestItem::new(cx)
- .with_singleton(true)
- .with_label("buffer 2")
- .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
- })),
- false,
- false,
- None,
- cx,
- );
- });
- assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
-
- // new multibuffer view with the same project entry
- pane.update(cx, |pane, cx| {
- pane.add_item(
- Box::new(cx.new_view(|cx| {
- TestItem::new(cx)
- .with_singleton(false)
- .with_label("multibuffer 1")
- .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
- })),
- false,
- false,
- None,
- cx,
- );
- });
- assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
-
- // another multibuffer view with the same project entry
- pane.update(cx, |pane, cx| {
- pane.add_item(
- Box::new(cx.new_view(|cx| {
- TestItem::new(cx)
- .with_singleton(false)
- .with_label("multibuffer 1b")
- .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
- })),
- false,
- false,
- None,
- cx,
- );
- });
- assert_item_labels(
- &pane,
- ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
- cx,
- );
- }
-
- #[gpui::test]
- async fn test_remove_item_ordering(cx: &mut TestAppContext) {
- init_test(cx);
- let fs = FakeFs::new(cx.executor());
-
- let project = Project::test(fs, None, cx).await;
- let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
- let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
-
- add_labeled_item(&pane, "A", false, cx);
- add_labeled_item(&pane, "B", false, cx);
- add_labeled_item(&pane, "C", false, cx);
- add_labeled_item(&pane, "D", false, cx);
- assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
-
- pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
- add_labeled_item(&pane, "1", false, cx);
- assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
-
- pane.update(cx, |pane, cx| {
- pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
- })
- .unwrap()
- .await
- .unwrap();
- assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
-
- pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
- assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
-
- pane.update(cx, |pane, cx| {
- pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
- })
- .unwrap()
- .await
- .unwrap();
- assert_item_labels(&pane, ["A", "B*", "C"], cx);
-
- pane.update(cx, |pane, cx| {
- pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
- })
- .unwrap()
- .await
- .unwrap();
- assert_item_labels(&pane, ["A", "C*"], cx);
-
- pane.update(cx, |pane, cx| {
- pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
- })
- .unwrap()
- .await
- .unwrap();
- assert_item_labels(&pane, ["A*"], cx);
- }
-
- #[gpui::test]
- async fn test_close_inactive_items(cx: &mut TestAppContext) {
- init_test(cx);
- let fs = FakeFs::new(cx.executor());
-
- let project = Project::test(fs, None, cx).await;
- let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
- let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
-
- set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
-
- pane.update(cx, |pane, cx| {
- pane.close_inactive_items(&CloseInactiveItems, cx)
- })
- .unwrap()
- .await
- .unwrap();
- assert_item_labels(&pane, ["C*"], cx);
- }
-
- #[gpui::test]
- async fn test_close_clean_items(cx: &mut TestAppContext) {
- init_test(cx);
- let fs = FakeFs::new(cx.executor());
-
- let project = Project::test(fs, None, cx).await;
- let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
- let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
-
- add_labeled_item(&pane, "A", true, cx);
- add_labeled_item(&pane, "B", false, cx);
- add_labeled_item(&pane, "C", true, cx);
- add_labeled_item(&pane, "D", false, cx);
- add_labeled_item(&pane, "E", false, cx);
- assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
-
- pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
- .unwrap()
- .await
- .unwrap();
- assert_item_labels(&pane, ["A^", "C*^"], cx);
- }
-
- #[gpui::test]
- async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
- init_test(cx);
- let fs = FakeFs::new(cx.executor());
-
- let project = Project::test(fs, None, cx).await;
- let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
- let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
-
- set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
-
- pane.update(cx, |pane, cx| {
- pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
- })
- .unwrap()
- .await
- .unwrap();
- assert_item_labels(&pane, ["C*", "D", "E"], cx);
- }
-
- #[gpui::test]
- async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
- init_test(cx);
- let fs = FakeFs::new(cx.executor());
-
- let project = Project::test(fs, None, cx).await;
- let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
- let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
-
- set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
-
- pane.update(cx, |pane, cx| {
- pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
- })
- .unwrap()
- .await
- .unwrap();
- assert_item_labels(&pane, ["A", "B", "C*"], cx);
- }
-
- #[gpui::test]
- async fn test_close_all_items(cx: &mut TestAppContext) {
- init_test(cx);
- let fs = FakeFs::new(cx.executor());
-
- let project = Project::test(fs, None, cx).await;
- let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
- let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
-
- add_labeled_item(&pane, "A", false, cx);
- add_labeled_item(&pane, "B", false, cx);
- add_labeled_item(&pane, "C", false, cx);
- assert_item_labels(&pane, ["A", "B", "C*"], cx);
-
- pane.update(cx, |pane, cx| {
- pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
- })
- .unwrap()
- .await
- .unwrap();
- assert_item_labels(&pane, [], cx);
-
- add_labeled_item(&pane, "A", true, cx);
- add_labeled_item(&pane, "B", true, cx);
- add_labeled_item(&pane, "C", true, cx);
- assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
-
- let save = pane
- .update(cx, |pane, cx| {
- pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
- })
- .unwrap();
-
- cx.executor().run_until_parked();
- cx.simulate_prompt_answer(2);
- save.await.unwrap();
- assert_item_labels(&pane, [], cx);
- }
-
- fn init_test(cx: &mut TestAppContext) {
- cx.update(|cx| {
- let settings_store = SettingsStore::test(cx);
- cx.set_global(settings_store);
- theme::init(LoadThemes::JustBase, cx);
- crate::init_settings(cx);
- Project::init_settings(cx);
- });
- }
-
- fn add_labeled_item(
- pane: &View<Pane>,
- label: &str,
- is_dirty: bool,
- cx: &mut VisualTestContext,
- ) -> Box<View<TestItem>> {
- pane.update(cx, |pane, cx| {
- let labeled_item = Box::new(
- cx.new_view(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty)),
- );
- pane.add_item(labeled_item.clone(), false, false, None, cx);
- labeled_item
- })
- }
-
- fn set_labeled_items<const COUNT: usize>(
- pane: &View<Pane>,
- labels: [&str; COUNT],
- cx: &mut VisualTestContext,
- ) -> [Box<View<TestItem>>; COUNT] {
- pane.update(cx, |pane, cx| {
- pane.items.clear();
- let mut active_item_index = 0;
-
- let mut index = 0;
- let items = labels.map(|mut label| {
- if label.ends_with("*") {
- label = label.trim_end_matches("*");
- active_item_index = index;
- }
-
- let labeled_item = Box::new(cx.new_view(|cx| TestItem::new(cx).with_label(label)));
- pane.add_item(labeled_item.clone(), false, false, None, cx);
- index += 1;
- labeled_item
- });
-
- pane.activate_item(active_item_index, false, false, cx);
-
- items
- })
- }
-
- // Assert the item label, with the active item label suffixed with a '*'
- fn assert_item_labels<const COUNT: usize>(
- pane: &View<Pane>,
- expected_states: [&str; COUNT],
- cx: &mut VisualTestContext,
- ) {
- pane.update(cx, |pane, cx| {
- let actual_states = pane
- .items
- .iter()
- .enumerate()
- .map(|(ix, item)| {
- let mut state = item
- .to_any()
- .downcast::<TestItem>()
- .unwrap()
- .read(cx)
- .label
- .clone();
- if ix == pane.active_item_index {
- state.push('*');
- }
- if item.is_dirty(cx) {
- state.push('^');
- }
- state
- })
- .collect::<Vec<_>>();
-
- assert_eq!(
- actual_states, expected_states,
- "pane items do not match expectation"
- );
- })
- }
-}
-
-impl Render for DraggedTab {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
- let item = &self.pane.read(cx).items[self.ix];
- let label = item.tab_content(Some(self.detail), false, cx);
- Tab::new("")
- .selected(self.is_active)
- .child(label)
- .render(cx)
- .font(ui_font)
- }
-}
@@ -1,239 +0,0 @@
-use super::DraggedItem;
-use crate::{Pane, SplitDirection, Workspace};
-use gpui::{
- color::Color,
- elements::{Canvas, MouseEventHandler, ParentComponent, Stack},
- geometry::{rect::RectF, vector::Vector2F},
- platform::MouseButton,
- scene::MouseUp,
- AppContext, Element, EventContext, MouseState, Quad, ViewContext, WeakViewHandle,
-};
-use project2::ProjectEntryId;
-
-pub fn dragged_item_receiver<Tag, D, F>(
- pane: &Pane,
- region_id: usize,
- drop_index: usize,
- allow_same_pane: bool,
- split_margin: Option<f32>,
- cx: &mut ViewContext<Pane>,
- render_child: F,
-) -> MouseEventHandler<Pane>
-where
- Tag: 'static,
- D: Element<Pane>,
- F: FnOnce(&mut MouseState, &mut ViewContext<Pane>) -> D,
-{
- let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
- let drag_position = if (pane.can_drop)(drag_and_drop, cx) {
- drag_and_drop
- .currently_dragged::<DraggedItem>(cx.window())
- .map(|(drag_position, _)| drag_position)
- .or_else(|| {
- drag_and_drop
- .currently_dragged::<ProjectEntryId>(cx.window())
- .map(|(drag_position, _)| drag_position)
- })
- } else {
- None
- };
-
- let mut handler = MouseEventHandler::above::<Tag, _>(region_id, cx, |state, cx| {
- // Observing hovered will cause a render when the mouse enters regardless
- // of if mouse position was accessed before
- let drag_position = if state.dragging() {
- drag_position
- } else {
- None
- };
- Stack::new()
- .with_child(render_child(state, cx))
- .with_children(drag_position.map(|drag_position| {
- Canvas::new(move |bounds, _, _, cx| {
- if bounds.contains_point(drag_position) {
- let overlay_region = split_margin
- .and_then(|split_margin| {
- drop_split_direction(drag_position, bounds, split_margin)
- .map(|dir| (dir, split_margin))
- })
- .map(|(dir, margin)| dir.along_edge(bounds, margin))
- .unwrap_or(bounds);
-
- cx.scene().push_stacking_context(None, None);
- let background = overlay_color(cx);
- cx.scene().push_quad(Quad {
- bounds: overlay_region,
- background: Some(background),
- border: Default::default(),
- corner_radii: Default::default(),
- });
- cx.scene().pop_stacking_context();
- }
- })
- }))
- });
-
- if drag_position.is_some() {
- handler = handler
- .on_up(MouseButton::Left, {
- move |event, pane, cx| {
- let workspace = pane.workspace.clone();
- let pane = cx.weak_handle();
- handle_dropped_item(
- event,
- workspace,
- &pane,
- drop_index,
- allow_same_pane,
- split_margin,
- cx,
- );
- cx.notify();
- }
- })
- .on_move(|_, _, cx| {
- let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
-
- if drag_and_drop
- .currently_dragged::<DraggedItem>(cx.window())
- .is_some()
- || drag_and_drop
- .currently_dragged::<ProjectEntryId>(cx.window())
- .is_some()
- {
- cx.notify();
- } else {
- cx.propagate_event();
- }
- })
- }
-
- handler
-}
-
-pub fn handle_dropped_item<V: 'static>(
- event: MouseUp,
- workspace: WeakViewHandle<Workspace>,
- pane: &WeakViewHandle<Pane>,
- index: usize,
- allow_same_pane: bool,
- split_margin: Option<f32>,
- cx: &mut EventContext<V>,
-) {
- enum Action {
- Move(WeakViewHandle<Pane>, usize),
- Open(ProjectEntryId),
- }
- let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
- let action = if let Some((_, dragged_item)) =
- drag_and_drop.currently_dragged::<DraggedItem>(cx.window())
- {
- Action::Move(dragged_item.pane.clone(), dragged_item.handle.id())
- } else if let Some((_, project_entry)) =
- drag_and_drop.currently_dragged::<ProjectEntryId>(cx.window())
- {
- Action::Open(*project_entry)
- } else {
- cx.propagate_event();
- return;
- };
-
- if let Some(split_direction) =
- split_margin.and_then(|margin| drop_split_direction(event.position, event.region, margin))
- {
- let pane_to_split = pane.clone();
- match action {
- Action::Move(from, item_id_to_move) => {
- cx.window_context().defer(move |cx| {
- if let Some(workspace) = workspace.upgrade(cx) {
- workspace.update(cx, |workspace, cx| {
- workspace.split_pane_with_item(
- pane_to_split,
- split_direction,
- from,
- item_id_to_move,
- cx,
- );
- })
- }
- });
- }
- Action::Open(project_entry) => {
- cx.window_context().defer(move |cx| {
- if let Some(workspace) = workspace.upgrade(cx) {
- workspace.update(cx, |workspace, cx| {
- if let Some(task) = workspace.split_pane_with_project_entry(
- pane_to_split,
- split_direction,
- project_entry,
- cx,
- ) {
- task.detach_and_log_err(cx);
- }
- })
- }
- });
- }
- };
- } else {
- match action {
- Action::Move(from, item_id) => {
- if pane != &from || allow_same_pane {
- let pane = pane.clone();
- cx.window_context().defer(move |cx| {
- if let Some(((workspace, from), to)) = workspace
- .upgrade(cx)
- .zip(from.upgrade(cx))
- .zip(pane.upgrade(cx))
- {
- workspace.update(cx, |workspace, cx| {
- workspace.move_item(from, to, item_id, index, cx);
- })
- }
- });
- } else {
- cx.propagate_event();
- }
- }
- Action::Open(project_entry) => {
- let pane = pane.clone();
- cx.window_context().defer(move |cx| {
- if let Some(workspace) = workspace.upgrade(cx) {
- workspace.update(cx, |workspace, cx| {
- if let Some(path) =
- workspace.project.read(cx).path_for_entry(project_entry, cx)
- {
- workspace
- .open_path(path, Some(pane), true, cx)
- .detach_and_log_err(cx);
- }
- });
- }
- });
- }
- }
- }
-}
-
-fn drop_split_direction(
- position: Vector2F,
- region: RectF,
- split_margin: f32,
-) -> Option<SplitDirection> {
- let mut min_direction = None;
- let mut min_distance = split_margin;
- for direction in SplitDirection::all() {
- let edge_distance = (direction.edge(region) - direction.axis().component(position)).abs();
-
- if edge_distance < min_distance {
- min_direction = Some(direction);
- min_distance = edge_distance;
- }
- }
-
- min_direction
-}
-
-fn overlay_color(cx: &AppContext) -> Color {
- theme2::current(cx).workspace.drop_target_overlay_color
-}
@@ -1,865 +0,0 @@
-use crate::{pane_group::element::pane_axis, AppState, FollowerState, Pane, Workspace};
-use anyhow::{anyhow, Result};
-use call::{ActiveCall, ParticipantLocation};
-use collections::HashMap;
-use gpui::{
- point, size, AnyWeakView, Axis, Bounds, Entity as _, IntoElement, Model, Pixels, Point, View,
- ViewContext,
-};
-use parking_lot::Mutex;
-use project::Project;
-use serde::Deserialize;
-use std::sync::Arc;
-use ui::{prelude::*, Button};
-
-const HANDLE_HITBOX_SIZE: f32 = 10.0; //todo!(change this back to 4)
-const HORIZONTAL_MIN_SIZE: f32 = 80.;
-const VERTICAL_MIN_SIZE: f32 = 100.;
-
-#[derive(Clone)]
-pub struct PaneGroup {
- pub(crate) root: Member,
-}
-
-impl PaneGroup {
- pub(crate) fn with_root(root: Member) -> Self {
- Self { root }
- }
-
- pub fn new(pane: View<Pane>) -> Self {
- Self {
- root: Member::Pane(pane),
- }
- }
-
- pub fn split(
- &mut self,
- old_pane: &View<Pane>,
- new_pane: &View<Pane>,
- direction: SplitDirection,
- ) -> Result<()> {
- match &mut self.root {
- Member::Pane(pane) => {
- if pane == old_pane {
- self.root = Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
- Ok(())
- } else {
- Err(anyhow!("Pane not found"))
- }
- }
- Member::Axis(axis) => axis.split(old_pane, new_pane, direction),
- }
- }
-
- pub fn bounding_box_for_pane(&self, pane: &View<Pane>) -> Option<Bounds<Pixels>> {
- match &self.root {
- Member::Pane(_) => None,
- Member::Axis(axis) => axis.bounding_box_for_pane(pane),
- }
- }
-
- pub fn pane_at_pixel_position(&self, coordinate: Point<Pixels>) -> Option<&View<Pane>> {
- match &self.root {
- Member::Pane(pane) => Some(pane),
- Member::Axis(axis) => axis.pane_at_pixel_position(coordinate),
- }
- }
-
- /// Returns:
- /// - Ok(true) if it found and removed a pane
- /// - Ok(false) if it found but did not remove the pane
- /// - Err(_) if it did not find the pane
- pub fn remove(&mut self, pane: &View<Pane>) -> Result<bool> {
- match &mut self.root {
- Member::Pane(_) => Ok(false),
- Member::Axis(axis) => {
- if let Some(last_pane) = axis.remove(pane)? {
- self.root = last_pane;
- }
- Ok(true)
- }
- }
- }
-
- pub fn swap(&mut self, from: &View<Pane>, to: &View<Pane>) {
- match &mut self.root {
- Member::Pane(_) => {}
- Member::Axis(axis) => axis.swap(from, to),
- };
- }
-
- pub(crate) fn render(
- &self,
- project: &Model<Project>,
- follower_states: &HashMap<View<Pane>, FollowerState>,
- active_call: Option<&Model<ActiveCall>>,
- active_pane: &View<Pane>,
- zoomed: Option<&AnyWeakView>,
- app_state: &Arc<AppState>,
- cx: &mut ViewContext<Workspace>,
- ) -> impl IntoElement {
- self.root.render(
- project,
- 0,
- follower_states,
- active_call,
- active_pane,
- zoomed,
- app_state,
- cx,
- )
- }
-
- pub(crate) fn panes(&self) -> Vec<&View<Pane>> {
- let mut panes = Vec::new();
- self.root.collect_panes(&mut panes);
- panes
- }
-
- pub(crate) fn first_pane(&self) -> View<Pane> {
- self.root.first_pane()
- }
-}
-
-#[derive(Clone)]
-pub(crate) enum Member {
- Axis(PaneAxis),
- Pane(View<Pane>),
-}
-
-impl Member {
- fn new_axis(old_pane: View<Pane>, new_pane: View<Pane>, direction: SplitDirection) -> Self {
- use Axis::*;
- use SplitDirection::*;
-
- let axis = match direction {
- Up | Down => Vertical,
- Left | Right => Horizontal,
- };
-
- let members = match direction {
- Up | Left => vec![Member::Pane(new_pane), Member::Pane(old_pane)],
- Down | Right => vec![Member::Pane(old_pane), Member::Pane(new_pane)],
- };
-
- Member::Axis(PaneAxis::new(axis, members))
- }
-
- fn contains(&self, needle: &View<Pane>) -> bool {
- match self {
- Member::Axis(axis) => axis.members.iter().any(|member| member.contains(needle)),
- Member::Pane(pane) => pane == needle,
- }
- }
-
- fn first_pane(&self) -> View<Pane> {
- match self {
- Member::Axis(axis) => axis.members[0].first_pane(),
- Member::Pane(pane) => pane.clone(),
- }
- }
-
- pub fn render(
- &self,
- project: &Model<Project>,
- basis: usize,
- follower_states: &HashMap<View<Pane>, FollowerState>,
- active_call: Option<&Model<ActiveCall>>,
- active_pane: &View<Pane>,
- zoomed: Option<&AnyWeakView>,
- app_state: &Arc<AppState>,
- cx: &mut ViewContext<Workspace>,
- ) -> impl IntoElement {
- match self {
- Member::Pane(pane) => {
- if zoomed == Some(&pane.downgrade().into()) {
- return div().into_any();
- }
-
- let leader = follower_states.get(pane).and_then(|state| {
- let room = active_call?.read(cx).room()?.read(cx);
- room.remote_participant_for_peer_id(state.leader_id)
- });
-
- let mut leader_border = None;
- let mut leader_status_box = None;
- if let Some(leader) = &leader {
- let mut leader_color = cx
- .theme()
- .players()
- .color_for_participant(leader.participant_index.0)
- .cursor;
- leader_color.fade_out(0.3);
- leader_border = Some(leader_color);
-
- leader_status_box = match leader.location {
- ParticipantLocation::SharedProject {
- project_id: leader_project_id,
- } => {
- if Some(leader_project_id) == project.read(cx).remote_id() {
- None
- } else {
- let leader_user = leader.user.clone();
- let leader_user_id = leader.user.id;
- Some(
- Button::new(
- ("leader-status", pane.entity_id()),
- format!(
- "Follow {} to their active project",
- leader_user.github_login,
- ),
- )
- .on_click(cx.listener(
- move |this, _, cx| {
- crate::join_remote_project(
- leader_project_id,
- leader_user_id,
- this.app_state().clone(),
- cx,
- )
- .detach_and_log_err(cx);
- },
- )),
- )
- }
- }
- ParticipantLocation::UnsharedProject => Some(Button::new(
- ("leader-status", pane.entity_id()),
- format!(
- "{} is viewing an unshared Zed project",
- leader.user.github_login
- ),
- )),
- ParticipantLocation::External => Some(Button::new(
- ("leader-status", pane.entity_id()),
- format!(
- "{} is viewing a window outside of Zed",
- leader.user.github_login
- ),
- )),
- };
- }
-
- div()
- .relative()
- .flex_1()
- .size_full()
- .child(pane.clone())
- .when_some(leader_border, |this, color| {
- this.border_2().border_color(color)
- })
- .when_some(leader_status_box, |this, status_box| {
- this.child(
- div()
- .absolute()
- .w_96()
- .bottom_3()
- .right_3()
- .z_index(1)
- .child(status_box),
- )
- })
- .into_any()
-
- // let el = div()
- // .flex()
- // .flex_1()
- // .gap_px()
- // .w_full()
- // .h_full()
- // .bg(cx.theme().colors().editor)
- // .children();
- }
- Member::Axis(axis) => axis
- .render(
- project,
- basis + 1,
- follower_states,
- active_call,
- active_pane,
- zoomed,
- app_state,
- cx,
- )
- .into_any(),
- }
- }
-
- fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a View<Pane>>) {
- match self {
- Member::Axis(axis) => {
- for member in &axis.members {
- member.collect_panes(panes);
- }
- }
- Member::Pane(pane) => panes.push(pane),
- }
- }
-}
-
-#[derive(Clone)]
-pub(crate) struct PaneAxis {
- pub axis: Axis,
- pub members: Vec<Member>,
- pub flexes: Arc<Mutex<Vec<f32>>>,
- pub bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
-}
-
-impl PaneAxis {
- pub fn new(axis: Axis, members: Vec<Member>) -> Self {
- let flexes = Arc::new(Mutex::new(vec![1.; members.len()]));
- let bounding_boxes = Arc::new(Mutex::new(vec![None; members.len()]));
- Self {
- axis,
- members,
- flexes,
- bounding_boxes,
- }
- }
-
- pub fn load(axis: Axis, members: Vec<Member>, flexes: Option<Vec<f32>>) -> Self {
- let flexes = flexes.unwrap_or_else(|| vec![1.; members.len()]);
- debug_assert!(members.len() == flexes.len());
-
- let flexes = Arc::new(Mutex::new(flexes));
- let bounding_boxes = Arc::new(Mutex::new(vec![None; members.len()]));
- Self {
- axis,
- members,
- flexes,
- bounding_boxes,
- }
- }
-
- fn split(
- &mut self,
- old_pane: &View<Pane>,
- new_pane: &View<Pane>,
- direction: SplitDirection,
- ) -> Result<()> {
- for (mut idx, member) in self.members.iter_mut().enumerate() {
- match member {
- Member::Axis(axis) => {
- if axis.split(old_pane, new_pane, direction).is_ok() {
- return Ok(());
- }
- }
- Member::Pane(pane) => {
- if pane == old_pane {
- if direction.axis() == self.axis {
- if direction.increasing() {
- idx += 1;
- }
-
- self.members.insert(idx, Member::Pane(new_pane.clone()));
- *self.flexes.lock() = vec![1.; self.members.len()];
- } else {
- *member =
- Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
- }
- return Ok(());
- }
- }
- }
- }
- Err(anyhow!("Pane not found"))
- }
-
- fn remove(&mut self, pane_to_remove: &View<Pane>) -> Result<Option<Member>> {
- let mut found_pane = false;
- let mut remove_member = None;
- for (idx, member) in self.members.iter_mut().enumerate() {
- match member {
- Member::Axis(axis) => {
- if let Ok(last_pane) = axis.remove(pane_to_remove) {
- if let Some(last_pane) = last_pane {
- *member = last_pane;
- }
- found_pane = true;
- break;
- }
- }
- Member::Pane(pane) => {
- if pane == pane_to_remove {
- found_pane = true;
- remove_member = Some(idx);
- break;
- }
- }
- }
- }
-
- if found_pane {
- if let Some(idx) = remove_member {
- self.members.remove(idx);
- *self.flexes.lock() = vec![1.; self.members.len()];
- }
-
- if self.members.len() == 1 {
- let result = self.members.pop();
- *self.flexes.lock() = vec![1.; self.members.len()];
- Ok(result)
- } else {
- Ok(None)
- }
- } else {
- Err(anyhow!("Pane not found"))
- }
- }
-
- fn swap(&mut self, from: &View<Pane>, to: &View<Pane>) {
- for member in self.members.iter_mut() {
- match member {
- Member::Axis(axis) => axis.swap(from, to),
- Member::Pane(pane) => {
- if pane == from {
- *member = Member::Pane(to.clone());
- } else if pane == to {
- *member = Member::Pane(from.clone())
- }
- }
- }
- }
- }
-
- fn bounding_box_for_pane(&self, pane: &View<Pane>) -> Option<Bounds<Pixels>> {
- debug_assert!(self.members.len() == self.bounding_boxes.lock().len());
-
- for (idx, member) in self.members.iter().enumerate() {
- match member {
- Member::Pane(found) => {
- if pane == found {
- return self.bounding_boxes.lock()[idx];
- }
- }
- Member::Axis(axis) => {
- if let Some(rect) = axis.bounding_box_for_pane(pane) {
- return Some(rect);
- }
- }
- }
- }
- None
- }
-
- fn pane_at_pixel_position(&self, coordinate: Point<Pixels>) -> Option<&View<Pane>> {
- debug_assert!(self.members.len() == self.bounding_boxes.lock().len());
-
- let bounding_boxes = self.bounding_boxes.lock();
-
- for (idx, member) in self.members.iter().enumerate() {
- if let Some(coordinates) = bounding_boxes[idx] {
- if coordinates.contains(&coordinate) {
- return match member {
- Member::Pane(found) => Some(found),
- Member::Axis(axis) => axis.pane_at_pixel_position(coordinate),
- };
- }
- }
- }
- None
- }
-
- fn render(
- &self,
- project: &Model<Project>,
- basis: usize,
- follower_states: &HashMap<View<Pane>, FollowerState>,
- active_call: Option<&Model<ActiveCall>>,
- active_pane: &View<Pane>,
- zoomed: Option<&AnyWeakView>,
- app_state: &Arc<AppState>,
- cx: &mut ViewContext<Workspace>,
- ) -> gpui::AnyElement {
- debug_assert!(self.members.len() == self.flexes.lock().len());
- let mut active_pane_ix = None;
-
- pane_axis(
- self.axis,
- basis,
- self.flexes.clone(),
- self.bounding_boxes.clone(),
- )
- .children(self.members.iter().enumerate().map(|(ix, member)| {
- if member.contains(active_pane) {
- active_pane_ix = Some(ix);
- }
- member
- .render(
- project,
- (basis + ix) * 10,
- follower_states,
- active_call,
- active_pane,
- zoomed,
- app_state,
- cx,
- )
- .into_any_element()
- }))
- .with_active_pane(active_pane_ix)
- .into_any_element()
- }
-}
-
-#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
-pub enum SplitDirection {
- Up,
- Down,
- Left,
- Right,
-}
-
-impl SplitDirection {
- pub fn all() -> [Self; 4] {
- [Self::Up, Self::Down, Self::Left, Self::Right]
- }
-
- pub fn edge(&self, rect: Bounds<Pixels>) -> Pixels {
- match self {
- Self::Up => rect.origin.y,
- Self::Down => rect.lower_left().y,
- Self::Left => rect.lower_left().x,
- Self::Right => rect.lower_right().x,
- }
- }
-
- pub fn along_edge(&self, bounds: Bounds<Pixels>, length: Pixels) -> Bounds<Pixels> {
- match self {
- Self::Up => Bounds {
- origin: bounds.origin,
- size: size(bounds.size.width, length),
- },
- Self::Down => Bounds {
- origin: point(bounds.lower_left().x, bounds.lower_left().y - length),
- size: size(bounds.size.width, length),
- },
- Self::Left => Bounds {
- origin: bounds.origin,
- size: size(length, bounds.size.height),
- },
- Self::Right => Bounds {
- origin: point(bounds.lower_right().x - length, bounds.lower_left().y),
- size: size(length, bounds.size.height),
- },
- }
- }
-
- pub fn axis(&self) -> Axis {
- match self {
- Self::Up | Self::Down => Axis::Vertical,
- Self::Left | Self::Right => Axis::Horizontal,
- }
- }
-
- pub fn increasing(&self) -> bool {
- match self {
- Self::Left | Self::Up => false,
- Self::Down | Self::Right => true,
- }
- }
-}
-
-mod element {
-
- use std::{cell::RefCell, iter, rc::Rc, sync::Arc};
-
- use gpui::{
- px, relative, Along, AnyElement, Axis, Bounds, CursorStyle, Element, InteractiveBounds,
- IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point,
- Size, Style, WindowContext,
- };
- use parking_lot::Mutex;
- use smallvec::SmallVec;
- use ui::prelude::*;
-
- use super::{HANDLE_HITBOX_SIZE, HORIZONTAL_MIN_SIZE, VERTICAL_MIN_SIZE};
-
- const DIVIDER_SIZE: f32 = 1.0;
-
- pub fn pane_axis(
- axis: Axis,
- basis: usize,
- flexes: Arc<Mutex<Vec<f32>>>,
- bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
- ) -> PaneAxisElement {
- PaneAxisElement {
- axis,
- basis,
- flexes,
- bounding_boxes,
- children: SmallVec::new(),
- active_pane_ix: None,
- }
- }
-
- pub struct PaneAxisElement {
- axis: Axis,
- basis: usize,
- flexes: Arc<Mutex<Vec<f32>>>,
- bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
- children: SmallVec<[AnyElement; 2]>,
- active_pane_ix: Option<usize>,
- }
-
- impl PaneAxisElement {
- pub fn with_active_pane(mut self, active_pane_ix: Option<usize>) -> Self {
- self.active_pane_ix = active_pane_ix;
- self
- }
-
- fn compute_resize(
- flexes: &Arc<Mutex<Vec<f32>>>,
- e: &MouseMoveEvent,
- ix: usize,
- axis: Axis,
- child_start: Point<Pixels>,
- container_size: Size<Pixels>,
- cx: &mut WindowContext,
- ) {
- let min_size = match axis {
- Axis::Horizontal => px(HORIZONTAL_MIN_SIZE),
- Axis::Vertical => px(VERTICAL_MIN_SIZE),
- };
- let mut flexes = flexes.lock();
- debug_assert!(flex_values_in_bounds(flexes.as_slice()));
-
- let size = move |ix, flexes: &[f32]| {
- container_size.along(axis) * (flexes[ix] / flexes.len() as f32)
- };
-
- // Don't allow resizing to less than the minimum size, if elements are already too small
- if min_size - px(1.) > size(ix, flexes.as_slice()) {
- return;
- }
-
- let mut proposed_current_pixel_change =
- (e.position - child_start).along(axis) - size(ix, flexes.as_slice());
-
- let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| {
- let flex_change = pixel_dx / container_size.along(axis);
- let current_target_flex = flexes[target_ix] + flex_change;
- let next_target_flex = flexes[(target_ix as isize + next) as usize] - flex_change;
- (current_target_flex, next_target_flex)
- };
-
- let mut successors = iter::from_fn({
- let forward = proposed_current_pixel_change > px(0.);
- let mut ix_offset = 0;
- let len = flexes.len();
- move || {
- let result = if forward {
- (ix + 1 + ix_offset < len).then(|| ix + ix_offset)
- } else {
- (ix as isize - ix_offset as isize >= 0).then(|| ix - ix_offset)
- };
-
- ix_offset += 1;
-
- result
- }
- });
-
- while proposed_current_pixel_change.abs() > px(0.) {
- let Some(current_ix) = successors.next() else {
- break;
- };
-
- let next_target_size = Pixels::max(
- size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change,
- min_size,
- );
-
- let current_target_size = Pixels::max(
- size(current_ix, flexes.as_slice()) + size(current_ix + 1, flexes.as_slice())
- - next_target_size,
- min_size,
- );
-
- let current_pixel_change =
- current_target_size - size(current_ix, flexes.as_slice());
-
- let (current_target_flex, next_target_flex) =
- flex_changes(current_pixel_change, current_ix, 1, flexes.as_slice());
-
- flexes[current_ix] = current_target_flex;
- flexes[current_ix + 1] = next_target_flex;
-
- proposed_current_pixel_change -= current_pixel_change;
- }
-
- // todo!(schedule serialize)
- // workspace.schedule_serialize(cx);
- cx.notify();
- }
-
- fn push_handle(
- flexes: Arc<Mutex<Vec<f32>>>,
- dragged_handle: Rc<RefCell<Option<usize>>>,
- axis: Axis,
- ix: usize,
- pane_bounds: Bounds<Pixels>,
- axis_bounds: Bounds<Pixels>,
- cx: &mut WindowContext,
- ) {
- let handle_bounds = Bounds {
- origin: pane_bounds.origin.apply_along(axis, |origin| {
- origin + pane_bounds.size.along(axis) - px(HANDLE_HITBOX_SIZE / 2.)
- }),
- size: pane_bounds
- .size
- .apply_along(axis, |_| px(HANDLE_HITBOX_SIZE)),
- };
- let divider_bounds = Bounds {
- origin: pane_bounds
- .origin
- .apply_along(axis, |origin| origin + pane_bounds.size.along(axis)),
- size: pane_bounds.size.apply_along(axis, |_| px(DIVIDER_SIZE)),
- };
-
- cx.with_z_index(3, |cx| {
- let interactive_handle_bounds = InteractiveBounds {
- bounds: handle_bounds,
- stacking_order: cx.stacking_order().clone(),
- };
- if interactive_handle_bounds.visibly_contains(&cx.mouse_position(), cx) {
- cx.set_cursor_style(match axis {
- Axis::Vertical => CursorStyle::ResizeUpDown,
- Axis::Horizontal => CursorStyle::ResizeLeftRight,
- })
- }
-
- cx.add_opaque_layer(handle_bounds);
- cx.paint_quad(gpui::fill(divider_bounds, cx.theme().colors().border));
-
- cx.on_mouse_event({
- let dragged_handle = dragged_handle.clone();
- move |e: &MouseDownEvent, phase, _cx| {
- if phase.bubble() && handle_bounds.contains(&e.position) {
- dragged_handle.replace(Some(ix));
- }
- }
- });
- cx.on_mouse_event(move |e: &MouseMoveEvent, phase, cx| {
- let dragged_handle = dragged_handle.borrow();
- if phase.bubble() && *dragged_handle == Some(ix) {
- Self::compute_resize(
- &flexes,
- e,
- ix,
- axis,
- pane_bounds.origin,
- axis_bounds.size,
- cx,
- )
- }
- });
- });
- }
- }
-
- impl IntoElement for PaneAxisElement {
- type Element = Self;
-
- fn element_id(&self) -> Option<ui::prelude::ElementId> {
- Some(self.basis.into())
- }
-
- fn into_element(self) -> Self::Element {
- self
- }
- }
-
- impl Element for PaneAxisElement {
- type State = Rc<RefCell<Option<usize>>>;
-
- fn request_layout(
- &mut self,
- state: Option<Self::State>,
- cx: &mut ui::prelude::WindowContext,
- ) -> (gpui::LayoutId, Self::State) {
- let mut style = Style::default();
- style.flex_grow = 1.;
- style.flex_shrink = 1.;
- style.flex_basis = relative(0.).into();
- style.size.width = relative(1.).into();
- style.size.height = relative(1.).into();
- let layout_id = cx.request_layout(&style, None);
- let dragged_pane = state.unwrap_or_else(|| Rc::new(RefCell::new(None)));
- (layout_id, dragged_pane)
- }
-
- fn paint(
- &mut self,
- bounds: gpui::Bounds<ui::prelude::Pixels>,
- state: &mut Self::State,
- cx: &mut ui::prelude::WindowContext,
- ) {
- let flexes = self.flexes.lock().clone();
- let len = self.children.len();
- debug_assert!(flexes.len() == len);
- debug_assert!(flex_values_in_bounds(flexes.as_slice()));
-
- let mut origin = bounds.origin;
- let space_per_flex = bounds.size.along(self.axis) / len as f32;
-
- let mut bounding_boxes = self.bounding_boxes.lock();
- bounding_boxes.clear();
-
- for (ix, child) in self.children.iter_mut().enumerate() {
- //todo!(active_pane_magnification)
- // If using active pane magnification, need to switch to using
- // 1 for all non-active panes, and then the magnification for the
- // active pane.
- let child_size = bounds
- .size
- .apply_along(self.axis, |_| space_per_flex * flexes[ix]);
-
- let child_bounds = Bounds {
- origin,
- size: child_size,
- };
- bounding_boxes.push(Some(child_bounds));
- cx.with_z_index(0, |cx| {
- child.draw(origin, child_size.into(), cx);
- });
- cx.with_z_index(1, |cx| {
- if ix < len - 1 {
- Self::push_handle(
- self.flexes.clone(),
- state.clone(),
- self.axis,
- ix,
- child_bounds,
- bounds,
- cx,
- );
- }
- });
-
- origin = origin.apply_along(self.axis, |val| val + child_size.along(self.axis));
- }
-
- cx.with_z_index(1, |cx| {
- cx.on_mouse_event({
- let state = state.clone();
- move |_: &MouseUpEvent, phase, _cx| {
- if phase.bubble() {
- state.replace(None);
- }
- }
- });
- })
- }
- }
-
- impl ParentElement for PaneAxisElement {
- fn children_mut(&mut self) -> &mut smallvec::SmallVec<[AnyElement; 2]> {
- &mut self.children
- }
- }
-
- fn flex_values_in_bounds(flexes: &[f32]) -> bool {
- (flexes.iter().copied().sum::<f32>() - flexes.len() as f32).abs() < 0.001
- }
-}
@@ -1,973 +0,0 @@
-//#![allow(dead_code)]
-
-pub mod model;
-
-use std::path::Path;
-
-use anyhow::{anyhow, bail, Context, Result};
-use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
-use gpui::{Axis, WindowBounds};
-
-use util::{unzip_option, ResultExt};
-use uuid::Uuid;
-
-use crate::WorkspaceId;
-
-use model::{
- GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
- WorkspaceLocation,
-};
-
-use self::model::DockStructure;
-
-define_connection! {
- // Current schema shape using pseudo-rust syntax:
- //
- // workspaces(
- // workspace_id: usize, // Primary key for workspaces
- // workspace_location: Bincode<Vec<PathBuf>>,
- // dock_visible: bool, // Deprecated
- // dock_anchor: DockAnchor, // Deprecated
- // dock_pane: Option<usize>, // Deprecated
- // left_sidebar_open: boolean,
- // timestamp: String, // UTC YYYY-MM-DD HH:MM:SS
- // window_state: String, // WindowBounds Discriminant
- // window_x: Option<f32>, // WindowBounds::Fixed RectF x
- // window_y: Option<f32>, // WindowBounds::Fixed RectF y
- // window_width: Option<f32>, // WindowBounds::Fixed RectF width
- // window_height: Option<f32>, // WindowBounds::Fixed RectF height
- // display: Option<Uuid>, // Display id
- // )
- //
- // pane_groups(
- // group_id: usize, // Primary key for pane_groups
- // workspace_id: usize, // References workspaces table
- // parent_group_id: Option<usize>, // None indicates that this is the root node
- // position: Optiopn<usize>, // None indicates that this is the root node
- // axis: Option<Axis>, // 'Vertical', 'Horizontal'
- // flexes: Option<Vec<f32>>, // A JSON array of floats
- // )
- //
- // panes(
- // pane_id: usize, // Primary key for panes
- // workspace_id: usize, // References workspaces table
- // active: bool,
- // )
- //
- // center_panes(
- // pane_id: usize, // Primary key for center_panes
- // parent_group_id: Option<usize>, // References pane_groups. If none, this is the root
- // position: Option<usize>, // None indicates this is the root
- // )
- //
- // CREATE TABLE items(
- // item_id: usize, // This is the item's view id, so this is not unique
- // workspace_id: usize, // References workspaces table
- // pane_id: usize, // References panes table
- // kind: String, // Indicates which view this connects to. This is the key in the item_deserializers global
- // position: usize, // Position of the item in the parent pane. This is equivalent to panes' position column
- // active: bool, // Indicates if this item is the active one in the pane
- // )
- pub static ref DB: WorkspaceDb<()> =
- &[sql!(
- CREATE TABLE workspaces(
- workspace_id INTEGER PRIMARY KEY,
- workspace_location BLOB UNIQUE,
- dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
- dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
- dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
- left_sidebar_open INTEGER, // Boolean
- timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
- FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
- ) STRICT;
-
- CREATE TABLE pane_groups(
- group_id INTEGER PRIMARY KEY,
- workspace_id INTEGER NOT NULL,
- parent_group_id INTEGER, // NULL indicates that this is a root node
- position INTEGER, // NULL indicates that this is a root node
- axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
- FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
- ON DELETE CASCADE
- ON UPDATE CASCADE,
- FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
- ) STRICT;
-
- CREATE TABLE panes(
- pane_id INTEGER PRIMARY KEY,
- workspace_id INTEGER NOT NULL,
- active INTEGER NOT NULL, // Boolean
- FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
- ON DELETE CASCADE
- ON UPDATE CASCADE
- ) STRICT;
-
- CREATE TABLE center_panes(
- pane_id INTEGER PRIMARY KEY,
- parent_group_id INTEGER, // NULL means that this is a root pane
- position INTEGER, // NULL means that this is a root pane
- FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
- ON DELETE CASCADE,
- FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
- ) STRICT;
-
- CREATE TABLE items(
- item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
- workspace_id INTEGER NOT NULL,
- pane_id INTEGER NOT NULL,
- kind TEXT NOT NULL,
- position INTEGER NOT NULL,
- active INTEGER NOT NULL,
- FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
- ON DELETE CASCADE
- ON UPDATE CASCADE,
- FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
- ON DELETE CASCADE,
- PRIMARY KEY(item_id, workspace_id)
- ) STRICT;
- ),
- sql!(
- ALTER TABLE workspaces ADD COLUMN window_state TEXT;
- ALTER TABLE workspaces ADD COLUMN window_x REAL;
- ALTER TABLE workspaces ADD COLUMN window_y REAL;
- ALTER TABLE workspaces ADD COLUMN window_width REAL;
- ALTER TABLE workspaces ADD COLUMN window_height REAL;
- ALTER TABLE workspaces ADD COLUMN display BLOB;
- ),
- // Drop foreign key constraint from workspaces.dock_pane to panes table.
- sql!(
- CREATE TABLE workspaces_2(
- workspace_id INTEGER PRIMARY KEY,
- workspace_location BLOB UNIQUE,
- dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
- dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
- dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
- left_sidebar_open INTEGER, // Boolean
- timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
- window_state TEXT,
- window_x REAL,
- window_y REAL,
- window_width REAL,
- window_height REAL,
- display BLOB
- ) STRICT;
- INSERT INTO workspaces_2 SELECT * FROM workspaces;
- DROP TABLE workspaces;
- ALTER TABLE workspaces_2 RENAME TO workspaces;
- ),
- // Add panels related information
- sql!(
- ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
- ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
- ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
- ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
- ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
- ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
- ),
- // Add panel zoom persistence
- sql!(
- ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
- ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
- ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
- ),
- // Add pane group flex data
- sql!(
- ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
- )
- ];
-}
-
-impl WorkspaceDb {
- /// Returns a serialized workspace for the given worktree_roots. If the passed array
- /// is empty, the most recent workspace is returned instead. If no workspace for the
- /// passed roots is stored, returns none.
- pub fn workspace_for_roots<P: AsRef<Path>>(
- &self,
- worktree_roots: &[P],
- ) -> Option<SerializedWorkspace> {
- let workspace_location: WorkspaceLocation = worktree_roots.into();
-
- // Note that we re-assign the workspace_id here in case it's empty
- // and we've grabbed the most recent workspace
- let (workspace_id, workspace_location, bounds, display, docks): (
- WorkspaceId,
- WorkspaceLocation,
- Option<WindowBounds>,
- Option<Uuid>,
- DockStructure,
- ) = self
- .select_row_bound(sql! {
- SELECT
- workspace_id,
- workspace_location,
- window_state,
- window_x,
- window_y,
- window_width,
- window_height,
- display,
- left_dock_visible,
- left_dock_active_panel,
- left_dock_zoom,
- right_dock_visible,
- right_dock_active_panel,
- right_dock_zoom,
- bottom_dock_visible,
- bottom_dock_active_panel,
- bottom_dock_zoom
- FROM workspaces
- WHERE workspace_location = ?
- })
- .and_then(|mut prepared_statement| (prepared_statement)(&workspace_location))
- .context("No workspaces found")
- .warn_on_err()
- .flatten()?;
-
- Some(SerializedWorkspace {
- id: workspace_id,
- location: workspace_location.clone(),
- center_group: self
- .get_center_pane_group(workspace_id)
- .context("Getting center group")
- .log_err()?,
- bounds,
- display,
- docks,
- })
- }
-
- /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
- /// that used this workspace previously
- pub async fn save_workspace(&self, workspace: SerializedWorkspace) {
- self.write(move |conn| {
- conn.with_savepoint("update_worktrees", || {
- // Clear out panes and pane_groups
- conn.exec_bound(sql!(
- DELETE FROM pane_groups WHERE workspace_id = ?1;
- DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
- .expect("Clearing old panes");
-
- conn.exec_bound(sql!(
- DELETE FROM workspaces WHERE workspace_location = ? AND workspace_id != ?
- ))?((&workspace.location, workspace.id.clone()))
- .context("clearing out old locations")?;
-
- // Upsert
- conn.exec_bound(sql!(
- INSERT INTO workspaces(
- workspace_id,
- workspace_location,
- left_dock_visible,
- left_dock_active_panel,
- left_dock_zoom,
- right_dock_visible,
- right_dock_active_panel,
- right_dock_zoom,
- bottom_dock_visible,
- bottom_dock_active_panel,
- bottom_dock_zoom,
- timestamp
- )
- VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
- ON CONFLICT DO
- UPDATE SET
- workspace_location = ?2,
- left_dock_visible = ?3,
- left_dock_active_panel = ?4,
- left_dock_zoom = ?5,
- right_dock_visible = ?6,
- right_dock_active_panel = ?7,
- right_dock_zoom = ?8,
- bottom_dock_visible = ?9,
- bottom_dock_active_panel = ?10,
- bottom_dock_zoom = ?11,
- timestamp = CURRENT_TIMESTAMP
- ))?((workspace.id, &workspace.location, workspace.docks))
- .context("Updating workspace")?;
-
- // Save center pane group
- Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
- .context("save pane group in save workspace")?;
-
- Ok(())
- })
- .log_err();
- })
- .await;
- }
-
- query! {
- pub async fn next_id() -> Result<WorkspaceId> {
- INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
- }
- }
-
- query! {
- fn recent_workspaces() -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
- SELECT workspace_id, workspace_location
- FROM workspaces
- WHERE workspace_location IS NOT NULL
- ORDER BY timestamp DESC
- }
- }
-
- query! {
- async fn delete_stale_workspace(id: WorkspaceId) -> Result<()> {
- DELETE FROM workspaces
- WHERE workspace_id IS ?
- }
- }
-
- // Returns the recent locations which are still valid on disk and deletes ones which no longer
- // exist.
- pub async fn recent_workspaces_on_disk(&self) -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
- let mut result = Vec::new();
- let mut delete_tasks = Vec::new();
- for (id, location) in self.recent_workspaces()? {
- if location.paths().iter().all(|path| path.exists())
- && location.paths().iter().any(|path| path.is_dir())
- {
- result.push((id, location));
- } else {
- delete_tasks.push(self.delete_stale_workspace(id));
- }
- }
-
- futures::future::join_all(delete_tasks).await;
- Ok(result)
- }
-
- pub async fn last_workspace(&self) -> Result<Option<WorkspaceLocation>> {
- Ok(self
- .recent_workspaces_on_disk()
- .await?
- .into_iter()
- .next()
- .map(|(_, location)| location))
- }
-
- fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
- Ok(self
- .get_pane_group(workspace_id, None)?
- .into_iter()
- .next()
- .unwrap_or_else(|| {
- SerializedPaneGroup::Pane(SerializedPane {
- active: true,
- children: vec![],
- })
- }))
- }
-
- fn get_pane_group(
- &self,
- workspace_id: WorkspaceId,
- group_id: Option<GroupId>,
- ) -> Result<Vec<SerializedPaneGroup>> {
- type GroupKey = (Option<GroupId>, WorkspaceId);
- type GroupOrPane = (
- Option<GroupId>,
- Option<Axis>,
- Option<PaneId>,
- Option<bool>,
- Option<String>,
- );
- self.select_bound::<GroupKey, GroupOrPane>(sql!(
- SELECT group_id, axis, pane_id, active, flexes
- FROM (SELECT
- group_id,
- axis,
- NULL as pane_id,
- NULL as active,
- position,
- parent_group_id,
- workspace_id,
- flexes
- FROM pane_groups
- UNION
- SELECT
- NULL,
- NULL,
- center_panes.pane_id,
- panes.active as active,
- position,
- parent_group_id,
- panes.workspace_id as workspace_id,
- NULL
- FROM center_panes
- JOIN panes ON center_panes.pane_id = panes.pane_id)
- WHERE parent_group_id IS ? AND workspace_id = ?
- ORDER BY position
- ))?((group_id, workspace_id))?
- .into_iter()
- .map(|(group_id, axis, pane_id, active, flexes)| {
- if let Some((group_id, axis)) = group_id.zip(axis) {
- let flexes = flexes
- .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
- .transpose()?;
-
- Ok(SerializedPaneGroup::Group {
- axis,
- children: self.get_pane_group(workspace_id, Some(group_id))?,
- flexes,
- })
- } else if let Some((pane_id, active)) = pane_id.zip(active) {
- Ok(SerializedPaneGroup::Pane(SerializedPane::new(
- self.get_items(pane_id)?,
- active,
- )))
- } else {
- bail!("Pane Group Child was neither a pane group or a pane");
- }
- })
- // Filter out panes and pane groups which don't have any children or items
- .filter(|pane_group| match pane_group {
- Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
- Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
- _ => true,
- })
- .collect::<Result<_>>()
- }
-
- fn save_pane_group(
- conn: &Connection,
- workspace_id: WorkspaceId,
- pane_group: &SerializedPaneGroup,
- parent: Option<(GroupId, usize)>,
- ) -> Result<()> {
- match pane_group {
- SerializedPaneGroup::Group {
- axis,
- children,
- flexes,
- } => {
- let (parent_id, position) = unzip_option(parent);
-
- let flex_string = flexes
- .as_ref()
- .map(|flexes| serde_json::json!(flexes).to_string());
-
- let group_id = conn.select_row_bound::<_, i64>(sql!(
- INSERT INTO pane_groups(
- workspace_id,
- parent_group_id,
- position,
- axis,
- flexes
- )
- VALUES (?, ?, ?, ?, ?)
- RETURNING group_id
- ))?((
- workspace_id,
- parent_id,
- position,
- *axis,
- flex_string,
- ))?
- .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
-
- for (position, group) in children.iter().enumerate() {
- Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
- }
-
- Ok(())
- }
- SerializedPaneGroup::Pane(pane) => {
- Self::save_pane(conn, workspace_id, &pane, parent)?;
- Ok(())
- }
- }
- }
-
- fn save_pane(
- conn: &Connection,
- workspace_id: WorkspaceId,
- pane: &SerializedPane,
- parent: Option<(GroupId, usize)>,
- ) -> Result<PaneId> {
- let pane_id = conn.select_row_bound::<_, i64>(sql!(
- INSERT INTO panes(workspace_id, active)
- VALUES (?, ?)
- RETURNING pane_id
- ))?((workspace_id, pane.active))?
- .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
-
- let (parent_id, order) = unzip_option(parent);
- conn.exec_bound(sql!(
- INSERT INTO center_panes(pane_id, parent_group_id, position)
- VALUES (?, ?, ?)
- ))?((pane_id, parent_id, order))?;
-
- Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
-
- Ok(pane_id)
- }
-
- fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
- Ok(self.select_bound(sql!(
- SELECT kind, item_id, active FROM items
- WHERE pane_id = ?
- ORDER BY position
- ))?(pane_id)?)
- }
-
- fn save_items(
- conn: &Connection,
- workspace_id: WorkspaceId,
- pane_id: PaneId,
- items: &[SerializedItem],
- ) -> Result<()> {
- let mut insert = conn.exec_bound(sql!(
- INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active) VALUES (?, ?, ?, ?, ?, ?)
- )).context("Preparing insertion")?;
- for (position, item) in items.iter().enumerate() {
- insert((workspace_id, pane_id, position, item))?;
- }
-
- Ok(())
- }
-
- query! {
- pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
- UPDATE workspaces
- SET timestamp = CURRENT_TIMESTAMP
- WHERE workspace_id = ?
- }
- }
-
- query! {
- pub async fn set_window_bounds(workspace_id: WorkspaceId, bounds: WindowBounds, display: Uuid) -> Result<()> {
- UPDATE workspaces
- SET window_state = ?2,
- window_x = ?3,
- window_y = ?4,
- window_width = ?5,
- window_height = ?6,
- display = ?7
- WHERE workspace_id = ?1
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use db::open_test_db;
- use gpui;
-
- #[gpui::test]
- async fn test_next_id_stability() {
- env_logger::try_init().ok();
-
- let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
-
- db.write(|conn| {
- conn.migrate(
- "test_table",
- &[sql!(
- CREATE TABLE test_table(
- text TEXT,
- workspace_id INTEGER,
- FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
- ON DELETE CASCADE
- ) STRICT;
- )],
- )
- .unwrap();
- })
- .await;
-
- let id = db.next_id().await.unwrap();
- // Assert the empty row got inserted
- assert_eq!(
- Some(id),
- db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
- SELECT workspace_id FROM workspaces WHERE workspace_id = ?
- ))
- .unwrap()(id)
- .unwrap()
- );
-
- db.write(move |conn| {
- conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
- .unwrap()(("test-text-1", id))
- .unwrap()
- })
- .await;
-
- let test_text_1 = db
- .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
- .unwrap()(1)
- .unwrap()
- .unwrap();
- assert_eq!(test_text_1, "test-text-1");
- }
-
- #[gpui::test]
- async fn test_workspace_id_stability() {
- env_logger::try_init().ok();
-
- let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
-
- db.write(|conn| {
- conn.migrate(
- "test_table",
- &[sql!(
- CREATE TABLE test_table(
- text TEXT,
- workspace_id INTEGER,
- FOREIGN KEY(workspace_id)
- REFERENCES workspaces(workspace_id)
- ON DELETE CASCADE
- ) STRICT;)],
- )
- })
- .await
- .unwrap();
-
- let mut workspace_1 = SerializedWorkspace {
- id: 1,
- location: (["/tmp", "/tmp2"]).into(),
- center_group: Default::default(),
- bounds: Default::default(),
- display: Default::default(),
- docks: Default::default(),
- };
-
- let workspace_2 = SerializedWorkspace {
- id: 2,
- location: (["/tmp"]).into(),
- center_group: Default::default(),
- bounds: Default::default(),
- display: Default::default(),
- docks: Default::default(),
- };
-
- db.save_workspace(workspace_1.clone()).await;
-
- db.write(|conn| {
- conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
- .unwrap()(("test-text-1", 1))
- .unwrap();
- })
- .await;
-
- db.save_workspace(workspace_2.clone()).await;
-
- db.write(|conn| {
- conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
- .unwrap()(("test-text-2", 2))
- .unwrap();
- })
- .await;
-
- workspace_1.location = (["/tmp", "/tmp3"]).into();
- db.save_workspace(workspace_1.clone()).await;
- db.save_workspace(workspace_1).await;
- db.save_workspace(workspace_2).await;
-
- let test_text_2 = db
- .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
- .unwrap()(2)
- .unwrap()
- .unwrap();
- assert_eq!(test_text_2, "test-text-2");
-
- let test_text_1 = db
- .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
- .unwrap()(1)
- .unwrap()
- .unwrap();
- assert_eq!(test_text_1, "test-text-1");
- }
-
- fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
- SerializedPaneGroup::Group {
- axis,
- flexes: None,
- children,
- }
- }
-
- #[gpui::test]
- async fn test_full_workspace_serialization() {
- env_logger::try_init().ok();
-
- let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
-
- // -----------------
- // | 1,2 | 5,6 |
- // | - - - | |
- // | 3,4 | |
- // -----------------
- let center_group = group(
- Axis::Horizontal,
- vec![
- group(
- Axis::Vertical,
- vec![
- SerializedPaneGroup::Pane(SerializedPane::new(
- vec![
- SerializedItem::new("Terminal", 5, false),
- SerializedItem::new("Terminal", 6, true),
- ],
- false,
- )),
- SerializedPaneGroup::Pane(SerializedPane::new(
- vec![
- SerializedItem::new("Terminal", 7, true),
- SerializedItem::new("Terminal", 8, false),
- ],
- false,
- )),
- ],
- ),
- SerializedPaneGroup::Pane(SerializedPane::new(
- vec![
- SerializedItem::new("Terminal", 9, false),
- SerializedItem::new("Terminal", 10, true),
- ],
- false,
- )),
- ],
- );
-
- let workspace = SerializedWorkspace {
- id: 5,
- location: (["/tmp", "/tmp2"]).into(),
- center_group,
- bounds: Default::default(),
- display: Default::default(),
- docks: Default::default(),
- };
-
- db.save_workspace(workspace.clone()).await;
- let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
-
- assert_eq!(workspace, round_trip_workspace.unwrap());
-
- // Test guaranteed duplicate IDs
- db.save_workspace(workspace.clone()).await;
- db.save_workspace(workspace.clone()).await;
-
- let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
- assert_eq!(workspace, round_trip_workspace.unwrap());
- }
-
- #[gpui::test]
- async fn test_workspace_assignment() {
- env_logger::try_init().ok();
-
- let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
-
- let workspace_1 = SerializedWorkspace {
- id: 1,
- location: (["/tmp", "/tmp2"]).into(),
- center_group: Default::default(),
- bounds: Default::default(),
- display: Default::default(),
- docks: Default::default(),
- };
-
- let mut workspace_2 = SerializedWorkspace {
- id: 2,
- location: (["/tmp"]).into(),
- center_group: Default::default(),
- bounds: Default::default(),
- display: Default::default(),
- docks: Default::default(),
- };
-
- db.save_workspace(workspace_1.clone()).await;
- db.save_workspace(workspace_2.clone()).await;
-
- // Test that paths are treated as a set
- assert_eq!(
- db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
- workspace_1
- );
- assert_eq!(
- db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
- workspace_1
- );
-
- // Make sure that other keys work
- assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
- assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
-
- // Test 'mutate' case of updating a pre-existing id
- workspace_2.location = (["/tmp", "/tmp2"]).into();
-
- db.save_workspace(workspace_2.clone()).await;
- assert_eq!(
- db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
- workspace_2
- );
-
- // Test other mechanism for mutating
- let mut workspace_3 = SerializedWorkspace {
- id: 3,
- location: (&["/tmp", "/tmp2"]).into(),
- center_group: Default::default(),
- bounds: Default::default(),
- display: Default::default(),
- docks: Default::default(),
- };
-
- db.save_workspace(workspace_3.clone()).await;
- assert_eq!(
- db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
- workspace_3
- );
-
- // Make sure that updating paths differently also works
- workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
- db.save_workspace(workspace_3.clone()).await;
- assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
- assert_eq!(
- db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
- .unwrap(),
- workspace_3
- );
- }
-
- use crate::persistence::model::SerializedWorkspace;
- use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
-
- fn default_workspace<P: AsRef<Path>>(
- workspace_id: &[P],
- center_group: &SerializedPaneGroup,
- ) -> SerializedWorkspace {
- SerializedWorkspace {
- id: 4,
- location: workspace_id.into(),
- center_group: center_group.clone(),
- bounds: Default::default(),
- display: Default::default(),
- docks: Default::default(),
- }
- }
-
- #[gpui::test]
- async fn test_simple_split() {
- env_logger::try_init().ok();
-
- let db = WorkspaceDb(open_test_db("simple_split").await);
-
- // -----------------
- // | 1,2 | 5,6 |
- // | - - - | |
- // | 3,4 | |
- // -----------------
- let center_pane = group(
- Axis::Horizontal,
- vec![
- group(
- Axis::Vertical,
- vec![
- SerializedPaneGroup::Pane(SerializedPane::new(
- vec![
- SerializedItem::new("Terminal", 1, false),
- SerializedItem::new("Terminal", 2, true),
- ],
- false,
- )),
- SerializedPaneGroup::Pane(SerializedPane::new(
- vec![
- SerializedItem::new("Terminal", 4, false),
- SerializedItem::new("Terminal", 3, true),
- ],
- true,
- )),
- ],
- ),
- SerializedPaneGroup::Pane(SerializedPane::new(
- vec![
- SerializedItem::new("Terminal", 5, true),
- SerializedItem::new("Terminal", 6, false),
- ],
- false,
- )),
- ],
- );
-
- let workspace = default_workspace(&["/tmp"], ¢er_pane);
-
- db.save_workspace(workspace.clone()).await;
-
- let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
-
- assert_eq!(workspace.center_group, new_workspace.center_group);
- }
-
- #[gpui::test]
- async fn test_cleanup_panes() {
- env_logger::try_init().ok();
-
- let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
-
- let center_pane = group(
- Axis::Horizontal,
- vec![
- group(
- Axis::Vertical,
- vec![
- SerializedPaneGroup::Pane(SerializedPane::new(
- vec![
- SerializedItem::new("Terminal", 1, false),
- SerializedItem::new("Terminal", 2, true),
- ],
- false,
- )),
- SerializedPaneGroup::Pane(SerializedPane::new(
- vec![
- SerializedItem::new("Terminal", 4, false),
- SerializedItem::new("Terminal", 3, true),
- ],
- true,
- )),
- ],
- ),
- SerializedPaneGroup::Pane(SerializedPane::new(
- vec![
- SerializedItem::new("Terminal", 5, false),
- SerializedItem::new("Terminal", 6, true),
- ],
- false,
- )),
- ],
- );
-
- let id = &["/tmp"];
-
- let mut workspace = default_workspace(id, ¢er_pane);
-
- db.save_workspace(workspace.clone()).await;
-
- workspace.center_group = group(
- Axis::Vertical,
- vec![
- SerializedPaneGroup::Pane(SerializedPane::new(
- vec![
- SerializedItem::new("Terminal", 1, false),
- SerializedItem::new("Terminal", 2, true),
- ],
- false,
- )),
- SerializedPaneGroup::Pane(SerializedPane::new(
- vec![
- SerializedItem::new("Terminal", 4, true),
- SerializedItem::new("Terminal", 3, false),
- ],
- true,
- )),
- ],
- );
-
- db.save_workspace(workspace.clone()).await;
-
- let new_workspace = db.workspace_for_roots(id).unwrap();
-
- assert_eq!(workspace.center_group, new_workspace.center_group);
- }
-}
@@ -1,335 +0,0 @@
-use crate::{item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId};
-use anyhow::{Context, Result};
-use async_recursion::async_recursion;
-use db::sqlez::{
- bindable::{Bind, Column, StaticColumnCount},
- statement::Statement,
-};
-use gpui::{AsyncWindowContext, Axis, Model, Task, View, WeakView, WindowBounds};
-use project::Project;
-use std::{
- path::{Path, PathBuf},
- sync::Arc,
-};
-use util::ResultExt;
-use uuid::Uuid;
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct WorkspaceLocation(Arc<Vec<PathBuf>>);
-
-impl WorkspaceLocation {
- pub fn paths(&self) -> Arc<Vec<PathBuf>> {
- self.0.clone()
- }
-}
-
-impl<P: AsRef<Path>, T: IntoIterator<Item = P>> From<T> for WorkspaceLocation {
- fn from(iterator: T) -> Self {
- let mut roots = iterator
- .into_iter()
- .map(|p| p.as_ref().to_path_buf())
- .collect::<Vec<_>>();
- roots.sort();
- Self(Arc::new(roots))
- }
-}
-
-impl StaticColumnCount for WorkspaceLocation {}
-impl Bind for &WorkspaceLocation {
- fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
- bincode::serialize(&self.0)
- .expect("Bincode serialization of paths should not fail")
- .bind(statement, start_index)
- }
-}
-
-impl Column for WorkspaceLocation {
- fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
- let blob = statement.column_blob(start_index)?;
- Ok((
- WorkspaceLocation(bincode::deserialize(blob).context("Bincode failed")?),
- start_index + 1,
- ))
- }
-}
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct SerializedWorkspace {
- pub id: WorkspaceId,
- pub location: WorkspaceLocation,
- pub center_group: SerializedPaneGroup,
- pub bounds: Option<WindowBounds>,
- pub display: Option<Uuid>,
- pub docks: DockStructure,
-}
-
-#[derive(Debug, PartialEq, Clone, Default)]
-pub struct DockStructure {
- pub(crate) left: DockData,
- pub(crate) right: DockData,
- pub(crate) bottom: DockData,
-}
-
-impl Column for DockStructure {
- fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
- let (left, next_index) = DockData::column(statement, start_index)?;
- let (right, next_index) = DockData::column(statement, next_index)?;
- let (bottom, next_index) = DockData::column(statement, next_index)?;
- Ok((
- DockStructure {
- left,
- right,
- bottom,
- },
- next_index,
- ))
- }
-}
-
-impl Bind for DockStructure {
- fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
- let next_index = statement.bind(&self.left, start_index)?;
- let next_index = statement.bind(&self.right, next_index)?;
- statement.bind(&self.bottom, next_index)
- }
-}
-
-#[derive(Debug, PartialEq, Clone, Default)]
-pub struct DockData {
- pub(crate) visible: bool,
- pub(crate) active_panel: Option<String>,
- pub(crate) zoom: bool,
-}
-
-impl Column for DockData {
- fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
- let (visible, next_index) = Option::<bool>::column(statement, start_index)?;
- let (active_panel, next_index) = Option::<String>::column(statement, next_index)?;
- let (zoom, next_index) = Option::<bool>::column(statement, next_index)?;
- Ok((
- DockData {
- visible: visible.unwrap_or(false),
- active_panel,
- zoom: zoom.unwrap_or(false),
- },
- next_index,
- ))
- }
-}
-
-impl Bind for DockData {
- fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
- let next_index = statement.bind(&self.visible, start_index)?;
- let next_index = statement.bind(&self.active_panel, next_index)?;
- statement.bind(&self.zoom, next_index)
- }
-}
-
-#[derive(Debug, PartialEq, Clone)]
-pub enum SerializedPaneGroup {
- Group {
- axis: Axis,
- flexes: Option<Vec<f32>>,
- children: Vec<SerializedPaneGroup>,
- },
- Pane(SerializedPane),
-}
-
-#[cfg(test)]
-impl Default for SerializedPaneGroup {
- fn default() -> Self {
- Self::Pane(SerializedPane {
- children: vec![SerializedItem::default()],
- active: false,
- })
- }
-}
-
-impl SerializedPaneGroup {
- #[async_recursion(?Send)]
- pub(crate) async fn deserialize(
- self,
- project: &Model<Project>,
- workspace_id: WorkspaceId,
- workspace: WeakView<Workspace>,
- cx: &mut AsyncWindowContext,
- ) -> Option<(Member, Option<View<Pane>>, Vec<Option<Box<dyn ItemHandle>>>)> {
- match self {
- SerializedPaneGroup::Group {
- axis,
- children,
- flexes,
- } => {
- let mut current_active_pane = None;
- let mut members = Vec::new();
- let mut items = Vec::new();
- for child in children {
- if let Some((new_member, active_pane, new_items)) = child
- .deserialize(project, workspace_id, workspace.clone(), cx)
- .await
- {
- members.push(new_member);
- items.extend(new_items);
- current_active_pane = current_active_pane.or(active_pane);
- }
- }
-
- if members.is_empty() {
- return None;
- }
-
- if members.len() == 1 {
- return Some((members.remove(0), current_active_pane, items));
- }
-
- Some((
- Member::Axis(PaneAxis::load(axis, members, flexes)),
- current_active_pane,
- items,
- ))
- }
- SerializedPaneGroup::Pane(serialized_pane) => {
- let pane = workspace
- .update(cx, |workspace, cx| workspace.add_pane(cx).downgrade())
- .log_err()?;
- let active = serialized_pane.active;
- let new_items = serialized_pane
- .deserialize_to(project, &pane, workspace_id, workspace.clone(), cx)
- .await
- .log_err()?;
-
- if pane.update(cx, |pane, _| pane.items_len() != 0).log_err()? {
- let pane = pane.upgrade()?;
- Some((Member::Pane(pane.clone()), active.then(|| pane), new_items))
- } else {
- let pane = pane.upgrade()?;
- workspace
- .update(cx, |workspace, cx| workspace.force_remove_pane(&pane, cx))
- .log_err()?;
- None
- }
- }
- }
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Default, Clone)]
-pub struct SerializedPane {
- pub(crate) active: bool,
- pub(crate) children: Vec<SerializedItem>,
-}
-
-impl SerializedPane {
- pub fn new(children: Vec<SerializedItem>, active: bool) -> Self {
- SerializedPane { children, active }
- }
-
- pub async fn deserialize_to(
- &self,
- project: &Model<Project>,
- pane: &WeakView<Pane>,
- workspace_id: WorkspaceId,
- workspace: WeakView<Workspace>,
- cx: &mut AsyncWindowContext,
- ) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
- let mut items = Vec::new();
- let mut active_item_index = None;
- for (index, item) in self.children.iter().enumerate() {
- let project = project.clone();
- let item_handle = pane
- .update(cx, |_, cx| {
- if let Some(deserializer) = cx.global::<ItemDeserializers>().get(&item.kind) {
- deserializer(project, workspace.clone(), workspace_id, item.item_id, cx)
- } else {
- Task::ready(Err(anyhow::anyhow!(
- "Deserializer does not exist for item kind: {}",
- item.kind
- )))
- }
- })?
- .await
- .log_err();
-
- items.push(item_handle.clone());
-
- if let Some(item_handle) = item_handle {
- pane.update(cx, |pane, cx| {
- pane.add_item(item_handle.clone(), true, true, None, cx);
- })?;
- }
-
- if item.active {
- active_item_index = Some(index);
- }
- }
-
- if let Some(active_item_index) = active_item_index {
- pane.update(cx, |pane, cx| {
- pane.activate_item(active_item_index, false, false, cx);
- })?;
- }
-
- anyhow::Ok(items)
- }
-}
-
-pub type GroupId = i64;
-pub type PaneId = i64;
-pub type ItemId = u64;
-
-#[derive(Debug, PartialEq, Eq, Clone)]
-pub struct SerializedItem {
- pub kind: Arc<str>,
- pub item_id: ItemId,
- pub active: bool,
-}
-
-impl SerializedItem {
- pub fn new(kind: impl AsRef<str>, item_id: ItemId, active: bool) -> Self {
- Self {
- kind: Arc::from(kind.as_ref()),
- item_id,
- active,
- }
- }
-}
-
-#[cfg(test)]
-impl Default for SerializedItem {
- fn default() -> Self {
- SerializedItem {
- kind: Arc::from("Terminal"),
- item_id: 100000,
- active: false,
- }
- }
-}
-
-impl StaticColumnCount for SerializedItem {
- fn column_count() -> usize {
- 3
- }
-}
-impl Bind for &SerializedItem {
- fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
- let next_index = statement.bind(&self.kind, start_index)?;
- let next_index = statement.bind(&self.item_id, next_index)?;
- statement.bind(&self.active, next_index)
- }
-}
-
-impl Column for SerializedItem {
- fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
- let (kind, next_index) = Arc::<str>::column(statement, start_index)?;
- let (item_id, next_index) = ItemId::column(statement, next_index)?;
- let (active, next_index) = bool::column(statement, next_index)?;
- Ok((
- SerializedItem {
- kind,
- item_id,
- active,
- },
- next_index,
- ))
- }
-}
@@ -1,277 +0,0 @@
-use std::{any::Any, sync::Arc};
-
-use gpui::{
- AnyView, AppContext, EventEmitter, Subscription, Task, View, ViewContext, WeakView,
- WindowContext,
-};
-use project::search::SearchQuery;
-
-use crate::{
- item::{Item, WeakItemHandle},
- ItemHandle,
-};
-
-#[derive(Debug)]
-pub enum SearchEvent {
- MatchesInvalidated,
- ActiveMatchChanged,
-}
-
-#[derive(Clone, Copy, PartialEq, Eq, Debug)]
-pub enum Direction {
- Prev,
- Next,
-}
-
-#[derive(Clone, Copy, Debug, Default)]
-pub struct SearchOptions {
- pub case: bool,
- pub word: bool,
- pub regex: bool,
- /// Specifies whether the item supports search & replace.
- pub replacement: bool,
-}
-
-pub trait SearchableItem: Item + EventEmitter<SearchEvent> {
- type Match: Any + Sync + Send + Clone;
-
- fn supported_options() -> SearchOptions {
- SearchOptions {
- case: true,
- word: true,
- regex: true,
- replacement: true,
- }
- }
-
- fn clear_matches(&mut self, cx: &mut ViewContext<Self>);
- fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>);
- fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String;
- fn activate_match(
- &mut self,
- index: usize,
- matches: Vec<Self::Match>,
- cx: &mut ViewContext<Self>,
- );
- fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>);
- fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext<Self>);
- fn match_index_for_direction(
- &mut self,
- matches: &Vec<Self::Match>,
- current_index: usize,
- direction: Direction,
- count: usize,
- _: &mut ViewContext<Self>,
- ) -> usize {
- match direction {
- Direction::Prev => {
- let count = count % matches.len();
- if current_index >= count {
- current_index - count
- } else {
- matches.len() - (count - current_index)
- }
- }
- Direction::Next => (current_index + count) % matches.len(),
- }
- }
- fn find_matches(
- &mut self,
- query: Arc<SearchQuery>,
- cx: &mut ViewContext<Self>,
- ) -> Task<Vec<Self::Match>>;
- fn active_match_index(
- &mut self,
- matches: Vec<Self::Match>,
- cx: &mut ViewContext<Self>,
- ) -> Option<usize>;
-}
-
-pub trait SearchableItemHandle: ItemHandle {
- fn downgrade(&self) -> Box<dyn WeakSearchableItemHandle>;
- fn boxed_clone(&self) -> Box<dyn SearchableItemHandle>;
- fn supported_options(&self) -> SearchOptions;
- fn subscribe_to_search_events(
- &self,
- cx: &mut WindowContext,
- handler: Box<dyn Fn(&SearchEvent, &mut WindowContext) + Send>,
- ) -> Subscription;
- fn clear_matches(&self, cx: &mut WindowContext);
- fn update_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut WindowContext);
- fn query_suggestion(&self, cx: &mut WindowContext) -> String;
- fn activate_match(
- &self,
- index: usize,
- matches: &Vec<Box<dyn Any + Send>>,
- cx: &mut WindowContext,
- );
- fn select_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut WindowContext);
- fn replace(&self, _: &Box<dyn Any + Send>, _: &SearchQuery, _: &mut WindowContext);
- fn match_index_for_direction(
- &self,
- matches: &Vec<Box<dyn Any + Send>>,
- current_index: usize,
- direction: Direction,
- count: usize,
- cx: &mut WindowContext,
- ) -> usize;
- fn find_matches(
- &self,
- query: Arc<SearchQuery>,
- cx: &mut WindowContext,
- ) -> Task<Vec<Box<dyn Any + Send>>>;
- fn active_match_index(
- &self,
- matches: &Vec<Box<dyn Any + Send>>,
- cx: &mut WindowContext,
- ) -> Option<usize>;
-}
-
-// todo!("here is where we need to use AnyWeakView");
-impl<T: SearchableItem> SearchableItemHandle for View<T> {
- fn downgrade(&self) -> Box<dyn WeakSearchableItemHandle> {
- Box::new(self.downgrade())
- }
-
- fn boxed_clone(&self) -> Box<dyn SearchableItemHandle> {
- Box::new(self.clone())
- }
-
- fn supported_options(&self) -> SearchOptions {
- T::supported_options()
- }
-
- fn subscribe_to_search_events(
- &self,
- cx: &mut WindowContext,
- handler: Box<dyn Fn(&SearchEvent, &mut WindowContext) + Send>,
- ) -> Subscription {
- cx.subscribe(self, move |_, event: &SearchEvent, cx| handler(event, cx))
- }
-
- fn clear_matches(&self, cx: &mut WindowContext) {
- self.update(cx, |this, cx| this.clear_matches(cx));
- }
- fn update_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut WindowContext) {
- let matches = downcast_matches(matches);
- self.update(cx, |this, cx| this.update_matches(matches, cx));
- }
- fn query_suggestion(&self, cx: &mut WindowContext) -> String {
- self.update(cx, |this, cx| this.query_suggestion(cx))
- }
- fn activate_match(
- &self,
- index: usize,
- matches: &Vec<Box<dyn Any + Send>>,
- cx: &mut WindowContext,
- ) {
- let matches = downcast_matches(matches);
- self.update(cx, |this, cx| this.activate_match(index, matches, cx));
- }
-
- fn select_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut WindowContext) {
- let matches = downcast_matches(matches);
- self.update(cx, |this, cx| this.select_matches(matches, cx));
- }
-
- fn match_index_for_direction(
- &self,
- matches: &Vec<Box<dyn Any + Send>>,
- current_index: usize,
- direction: Direction,
- count: usize,
- cx: &mut WindowContext,
- ) -> usize {
- let matches = downcast_matches(matches);
- self.update(cx, |this, cx| {
- this.match_index_for_direction(&matches, current_index, direction, count, cx)
- })
- }
- fn find_matches(
- &self,
- query: Arc<SearchQuery>,
- cx: &mut WindowContext,
- ) -> Task<Vec<Box<dyn Any + Send>>> {
- let matches = self.update(cx, |this, cx| this.find_matches(query, cx));
- cx.spawn(|_| async {
- let matches = matches.await;
- matches
- .into_iter()
- .map::<Box<dyn Any + Send>, _>(|range| Box::new(range))
- .collect()
- })
- }
- fn active_match_index(
- &self,
- matches: &Vec<Box<dyn Any + Send>>,
- cx: &mut WindowContext,
- ) -> Option<usize> {
- let matches = downcast_matches(matches);
- self.update(cx, |this, cx| this.active_match_index(matches, cx))
- }
-
- fn replace(&self, matches: &Box<dyn Any + Send>, query: &SearchQuery, cx: &mut WindowContext) {
- let matches = matches.downcast_ref().unwrap();
- self.update(cx, |this, cx| this.replace(matches, query, cx))
- }
-}
-
-fn downcast_matches<T: Any + Clone>(matches: &Vec<Box<dyn Any + Send>>) -> Vec<T> {
- matches
- .iter()
- .map(|range| range.downcast_ref::<T>().cloned())
- .collect::<Option<Vec<_>>>()
- .expect(
- "SearchableItemHandle function called with vec of matches of a different type than expected",
- )
-}
-
-impl From<Box<dyn SearchableItemHandle>> for AnyView {
- fn from(this: Box<dyn SearchableItemHandle>) -> Self {
- this.to_any().clone()
- }
-}
-
-impl From<&Box<dyn SearchableItemHandle>> for AnyView {
- fn from(this: &Box<dyn SearchableItemHandle>) -> Self {
- this.to_any().clone()
- }
-}
-
-impl PartialEq for Box<dyn SearchableItemHandle> {
- fn eq(&self, other: &Self) -> bool {
- self.item_id() == other.item_id()
- }
-}
-
-impl Eq for Box<dyn SearchableItemHandle> {}
-
-pub trait WeakSearchableItemHandle: WeakItemHandle {
- fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
-
- // fn into_any(self) -> AnyWeakView;
-}
-
-impl<T: SearchableItem> WeakSearchableItemHandle for WeakView<T> {
- fn upgrade(&self, _cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
- Some(Box::new(self.upgrade()?))
- }
-
- // fn into_any(self) -> AnyView {
- // self.into_any()
- // }
-}
-
-impl PartialEq for Box<dyn WeakSearchableItemHandle> {
- fn eq(&self, other: &Self) -> bool {
- self.id() == other.id()
- }
-}
-
-impl Eq for Box<dyn WeakSearchableItemHandle> {}
-
-impl std::hash::Hash for Box<dyn WeakSearchableItemHandle> {
- fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
- self.id().hash(state)
- }
-}
@@ -1,128 +0,0 @@
-use crate::{
- item::{Item, ItemEvent},
- ItemNavHistory, WorkspaceId,
-};
-use anyhow::Result;
-use call::participant::{Frame, RemoteVideoTrack};
-use client::{proto::PeerId, User};
-use futures::StreamExt;
-use gpui::{
- div, img, AppContext, Element, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
- ParentElement, Render, SharedString, Styled, Task, View, ViewContext, VisualContext,
- WindowContext,
-};
-use std::sync::{Arc, Weak};
-use ui::{h_stack, prelude::*, Icon, IconElement, Label};
-
-pub enum Event {
- Close,
-}
-
-pub struct SharedScreen {
- track: Weak<RemoteVideoTrack>,
- frame: Option<Frame>,
- pub peer_id: PeerId,
- user: Arc<User>,
- nav_history: Option<ItemNavHistory>,
- _maintain_frame: Task<Result<()>>,
- focus: FocusHandle,
-}
-
-impl SharedScreen {
- pub fn new(
- track: &Arc<RemoteVideoTrack>,
- peer_id: PeerId,
- user: Arc<User>,
- cx: &mut ViewContext<Self>,
- ) -> Self {
- cx.focus_handle();
- let mut frames = track.frames();
- Self {
- track: Arc::downgrade(track),
- frame: None,
- peer_id,
- user,
- nav_history: Default::default(),
- _maintain_frame: cx.spawn(|this, mut cx| async move {
- while let Some(frame) = frames.next().await {
- this.update(&mut cx, |this, cx| {
- this.frame = Some(frame);
- cx.notify();
- })?;
- }
- this.update(&mut cx, |_, cx| cx.emit(Event::Close))?;
- Ok(())
- }),
- focus: cx.focus_handle(),
- }
- }
-}
-
-impl EventEmitter<Event> for SharedScreen {}
-
-impl FocusableView for SharedScreen {
- fn focus_handle(&self, _: &AppContext) -> FocusHandle {
- self.focus.clone()
- }
-}
-impl Render for SharedScreen {
- fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
- div().track_focus(&self.focus).size_full().children(
- self.frame
- .as_ref()
- .map(|frame| img(frame.image()).size_full()),
- )
- }
-}
-
-impl Item for SharedScreen {
- type Event = Event;
-
- fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
- Some(format!("{}'s screen", self.user.github_login).into())
- }
-
- fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
- if let Some(nav_history) = self.nav_history.as_mut() {
- nav_history.push::<()>(None, cx);
- }
- }
-
- fn tab_content(
- &self,
- _: Option<usize>,
- selected: bool,
- _: &WindowContext<'_>,
- ) -> gpui::AnyElement {
- h_stack()
- .gap_1()
- .child(IconElement::new(Icon::Screen))
- .child(
- Label::new(format!("{}'s screen", self.user.github_login)).color(if selected {
- Color::Default
- } else {
- Color::Muted
- }),
- )
- .into_any()
- }
-
- fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
- self.nav_history = Some(history);
- }
-
- fn clone_on_split(
- &self,
- _workspace_id: WorkspaceId,
- cx: &mut ViewContext<Self>,
- ) -> Option<View<Self>> {
- let track = self.track.upgrade()?;
- Some(cx.new_view(|cx| Self::new(&track, self.peer_id, self.user.clone(), cx)))
- }
-
- fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
- match event {
- Event::Close => f(ItemEvent::CloseItem),
- }
- }
-}
@@ -1,194 +0,0 @@
-use crate::{ItemHandle, Pane};
-use gpui::{
- div, AnyView, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext,
- WindowContext,
-};
-use std::any::TypeId;
-use ui::{h_stack, prelude::*};
-use util::ResultExt;
-
-pub trait StatusItemView: Render {
- fn set_active_pane_item(
- &mut self,
- active_pane_item: Option<&dyn crate::ItemHandle>,
- cx: &mut ViewContext<Self>,
- );
-}
-
-trait StatusItemViewHandle: Send {
- fn to_any(&self) -> AnyView;
- fn set_active_pane_item(
- &self,
- active_pane_item: Option<&dyn ItemHandle>,
- cx: &mut WindowContext,
- );
- fn item_type(&self) -> TypeId;
-}
-
-pub struct StatusBar {
- left_items: Vec<Box<dyn StatusItemViewHandle>>,
- right_items: Vec<Box<dyn StatusItemViewHandle>>,
- active_pane: View<Pane>,
- _observe_active_pane: Subscription,
-}
-
-impl Render for StatusBar {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- div()
- .py_0p5()
- .px_1()
- .flex()
- .items_center()
- .justify_between()
- .w_full()
- .h_8()
- .bg(cx.theme().colors().status_bar_background)
- .child(self.render_left_tools(cx))
- .child(self.render_right_tools(cx))
- }
-}
-
-impl StatusBar {
- fn render_left_tools(&self, _: &mut ViewContext<Self>) -> impl IntoElement {
- h_stack()
- .items_center()
- .gap_2()
- .children(self.left_items.iter().map(|item| item.to_any()))
- }
-
- fn render_right_tools(&self, _: &mut ViewContext<Self>) -> impl IntoElement {
- h_stack()
- .items_center()
- .gap_2()
- .children(self.right_items.iter().rev().map(|item| item.to_any()))
- }
-}
-
-impl StatusBar {
- pub fn new(active_pane: &View<Pane>, cx: &mut ViewContext<Self>) -> Self {
- let mut this = Self {
- left_items: Default::default(),
- right_items: Default::default(),
- active_pane: active_pane.clone(),
- _observe_active_pane: cx
- .observe(active_pane, |this, _, cx| this.update_active_pane_item(cx)),
- };
- this.update_active_pane_item(cx);
- this
- }
-
- pub fn add_left_item<T>(&mut self, item: View<T>, cx: &mut ViewContext<Self>)
- where
- T: 'static + StatusItemView,
- {
- let active_pane_item = self.active_pane.read(cx).active_item();
- item.set_active_pane_item(active_pane_item.as_deref(), cx);
-
- self.left_items.push(Box::new(item));
- cx.notify();
- }
-
- pub fn item_of_type<T: StatusItemView>(&self) -> Option<View<T>> {
- self.left_items
- .iter()
- .chain(self.right_items.iter())
- .find_map(|item| item.to_any().clone().downcast().log_err())
- }
-
- pub fn position_of_item<T>(&self) -> Option<usize>
- where
- T: StatusItemView,
- {
- for (index, item) in self.left_items.iter().enumerate() {
- if item.item_type() == TypeId::of::<T>() {
- return Some(index);
- }
- }
- for (index, item) in self.right_items.iter().enumerate() {
- if item.item_type() == TypeId::of::<T>() {
- return Some(index + self.left_items.len());
- }
- }
- return None;
- }
-
- pub fn insert_item_after<T>(
- &mut self,
- position: usize,
- item: View<T>,
- cx: &mut ViewContext<Self>,
- ) where
- T: 'static + StatusItemView,
- {
- let active_pane_item = self.active_pane.read(cx).active_item();
- item.set_active_pane_item(active_pane_item.as_deref(), cx);
-
- if position < self.left_items.len() {
- self.left_items.insert(position + 1, Box::new(item))
- } else {
- self.right_items
- .insert(position + 1 - self.left_items.len(), Box::new(item))
- }
- cx.notify()
- }
-
- pub fn remove_item_at(&mut self, position: usize, cx: &mut ViewContext<Self>) {
- if position < self.left_items.len() {
- self.left_items.remove(position);
- } else {
- self.right_items.remove(position - self.left_items.len());
- }
- cx.notify();
- }
-
- pub fn add_right_item<T>(&mut self, item: View<T>, cx: &mut ViewContext<Self>)
- where
- T: 'static + StatusItemView,
- {
- let active_pane_item = self.active_pane.read(cx).active_item();
- item.set_active_pane_item(active_pane_item.as_deref(), cx);
-
- self.right_items.push(Box::new(item));
- cx.notify();
- }
-
- pub fn set_active_pane(&mut self, active_pane: &View<Pane>, cx: &mut ViewContext<Self>) {
- self.active_pane = active_pane.clone();
- self._observe_active_pane =
- cx.observe(active_pane, |this, _, cx| this.update_active_pane_item(cx));
- self.update_active_pane_item(cx);
- }
-
- fn update_active_pane_item(&mut self, cx: &mut ViewContext<Self>) {
- let active_pane_item = self.active_pane.read(cx).active_item();
- for item in self.left_items.iter().chain(&self.right_items) {
- item.set_active_pane_item(active_pane_item.as_deref(), cx);
- }
- }
-}
-
-impl<T: StatusItemView> StatusItemViewHandle for View<T> {
- fn to_any(&self) -> AnyView {
- self.clone().into()
- }
-
- fn set_active_pane_item(
- &self,
- active_pane_item: Option<&dyn ItemHandle>,
- cx: &mut WindowContext,
- ) {
- self.update(cx, |this, cx| {
- this.set_active_pane_item(active_pane_item, cx)
- });
- }
-
- fn item_type(&self) -> TypeId {
- TypeId::of::<T>()
- }
-}
-
-impl From<&dyn StatusItemViewHandle> for AnyView {
- fn from(val: &dyn StatusItemViewHandle) -> Self {
- val.to_any().clone()
- }
-}
@@ -1,321 +0,0 @@
-use crate::ItemHandle;
-use gpui::{
- AnyView, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View, ViewContext,
- WindowContext,
-};
-use ui::prelude::*;
-use ui::{h_stack, v_stack};
-
-pub enum ToolbarItemEvent {
- ChangeLocation(ToolbarItemLocation),
-}
-
-pub trait ToolbarItemView: Render + EventEmitter<ToolbarItemEvent> {
- fn set_active_pane_item(
- &mut self,
- active_pane_item: Option<&dyn crate::ItemHandle>,
- cx: &mut ViewContext<Self>,
- ) -> ToolbarItemLocation;
-
- fn pane_focus_update(&mut self, _pane_focused: bool, _cx: &mut ViewContext<Self>) {}
-
- /// Number of times toolbar's height will be repeated to get the effective height.
- /// Useful when multiple rows one under each other are needed.
- /// The rows have the same width and act as a whole when reacting to resizes and similar events.
- fn row_count(&self, _cx: &WindowContext) -> usize {
- 1
- }
-}
-
-trait ToolbarItemViewHandle: Send {
- fn id(&self) -> EntityId;
- fn to_any(&self) -> AnyView;
- fn set_active_pane_item(
- &self,
- active_pane_item: Option<&dyn ItemHandle>,
- cx: &mut WindowContext,
- ) -> ToolbarItemLocation;
- fn focus_changed(&mut self, pane_focused: bool, cx: &mut WindowContext);
- fn row_count(&self, cx: &WindowContext) -> usize;
-}
-
-#[derive(Copy, Clone, Debug, PartialEq)]
-pub enum ToolbarItemLocation {
- Hidden,
- PrimaryLeft,
- PrimaryRight,
- Secondary,
-}
-
-pub struct Toolbar {
- active_item: Option<Box<dyn ItemHandle>>,
- hidden: bool,
- can_navigate: bool,
- items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
-}
-
-impl Toolbar {
- fn has_any_visible_items(&self) -> bool {
- self.items
- .iter()
- .any(|(_item, location)| *location != ToolbarItemLocation::Hidden)
- }
-
- fn left_items(&self) -> impl Iterator<Item = &dyn ToolbarItemViewHandle> {
- self.items.iter().filter_map(|(item, location)| {
- if *location == ToolbarItemLocation::PrimaryLeft {
- Some(item.as_ref())
- } else {
- None
- }
- })
- }
-
- fn right_items(&self) -> impl Iterator<Item = &dyn ToolbarItemViewHandle> {
- self.items.iter().filter_map(|(item, location)| {
- if *location == ToolbarItemLocation::PrimaryRight {
- Some(item.as_ref())
- } else {
- None
- }
- })
- }
-
- fn secondary_items(&self) -> impl Iterator<Item = &dyn ToolbarItemViewHandle> {
- self.items.iter().filter_map(|(item, location)| {
- if *location == ToolbarItemLocation::Secondary {
- Some(item.as_ref())
- } else {
- None
- }
- })
- }
-}
-
-impl Render for Toolbar {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- if !self.has_any_visible_items() {
- return div();
- }
-
- let secondary_item = self.secondary_items().next().map(|item| item.to_any());
-
- let has_left_items = self.left_items().count() > 0;
- let has_right_items = self.right_items().count() > 0;
-
- v_stack()
- .p_2()
- .when(has_left_items || has_right_items, |this| this.gap_2())
- .border_b()
- .border_color(cx.theme().colors().border_variant)
- .bg(cx.theme().colors().toolbar_background)
- .child(
- h_stack()
- .justify_between()
- .when(has_left_items, |this| {
- this.child(
- h_stack()
- .flex_1()
- .justify_start()
- .children(self.left_items().map(|item| item.to_any())),
- )
- })
- .when(has_right_items, |this| {
- this.child(
- h_stack()
- .flex_1()
- .justify_end()
- .children(self.right_items().map(|item| item.to_any())),
- )
- }),
- )
- .children(secondary_item)
- }
-}
-
-// todo!()
-// impl View for Toolbar {
-// fn ui_name() -> &'static str {
-// "Toolbar"
-// }
-
-// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-// let theme = &theme::current(cx).workspace.toolbar;
-
-// let mut primary_left_items = Vec::new();
-// let mut primary_right_items = Vec::new();
-// let mut secondary_item = None;
-// let spacing = theme.item_spacing;
-// let mut primary_items_row_count = 1;
-
-// for (item, position) in &self.items {
-// match *position {
-// ToolbarItemLocation::Hidden => {}
-
-// ToolbarItemLocation::PrimaryLeft { flex } => {
-// primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
-// let left_item = ChildView::new(item.as_any(), cx).aligned();
-// if let Some((flex, expanded)) = flex {
-// primary_left_items.push(left_item.flex(flex, expanded).into_any());
-// } else {
-// primary_left_items.push(left_item.into_any());
-// }
-// }
-
-// ToolbarItemLocation::PrimaryRight { flex } => {
-// primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
-// let right_item = ChildView::new(item.as_any(), cx).aligned().flex_float();
-// if let Some((flex, expanded)) = flex {
-// primary_right_items.push(right_item.flex(flex, expanded).into_any());
-// } else {
-// primary_right_items.push(right_item.into_any());
-// }
-// }
-
-// ToolbarItemLocation::Secondary => {
-// secondary_item = Some(
-// ChildView::new(item.as_any(), cx)
-// .constrained()
-// .with_height(theme.height * item.row_count(cx) as f32)
-// .into_any(),
-// );
-// }
-// }
-// }
-
-// let container_style = theme.container;
-// let height = theme.height * primary_items_row_count as f32;
-
-// let mut primary_items = Flex::row().with_spacing(spacing);
-// primary_items.extend(primary_left_items);
-// primary_items.extend(primary_right_items);
-
-// let mut toolbar = Flex::column();
-// if !primary_items.is_empty() {
-// toolbar.add_child(primary_items.constrained().with_height(height));
-// }
-// if let Some(secondary_item) = secondary_item {
-// toolbar.add_child(secondary_item);
-// }
-
-// if toolbar.is_empty() {
-// toolbar.into_any_named("toolbar")
-// } else {
-// toolbar
-// .contained()
-// .with_style(container_style)
-// .into_any_named("toolbar")
-// }
-// }
-// }
-
-impl Toolbar {
- pub fn new() -> Self {
- Self {
- active_item: None,
- items: Default::default(),
- hidden: false,
- can_navigate: true,
- }
- }
-
- pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
- self.can_navigate = can_navigate;
- cx.notify();
- }
-
- pub fn add_item<T>(&mut self, item: View<T>, cx: &mut ViewContext<Self>)
- where
- T: 'static + ToolbarItemView,
- {
- let location = item.set_active_pane_item(self.active_item.as_deref(), cx);
- cx.subscribe(&item, |this, item, event, cx| {
- if let Some((_, current_location)) =
- this.items.iter_mut().find(|(i, _)| i.id() == item.id())
- {
- match event {
- ToolbarItemEvent::ChangeLocation(new_location) => {
- if new_location != current_location {
- *current_location = *new_location;
- cx.notify();
- }
- }
- }
- }
- })
- .detach();
- self.items.push((Box::new(item), location));
- cx.notify();
- }
-
- pub fn set_active_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
- self.active_item = item.map(|item| item.boxed_clone());
- self.hidden = self
- .active_item
- .as_ref()
- .map(|item| !item.show_toolbar(cx))
- .unwrap_or(false);
-
- for (toolbar_item, current_location) in self.items.iter_mut() {
- let new_location = toolbar_item.set_active_pane_item(item, cx);
- if new_location != *current_location {
- *current_location = new_location;
- cx.notify();
- }
- }
- }
-
- pub fn focus_changed(&mut self, focused: bool, cx: &mut ViewContext<Self>) {
- for (toolbar_item, _) in self.items.iter_mut() {
- toolbar_item.focus_changed(focused, cx);
- }
- }
-
- pub fn item_of_type<T: ToolbarItemView>(&self) -> Option<View<T>> {
- self.items
- .iter()
- .find_map(|(item, _)| item.to_any().downcast().ok())
- }
-
- pub fn hidden(&self) -> bool {
- self.hidden
- }
-}
-
-impl<T: ToolbarItemView> ToolbarItemViewHandle for View<T> {
- fn id(&self) -> EntityId {
- self.entity_id()
- }
-
- fn to_any(&self) -> AnyView {
- self.clone().into()
- }
-
- fn set_active_pane_item(
- &self,
- active_pane_item: Option<&dyn ItemHandle>,
- cx: &mut WindowContext,
- ) -> ToolbarItemLocation {
- self.update(cx, |this, cx| {
- this.set_active_pane_item(active_pane_item, cx)
- })
- }
-
- fn focus_changed(&mut self, pane_focused: bool, cx: &mut WindowContext) {
- self.update(cx, |this, cx| {
- this.pane_focus_update(pane_focused, cx);
- cx.notify();
- });
- }
-
- fn row_count(&self, cx: &WindowContext) -> usize {
- self.read(cx).row_count(cx)
- }
-}
-
-// todo!()
-// impl From<&dyn ToolbarItemViewHandle> for AnyViewHandle {
-// fn from(val: &dyn ToolbarItemViewHandle) -> Self {
-// val.as_any().clone()
-// }
-// }
@@ -1,5277 +0,0 @@
-pub mod dock;
-pub mod item;
-mod modal_layer;
-pub mod notifications;
-pub mod pane;
-pub mod pane_group;
-mod persistence;
-pub mod searchable;
-pub mod shared_screen;
-mod status_bar;
-mod toolbar;
-mod workspace_settings;
-
-use anyhow::{anyhow, Context as _, Result};
-use call::ActiveCall;
-use client::{
- proto::{self, PeerId},
- Client, Status, TelemetrySettings, TypedEnvelope, UserStore,
-};
-use collections::{hash_map, HashMap, HashSet};
-use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
-use futures::{
- channel::{mpsc, oneshot},
- future::try_join_all,
- Future, FutureExt, StreamExt,
-};
-use gpui::{
- actions, canvas, div, impl_actions, point, size, Action, AnyElement, AnyModel, AnyView,
- AnyWeakView, AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, BorrowWindow,
- Bounds, Context, Div, DragMoveEvent, Element, Entity, EntityId, EventEmitter, FocusHandle,
- FocusableView, GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId,
- ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel,
- Render, Size, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
- WindowBounds, WindowContext, WindowHandle, WindowOptions,
-};
-use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
-use itertools::Itertools;
-use language::{LanguageRegistry, Rope};
-use lazy_static::lazy_static;
-pub use modal_layer::*;
-use node_runtime::NodeRuntime;
-use notifications::{simple_message_notification::MessageNotification, NotificationHandle};
-pub use pane::*;
-pub use pane_group::*;
-use persistence::DB;
-pub use persistence::{
- model::{ItemId, SerializedWorkspace, WorkspaceLocation},
- WorkspaceDb, DB as WORKSPACE_DB,
-};
-use postage::stream::Stream;
-use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
-use serde::Deserialize;
-use settings::Settings;
-use shared_screen::SharedScreen;
-use status_bar::StatusBar;
-pub use status_bar::StatusItemView;
-use std::{
- any::TypeId,
- borrow::Cow,
- cmp, env,
- path::{Path, PathBuf},
- sync::{atomic::AtomicUsize, Arc},
- time::Duration,
-};
-use theme::{ActiveTheme, ThemeSettings};
-pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
-pub use ui;
-use ui::Label;
-use util::ResultExt;
-use uuid::Uuid;
-pub use workspace_settings::{AutosaveSetting, WorkspaceSettings};
-
-use crate::persistence::model::{
- DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup,
-};
-
-lazy_static! {
- static ref ZED_WINDOW_SIZE: Option<Size<GlobalPixels>> = env::var("ZED_WINDOW_SIZE")
- .ok()
- .as_deref()
- .and_then(parse_pixel_size_env_var);
- static ref ZED_WINDOW_POSITION: Option<Point<GlobalPixels>> = env::var("ZED_WINDOW_POSITION")
- .ok()
- .as_deref()
- .and_then(parse_pixel_position_env_var);
-}
-
-#[derive(Clone, PartialEq)]
-pub struct RemoveWorktreeFromProject(pub WorktreeId);
-
-actions!(
- workspace,
- [
- Open,
- NewFile,
- NewWindow,
- CloseWindow,
- CloseInactiveTabsAndPanes,
- AddFolderToProject,
- Unfollow,
- SaveAs,
- ReloadActiveItem,
- ActivatePreviousPane,
- ActivateNextPane,
- FollowNextCollaborator,
- NewTerminal,
- NewCenterTerminal,
- ToggleTerminalFocus,
- NewSearch,
- Feedback,
- Restart,
- Welcome,
- ToggleZoom,
- ToggleLeftDock,
- ToggleRightDock,
- ToggleBottomDock,
- CloseAllDocks,
- ]
-);
-
-#[derive(Clone, PartialEq)]
-pub struct OpenPaths {
- pub paths: Vec<PathBuf>,
-}
-
-#[derive(Clone, Deserialize, PartialEq)]
-pub struct ActivatePane(pub usize);
-
-#[derive(Clone, Deserialize, PartialEq)]
-pub struct ActivatePaneInDirection(pub SplitDirection);
-
-#[derive(Clone, Deserialize, PartialEq)]
-pub struct SwapPaneInDirection(pub SplitDirection);
-
-#[derive(Clone, Deserialize, PartialEq)]
-pub struct NewFileInDirection(pub SplitDirection);
-
-#[derive(Clone, PartialEq, Debug, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct SaveAll {
- pub save_intent: Option<SaveIntent>,
-}
-
-#[derive(Clone, PartialEq, Debug, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct Save {
- pub save_intent: Option<SaveIntent>,
-}
-
-#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
-#[serde(rename_all = "camelCase")]
-pub struct CloseAllItemsAndPanes {
- pub save_intent: Option<SaveIntent>,
-}
-
-impl_actions!(
- workspace,
- [
- ActivatePane,
- ActivatePaneInDirection,
- CloseAllItemsAndPanes,
- NewFileInDirection,
- OpenTerminal,
- Save,
- SaveAll,
- SwapPaneInDirection,
- ]
-);
-
-#[derive(Deserialize)]
-pub struct Toast {
- id: usize,
- msg: Cow<'static, str>,
- #[serde(skip)]
- on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
-}
-
-impl Toast {
- pub fn new<I: Into<Cow<'static, str>>>(id: usize, msg: I) -> Self {
- Toast {
- id,
- msg: msg.into(),
- on_click: None,
- }
- }
-
- pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
- where
- M: Into<Cow<'static, str>>,
- F: Fn(&mut WindowContext) + 'static,
- {
- self.on_click = Some((message.into(), Arc::new(on_click)));
- self
- }
-}
-
-impl PartialEq for Toast {
- fn eq(&self, other: &Self) -> bool {
- self.id == other.id
- && self.msg == other.msg
- && self.on_click.is_some() == other.on_click.is_some()
- }
-}
-
-impl Clone for Toast {
- fn clone(&self) -> Self {
- Toast {
- id: self.id,
- msg: self.msg.to_owned(),
- on_click: self.on_click.clone(),
- }
- }
-}
-
-#[derive(Debug, Default, Clone, Deserialize, PartialEq)]
-pub struct OpenTerminal {
- pub working_directory: PathBuf,
-}
-
-pub type WorkspaceId = i64;
-
-pub fn init_settings(cx: &mut AppContext) {
- WorkspaceSettings::register(cx);
- ItemSettings::register(cx);
-}
-
-pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
- init_settings(cx);
- notifications::init(cx);
-
- cx.on_action(Workspace::close_global);
- cx.on_action(restart);
-
- cx.on_action({
- let app_state = Arc::downgrade(&app_state);
- move |_: &Open, cx: &mut AppContext| {
- let paths = cx.prompt_for_paths(PathPromptOptions {
- files: true,
- directories: true,
- multiple: true,
- });
-
- if let Some(app_state) = app_state.upgrade() {
- cx.spawn(move |cx| async move {
- if let Some(paths) = paths.await.log_err().flatten() {
- cx.update(|cx| {
- open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx)
- })
- .ok();
- }
- })
- .detach();
- }
- }
- });
-}
-
-type ProjectItemBuilders =
- HashMap<TypeId, fn(Model<Project>, AnyModel, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>>;
-pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
- let builders = cx.default_global::<ProjectItemBuilders>();
- builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
- let item = model.downcast::<I::Item>().unwrap();
- Box::new(cx.new_view(|cx| I::for_project_item(project, item, cx)))
- });
-}
-
-type FollowableItemBuilder = fn(
- View<Pane>,
- View<Workspace>,
- ViewId,
- &mut Option<proto::view::Variant>,
- &mut WindowContext,
-) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
-type FollowableItemBuilders = HashMap<
- TypeId,
- (
- FollowableItemBuilder,
- fn(&AnyView) -> Box<dyn FollowableItemHandle>,
- ),
->;
-pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
- let builders = cx.default_global::<FollowableItemBuilders>();
- builders.insert(
- TypeId::of::<I>(),
- (
- |pane, workspace, id, state, cx| {
- I::from_state_proto(pane, workspace, id, state, cx).map(|task| {
- cx.foreground_executor()
- .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
- })
- },
- |this| Box::new(this.clone().downcast::<I>().unwrap()),
- ),
- );
-}
-
-type ItemDeserializers = HashMap<
- Arc<str>,
- fn(
- Model<Project>,
- WeakView<Workspace>,
- WorkspaceId,
- ItemId,
- &mut ViewContext<Pane>,
- ) -> Task<Result<Box<dyn ItemHandle>>>,
->;
-pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
- if let Some(serialized_item_kind) = I::serialized_item_kind() {
- let deserializers = cx.default_global::<ItemDeserializers>();
- deserializers.insert(
- Arc::from(serialized_item_kind),
- |project, workspace, workspace_id, item_id, cx| {
- let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
- cx.foreground_executor()
- .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
- },
- );
- }
-}
-
-pub struct AppState {
- pub languages: Arc<LanguageRegistry>,
- pub client: Arc<Client>,
- pub user_store: Model<UserStore>,
- pub workspace_store: Model<WorkspaceStore>,
- pub fs: Arc<dyn fs::Fs>,
- pub build_window_options:
- fn(Option<WindowBounds>, Option<Uuid>, &mut AppContext) -> WindowOptions,
- pub node_runtime: Arc<dyn NodeRuntime>,
-}
-
-pub struct WorkspaceStore {
- workspaces: HashSet<WindowHandle<Workspace>>,
- followers: Vec<Follower>,
- client: Arc<Client>,
- _subscriptions: Vec<client::Subscription>,
-}
-
-#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
-struct Follower {
- project_id: Option<u64>,
- peer_id: PeerId,
-}
-
-impl AppState {
- #[cfg(any(test, feature = "test-support"))]
- pub fn test(cx: &mut AppContext) -> Arc<Self> {
- use node_runtime::FakeNodeRuntime;
- use settings::SettingsStore;
-
- if !cx.has_global::<SettingsStore>() {
- let settings_store = SettingsStore::test(cx);
- cx.set_global(settings_store);
- }
-
- let fs = fs::FakeFs::new(cx.background_executor().clone());
- let languages = Arc::new(LanguageRegistry::test());
- let http_client = util::http::FakeHttpClient::with_404_response();
- let client = Client::new(http_client.clone(), cx);
- let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
- let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
-
- theme::init(theme::LoadThemes::JustBase, cx);
- client::init(&client, cx);
- crate::init_settings(cx);
-
- Arc::new(Self {
- client,
- fs,
- languages,
- user_store,
- workspace_store,
- node_runtime: FakeNodeRuntime::new(),
- build_window_options: |_, _, _| Default::default(),
- })
- }
-}
-
-struct DelayedDebouncedEditAction {
- task: Option<Task<()>>,
- cancel_channel: Option<oneshot::Sender<()>>,
-}
-
-impl DelayedDebouncedEditAction {
- fn new() -> DelayedDebouncedEditAction {
- DelayedDebouncedEditAction {
- task: None,
- cancel_channel: None,
- }
- }
-
- fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Workspace>, func: F)
- where
- F: 'static + Send + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> Task<Result<()>>,
- {
- if let Some(channel) = self.cancel_channel.take() {
- _ = channel.send(());
- }
-
- let (sender, mut receiver) = oneshot::channel::<()>();
- self.cancel_channel = Some(sender);
-
- let previous_task = self.task.take();
- self.task = Some(cx.spawn(move |workspace, mut cx| async move {
- let mut timer = cx.background_executor().timer(delay).fuse();
- if let Some(previous_task) = previous_task {
- previous_task.await;
- }
-
- futures::select_biased! {
- _ = receiver => return,
- _ = timer => {}
- }
-
- if let Some(result) = workspace
- .update(&mut cx, |workspace, cx| (func)(workspace, cx))
- .log_err()
- {
- result.await.log_err();
- }
- }));
- }
-}
-
-pub enum Event {
- PaneAdded(View<Pane>),
- ContactRequestedJoin(u64),
- WorkspaceCreated(WeakView<Workspace>),
-}
-
-pub struct Workspace {
- weak_self: WeakView<Self>,
- workspace_actions: Vec<Box<dyn Fn(Div, &mut ViewContext<Self>) -> Div>>,
- zoomed: Option<AnyWeakView>,
- zoomed_position: Option<DockPosition>,
- center: PaneGroup,
- left_dock: View<Dock>,
- bottom_dock: View<Dock>,
- right_dock: View<Dock>,
- panes: Vec<View<Pane>>,
- panes_by_item: HashMap<EntityId, WeakView<Pane>>,
- active_pane: View<Pane>,
- last_active_center_pane: Option<WeakView<Pane>>,
- last_active_view_id: Option<proto::ViewId>,
- status_bar: View<StatusBar>,
- modal_layer: View<ModalLayer>,
- titlebar_item: Option<AnyView>,
- notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
- project: Model<Project>,
- follower_states: HashMap<View<Pane>, FollowerState>,
- last_leaders_by_pane: HashMap<WeakView<Pane>, PeerId>,
- window_edited: bool,
- active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
- leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
- database_id: WorkspaceId,
- app_state: Arc<AppState>,
- _subscriptions: Vec<Subscription>,
- _apply_leader_updates: Task<Result<()>>,
- _observe_current_user: Task<Result<()>>,
- _schedule_serialize: Option<Task<()>>,
- pane_history_timestamp: Arc<AtomicUsize>,
- bounds: Bounds<Pixels>,
-}
-
-impl EventEmitter<Event> for Workspace {}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
-pub struct ViewId {
- pub creator: PeerId,
- pub id: u64,
-}
-
-#[derive(Default)]
-struct FollowerState {
- leader_id: PeerId,
- active_view_id: Option<ViewId>,
- items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
-}
-
-impl Workspace {
- pub fn new(
- workspace_id: WorkspaceId,
- project: Model<Project>,
- app_state: Arc<AppState>,
- cx: &mut ViewContext<Self>,
- ) -> Self {
- cx.observe(&project, |_, _, cx| cx.notify()).detach();
- cx.subscribe(&project, move |this, _, event, cx| {
- match event {
- project::Event::RemoteIdChanged(_) => {
- this.update_window_title(cx);
- }
-
- project::Event::CollaboratorLeft(peer_id) => {
- this.collaborator_left(*peer_id, cx);
- }
-
- project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
- this.update_window_title(cx);
- this.serialize_workspace(cx);
- }
-
- project::Event::DisconnectedFromHost => {
- this.update_window_edited(cx);
- cx.disable_focus();
- }
-
- project::Event::Closed => {
- cx.remove_window();
- }
-
- project::Event::DeletedEntry(entry_id) => {
- for pane in this.panes.iter() {
- pane.update(cx, |pane, cx| {
- pane.handle_deleted_project_item(*entry_id, cx)
- });
- }
- }
-
- project::Event::Notification(message) => this.show_notification(0, cx, |cx| {
- cx.new_view(|_| MessageNotification::new(message.clone()))
- }),
-
- _ => {}
- }
- cx.notify()
- })
- .detach();
-
- cx.on_blur_window(|this, cx| {
- let focus_handle = this.focus_handle(cx);
- cx.focus(&focus_handle);
- })
- .detach();
-
- let weak_handle = cx.view().downgrade();
- let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
-
- let center_pane = cx.new_view(|cx| {
- Pane::new(
- weak_handle.clone(),
- project.clone(),
- pane_history_timestamp.clone(),
- None,
- cx,
- )
- });
- cx.subscribe(¢er_pane, Self::handle_pane_event).detach();
-
- cx.focus_view(¢er_pane);
- cx.emit(Event::PaneAdded(center_pane.clone()));
-
- let window_handle = cx.window_handle().downcast::<Workspace>().unwrap();
- app_state.workspace_store.update(cx, |store, _| {
- store.workspaces.insert(window_handle);
- });
-
- let mut current_user = app_state.user_store.read(cx).watch_current_user();
- let mut connection_status = app_state.client.status();
- let _observe_current_user = cx.spawn(|this, mut cx| async move {
- current_user.next().await;
- connection_status.next().await;
- let mut stream =
- Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
-
- while stream.recv().await.is_some() {
- this.update(&mut cx, |_, cx| cx.notify())?;
- }
- anyhow::Ok(())
- });
-
- // All leader updates are enqueued and then processed in a single task, so
- // that each asynchronous operation can be run in order.
- let (leader_updates_tx, mut leader_updates_rx) =
- mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
- let _apply_leader_updates = cx.spawn(|this, mut cx| async move {
- while let Some((leader_id, update)) = leader_updates_rx.next().await {
- Self::process_leader_update(&this, leader_id, update, &mut cx)
- .await
- .log_err();
- }
-
- Ok(())
- });
-
- cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
-
- let left_dock = Dock::new(DockPosition::Left, cx);
- let bottom_dock = Dock::new(DockPosition::Bottom, cx);
- let right_dock = Dock::new(DockPosition::Right, cx);
- let left_dock_buttons = cx.new_view(|cx| PanelButtons::new(left_dock.clone(), cx));
- let bottom_dock_buttons = cx.new_view(|cx| PanelButtons::new(bottom_dock.clone(), cx));
- let right_dock_buttons = cx.new_view(|cx| PanelButtons::new(right_dock.clone(), cx));
- let status_bar = cx.new_view(|cx| {
- let mut status_bar = StatusBar::new(¢er_pane.clone(), cx);
- status_bar.add_left_item(left_dock_buttons, cx);
- status_bar.add_right_item(right_dock_buttons, cx);
- status_bar.add_right_item(bottom_dock_buttons, cx);
- status_bar
- });
-
- let modal_layer = cx.new_view(|_| ModalLayer::new());
-
- let mut active_call = None;
- if cx.has_global::<Model<ActiveCall>>() {
- let call = cx.global::<Model<ActiveCall>>().clone();
- let mut subscriptions = Vec::new();
- subscriptions.push(cx.subscribe(&call, Self::on_active_call_event));
- active_call = Some((call, subscriptions));
- }
-
- let subscriptions = vec![
- cx.observe_window_activation(Self::on_window_activation_changed),
- cx.observe_window_bounds(move |_, cx| {
- if let Some(display) = cx.display() {
- // Transform fixed bounds to be stored in terms of the containing display
- let mut bounds = cx.window_bounds();
- if let WindowBounds::Fixed(window_bounds) = &mut bounds {
- let display_bounds = display.bounds();
- window_bounds.origin.x -= display_bounds.origin.x;
- window_bounds.origin.y -= display_bounds.origin.y;
- }
-
- if let Some(display_uuid) = display.uuid().log_err() {
- cx.background_executor()
- .spawn(DB.set_window_bounds(workspace_id, bounds, display_uuid))
- .detach_and_log_err(cx);
- }
- }
- cx.notify();
- }),
- cx.observe(&left_dock, |this, _, cx| {
- this.serialize_workspace(cx);
- cx.notify();
- }),
- cx.observe(&bottom_dock, |this, _, cx| {
- this.serialize_workspace(cx);
- cx.notify();
- }),
- cx.observe(&right_dock, |this, _, cx| {
- this.serialize_workspace(cx);
- cx.notify();
- }),
- cx.on_release(|this, window, cx| {
- this.app_state.workspace_store.update(cx, |store, _| {
- let window = window.downcast::<Self>().unwrap();
- debug_assert!(store.workspaces.remove(&window));
- })
- }),
- ];
-
- cx.defer(|this, cx| {
- this.update_window_title(cx);
- // todo! @nate - these are useful for testing notifications
- // this.show_error(
- // &anyhow::anyhow!("what happens if this message is very very very very very long"),
- // cx,
- // );
-
- // this.show_notification(1, cx, |cx| {
- // cx.build_view(|_cx| {
- // simple_message_notification::MessageNotification::new(format!("Error:"))
- // .with_click_message("click here because!")
- // })
- // });
- });
- Workspace {
- weak_self: weak_handle.clone(),
- zoomed: None,
- zoomed_position: None,
- center: PaneGroup::new(center_pane.clone()),
- panes: vec![center_pane.clone()],
- panes_by_item: Default::default(),
- active_pane: center_pane.clone(),
- last_active_center_pane: Some(center_pane.downgrade()),
- last_active_view_id: None,
- status_bar,
- modal_layer,
- titlebar_item: None,
- notifications: Default::default(),
- left_dock,
- bottom_dock,
- right_dock,
- project: project.clone(),
- follower_states: Default::default(),
- last_leaders_by_pane: Default::default(),
- window_edited: false,
- active_call,
- database_id: workspace_id,
- app_state,
- _observe_current_user,
- _apply_leader_updates,
- _schedule_serialize: None,
- leader_updates_tx,
- _subscriptions: subscriptions,
- pane_history_timestamp,
- workspace_actions: Default::default(),
- // This data will be incorrect, but it will be overwritten by the time it needs to be used.
- bounds: Default::default(),
- }
- }
-
- fn new_local(
- abs_paths: Vec<PathBuf>,
- app_state: Arc<AppState>,
- requesting_window: Option<WindowHandle<Workspace>>,
- cx: &mut AppContext,
- ) -> Task<
- anyhow::Result<(
- WindowHandle<Workspace>,
- Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
- )>,
- > {
- let project_handle = Project::local(
- app_state.client.clone(),
- app_state.node_runtime.clone(),
- app_state.user_store.clone(),
- app_state.languages.clone(),
- app_state.fs.clone(),
- cx,
- );
-
- cx.spawn(|mut cx| async move {
- let serialized_workspace: Option<SerializedWorkspace> =
- persistence::DB.workspace_for_roots(&abs_paths.as_slice());
-
- let paths_to_open = Arc::new(abs_paths);
-
- // Get project paths for all of the abs_paths
- let mut worktree_roots: HashSet<Arc<Path>> = Default::default();
- let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
- Vec::with_capacity(paths_to_open.len());
- for path in paths_to_open.iter().cloned() {
- if let Some((worktree, project_entry)) = cx
- .update(|cx| {
- Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
- })?
- .await
- .log_err()
- {
- worktree_roots.extend(worktree.update(&mut cx, |tree, _| tree.abs_path()).ok());
- project_paths.push((path, Some(project_entry)));
- } else {
- project_paths.push((path, None));
- }
- }
-
- let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
- serialized_workspace.id
- } else {
- DB.next_id().await.unwrap_or(0)
- };
-
- let window = if let Some(window) = requesting_window {
- cx.update_window(window.into(), |_, cx| {
- cx.replace_root_view(|cx| {
- Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
- });
- })?;
- window
- } else {
- let window_bounds_override = window_bounds_env_override(&cx);
- let (bounds, display) = if let Some(bounds) = window_bounds_override {
- (Some(bounds), None)
- } else {
- serialized_workspace
- .as_ref()
- .and_then(|serialized_workspace| {
- let serialized_display = serialized_workspace.display?;
- let mut bounds = serialized_workspace.bounds?;
-
- // Stored bounds are relative to the containing display.
- // So convert back to global coordinates if that screen still exists
- if let WindowBounds::Fixed(mut window_bounds) = bounds {
- let screen = cx
- .update(|cx| {
- cx.displays().into_iter().find(|display| {
- display.uuid().ok() == Some(serialized_display)
- })
- })
- .ok()??;
- let screen_bounds = screen.bounds();
- window_bounds.origin.x += screen_bounds.origin.x;
- window_bounds.origin.y += screen_bounds.origin.y;
- bounds = WindowBounds::Fixed(window_bounds);
- }
-
- Some((bounds, serialized_display))
- })
- .unzip()
- };
-
- // Use the serialized workspace to construct the new window
- let options =
- cx.update(|cx| (app_state.build_window_options)(bounds, display, cx))?;
-
- cx.open_window(options, {
- let app_state = app_state.clone();
- let workspace_id = workspace_id.clone();
- let project_handle = project_handle.clone();
- move |cx| {
- cx.new_view(|cx| {
- Workspace::new(workspace_id, project_handle, app_state, cx)
- })
- }
- })?
- };
-
- window
- .update(&mut cx, |_, cx| cx.activate_window())
- .log_err();
-
- notify_if_database_failed(window, &mut cx);
- let opened_items = window
- .update(&mut cx, |_workspace, cx| {
- open_items(serialized_workspace, project_paths, app_state, cx)
- })?
- .await
- .unwrap_or_default();
-
- Ok((window, opened_items))
- })
- }
-
- pub fn weak_handle(&self) -> WeakView<Self> {
- self.weak_self.clone()
- }
-
- pub fn left_dock(&self) -> &View<Dock> {
- &self.left_dock
- }
-
- pub fn bottom_dock(&self) -> &View<Dock> {
- &self.bottom_dock
- }
-
- pub fn right_dock(&self) -> &View<Dock> {
- &self.right_dock
- }
-
- pub fn add_panel<T: Panel>(&mut self, panel: View<T>, cx: &mut ViewContext<Self>) {
- let dock = match panel.position(cx) {
- DockPosition::Left => &self.left_dock,
- DockPosition::Bottom => &self.bottom_dock,
- DockPosition::Right => &self.right_dock,
- };
-
- dock.update(cx, |dock, cx| {
- dock.add_panel(panel, self.weak_self.clone(), cx)
- });
- }
-
- pub fn status_bar(&self) -> &View<StatusBar> {
- &self.status_bar
- }
-
- pub fn app_state(&self) -> &Arc<AppState> {
- &self.app_state
- }
-
- pub fn user_store(&self) -> &Model<UserStore> {
- &self.app_state.user_store
- }
-
- pub fn project(&self) -> &Model<Project> {
- &self.project
- }
-
- pub fn recent_navigation_history(
- &self,
- limit: Option<usize>,
- cx: &AppContext,
- ) -> Vec<(ProjectPath, Option<PathBuf>)> {
- let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
- let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
- for pane in &self.panes {
- let pane = pane.read(cx);
- pane.nav_history()
- .for_each_entry(cx, |entry, (project_path, fs_path)| {
- if let Some(fs_path) = &fs_path {
- abs_paths_opened
- .entry(fs_path.clone())
- .or_default()
- .insert(project_path.clone());
- }
- let timestamp = entry.timestamp;
- match history.entry(project_path) {
- hash_map::Entry::Occupied(mut entry) => {
- let (_, old_timestamp) = entry.get();
- if ×tamp > old_timestamp {
- entry.insert((fs_path, timestamp));
- }
- }
- hash_map::Entry::Vacant(entry) => {
- entry.insert((fs_path, timestamp));
- }
- }
- });
- }
-
- history
- .into_iter()
- .sorted_by_key(|(_, (_, timestamp))| *timestamp)
- .map(|(project_path, (fs_path, _))| (project_path, fs_path))
- .rev()
- .filter(|(history_path, abs_path)| {
- let latest_project_path_opened = abs_path
- .as_ref()
- .and_then(|abs_path| abs_paths_opened.get(abs_path))
- .and_then(|project_paths| {
- project_paths
- .iter()
- .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
- });
-
- match latest_project_path_opened {
- Some(latest_project_path_opened) => latest_project_path_opened == history_path,
- None => true,
- }
- })
- .take(limit.unwrap_or(usize::MAX))
- .collect()
- }
-
- fn navigate_history(
- &mut self,
- pane: WeakView<Pane>,
- mode: NavigationMode,
- cx: &mut ViewContext<Workspace>,
- ) -> Task<Result<()>> {
- let to_load = if let Some(pane) = pane.upgrade() {
- // todo!("focus")
- // cx.focus(&pane);
-
- pane.update(cx, |pane, cx| {
- loop {
- // Retrieve the weak item handle from the history.
- let entry = pane.nav_history_mut().pop(mode, cx)?;
-
- // If the item is still present in this pane, then activate it.
- if let Some(index) = entry
- .item
- .upgrade()
- .and_then(|v| pane.index_for_item(v.as_ref()))
- {
- let prev_active_item_index = pane.active_item_index();
- pane.nav_history_mut().set_mode(mode);
- pane.activate_item(index, true, true, cx);
- pane.nav_history_mut().set_mode(NavigationMode::Normal);
-
- let mut navigated = prev_active_item_index != pane.active_item_index();
- if let Some(data) = entry.data {
- navigated |= pane.active_item()?.navigate(data, cx);
- }
-
- if navigated {
- break None;
- }
- }
- // If the item is no longer present in this pane, then retrieve its
- // project path in order to reopen it.
- else {
- break pane
- .nav_history()
- .path_for_item(entry.item.id())
- .map(|(project_path, _)| (project_path, entry));
- }
- }
- })
- } else {
- None
- };
-
- if let Some((project_path, entry)) = to_load {
- // If the item was no longer present, then load it again from its previous path.
- let task = self.load_path(project_path, cx);
- cx.spawn(|workspace, mut cx| async move {
- let task = task.await;
- let mut navigated = false;
- if let Some((project_entry_id, build_item)) = task.log_err() {
- let prev_active_item_id = pane.update(&mut cx, |pane, _| {
- pane.nav_history_mut().set_mode(mode);
- pane.active_item().map(|p| p.item_id())
- })?;
-
- pane.update(&mut cx, |pane, cx| {
- let item = pane.open_item(project_entry_id, true, cx, build_item);
- navigated |= Some(item.item_id()) != prev_active_item_id;
- pane.nav_history_mut().set_mode(NavigationMode::Normal);
- if let Some(data) = entry.data {
- navigated |= item.navigate(data, cx);
- }
- })?;
- }
-
- if !navigated {
- workspace
- .update(&mut cx, |workspace, cx| {
- Self::navigate_history(workspace, pane, mode, cx)
- })?
- .await?;
- }
-
- Ok(())
- })
- } else {
- Task::ready(Ok(()))
- }
- }
-
- pub fn go_back(
- &mut self,
- pane: WeakView<Pane>,
- cx: &mut ViewContext<Workspace>,
- ) -> Task<Result<()>> {
- self.navigate_history(pane, NavigationMode::GoingBack, cx)
- }
-
- pub fn go_forward(
- &mut self,
- pane: WeakView<Pane>,
- cx: &mut ViewContext<Workspace>,
- ) -> Task<Result<()>> {
- self.navigate_history(pane, NavigationMode::GoingForward, cx)
- }
-
- pub fn reopen_closed_item(&mut self, cx: &mut ViewContext<Workspace>) -> Task<Result<()>> {
- self.navigate_history(
- self.active_pane().downgrade(),
- NavigationMode::ReopeningClosedItem,
- cx,
- )
- }
-
- pub fn client(&self) -> &Client {
- &self.app_state.client
- }
-
- pub fn set_titlebar_item(&mut self, item: AnyView, cx: &mut ViewContext<Self>) {
- self.titlebar_item = Some(item);
- cx.notify();
- }
-
- pub fn titlebar_item(&self) -> Option<AnyView> {
- self.titlebar_item.clone()
- }
-
- /// Call the given callback with a workspace whose project is local.
- ///
- /// If the given workspace has a local project, then it will be passed
- /// to the callback. Otherwise, a new empty window will be created.
- pub fn with_local_workspace<T, F>(
- &mut self,
- cx: &mut ViewContext<Self>,
- callback: F,
- ) -> Task<Result<T>>
- where
- T: 'static,
- F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
- {
- if self.project.read(cx).is_local() {
- Task::Ready(Some(Ok(callback(self, cx))))
- } else {
- let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx);
- cx.spawn(|_vh, mut cx| async move {
- let (workspace, _) = task.await?;
- workspace.update(&mut cx, callback)
- })
- }
- }
-
- pub fn worktrees<'a>(&self, cx: &'a AppContext) -> impl 'a + Iterator<Item = Model<Worktree>> {
- self.project.read(cx).worktrees()
- }
-
- pub fn visible_worktrees<'a>(
- &self,
- cx: &'a AppContext,
- ) -> impl 'a + Iterator<Item = Model<Worktree>> {
- self.project.read(cx).visible_worktrees(cx)
- }
-
- pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
- let futures = self
- .worktrees(cx)
- .filter_map(|worktree| worktree.read(cx).as_local())
- .map(|worktree| worktree.scan_complete())
- .collect::<Vec<_>>();
- async move {
- for future in futures {
- future.await;
- }
- }
- }
-
- pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
- cx.windows().iter().find(|window| {
- window
- .update(cx, |_, window| {
- if window.is_window_active() {
- //This can only get called when the window's project connection has been lost
- //so we don't need to prompt the user for anything and instead just close the window
- window.remove_window();
- true
- } else {
- false
- }
- })
- .unwrap_or(false)
- });
- }
-
- pub fn close_window(&mut self, _: &CloseWindow, cx: &mut ViewContext<Self>) {
- let window = cx.window_handle();
- let prepare = self.prepare_to_close(false, cx);
- cx.spawn(|_, mut cx| async move {
- if prepare.await? {
- window.update(&mut cx, |_, cx| {
- cx.remove_window();
- })?;
- }
- anyhow::Ok(())
- })
- .detach_and_log_err(cx)
- }
-
- pub fn prepare_to_close(
- &mut self,
- quitting: bool,
- cx: &mut ViewContext<Self>,
- ) -> Task<Result<bool>> {
- //todo!(saveing)
- let active_call = self.active_call().cloned();
- let window = cx.window_handle();
-
- cx.spawn(|this, mut cx| async move {
- let workspace_count = (*cx).update(|cx| {
- cx.windows()
- .iter()
- .filter(|window| window.downcast::<Workspace>().is_some())
- .count()
- })?;
-
- if let Some(active_call) = active_call {
- if !quitting
- && workspace_count == 1
- && active_call.read_with(&cx, |call, _| call.room().is_some())?
- {
- let answer = window.update(&mut cx, |_, cx| {
- cx.prompt(
- PromptLevel::Warning,
- "Do you want to leave the current call?",
- &["Close window and hang up", "Cancel"],
- )
- })?;
-
- if answer.await.log_err() == Some(1) {
- return anyhow::Ok(false);
- } else {
- active_call
- .update(&mut cx, |call, cx| call.hang_up(cx))?
- .await
- .log_err();
- }
- }
- }
-
- Ok(this
- .update(&mut cx, |this, cx| {
- this.save_all_internal(SaveIntent::Close, cx)
- })?
- .await?)
- })
- }
-
- fn save_all(&mut self, action: &SaveAll, cx: &mut ViewContext<Self>) {
- self.save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx)
- .detach_and_log_err(cx);
- }
-
- fn save_all_internal(
- &mut self,
- mut save_intent: SaveIntent,
- cx: &mut ViewContext<Self>,
- ) -> Task<Result<bool>> {
- if self.project.read(cx).is_read_only() {
- return Task::ready(Ok(true));
- }
- let dirty_items = self
- .panes
- .iter()
- .flat_map(|pane| {
- pane.read(cx).items().filter_map(|item| {
- if item.is_dirty(cx) {
- Some((pane.downgrade(), item.boxed_clone()))
- } else {
- None
- }
- })
- })
- .collect::<Vec<_>>();
-
- let project = self.project.clone();
- cx.spawn(|workspace, mut cx| async move {
- // Override save mode and display "Save all files" prompt
- if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
- let answer = workspace.update(&mut cx, |_, cx| {
- let prompt = Pane::file_names_for_prompt(
- &mut dirty_items.iter().map(|(_, handle)| handle),
- dirty_items.len(),
- cx,
- );
- cx.prompt(
- PromptLevel::Warning,
- &prompt,
- &["Save all", "Discard all", "Cancel"],
- )
- })?;
- match answer.await.log_err() {
- Some(0) => save_intent = SaveIntent::SaveAll,
- Some(1) => save_intent = SaveIntent::Skip,
- _ => {}
- }
- }
- for (pane, item) in dirty_items {
- let (singleton, project_entry_ids) =
- cx.update(|_, cx| (item.is_singleton(cx), item.project_entry_ids(cx)))?;
- if singleton || !project_entry_ids.is_empty() {
- if let Some(ix) =
- pane.update(&mut cx, |pane, _| pane.index_for_item(item.as_ref()))?
- {
- if !Pane::save_item(
- project.clone(),
- &pane,
- ix,
- &*item,
- save_intent,
- &mut cx,
- )
- .await?
- {
- return Ok(false);
- }
- }
- }
- }
- Ok(true)
- })
- }
-
- pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
- let telemetry_settings = TelemetrySettings::get_global(cx).clone();
- self.client()
- .telemetry()
- .report_app_event(telemetry_settings, "open project", false);
- let paths = cx.prompt_for_paths(PathPromptOptions {
- files: true,
- directories: true,
- multiple: true,
- });
-
- cx.spawn(|this, mut cx| async move {
- let Some(paths) = paths.await.log_err().flatten() else {
- return;
- };
-
- if let Some(task) = this
- .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx))
- .log_err()
- {
- task.await.log_err();
- }
- })
- .detach()
- }
-
- pub fn open_workspace_for_paths(
- &mut self,
- paths: Vec<PathBuf>,
- cx: &mut ViewContext<Self>,
- ) -> Task<Result<()>> {
- let window = cx.window_handle().downcast::<Self>();
- let is_remote = self.project.read(cx).is_remote();
- let has_worktree = self.project.read(cx).worktrees().next().is_some();
- let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
- let close_task = if is_remote || has_worktree || has_dirty_items {
- None
- } else {
- Some(self.prepare_to_close(false, cx))
- };
- let app_state = self.app_state.clone();
-
- cx.spawn(|_, mut cx| async move {
- let window_to_replace = if let Some(close_task) = close_task {
- if !close_task.await? {
- return Ok(());
- }
- window
- } else {
- None
- };
- cx.update(|_, cx| open_paths(&paths, &app_state, window_to_replace, cx))?
- .await?;
- Ok(())
- })
- }
-
- #[allow(clippy::type_complexity)]
- pub fn open_paths(
- &mut self,
- mut abs_paths: Vec<PathBuf>,
- visible: bool,
- cx: &mut ViewContext<Self>,
- ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
- log::info!("open paths {abs_paths:?}");
-
- let fs = self.app_state.fs.clone();
-
- // Sort the paths to ensure we add worktrees for parents before their children.
- abs_paths.sort_unstable();
- cx.spawn(move |this, mut cx| async move {
- let mut tasks = Vec::with_capacity(abs_paths.len());
- for abs_path in &abs_paths {
- let project_path = match this
- .update(&mut cx, |this, cx| {
- Workspace::project_path_for_path(
- this.project.clone(),
- abs_path,
- visible,
- cx,
- )
- })
- .log_err()
- {
- Some(project_path) => project_path.await.log_err(),
- None => None,
- };
-
- let this = this.clone();
- let abs_path = abs_path.clone();
- let fs = fs.clone();
- let task = cx.spawn(move |mut cx| async move {
- let (worktree, project_path) = project_path?;
- if fs.is_file(&abs_path).await {
- Some(
- this.update(&mut cx, |this, cx| {
- this.open_path(project_path, None, true, cx)
- })
- .log_err()?
- .await,
- )
- } else {
- this.update(&mut cx, |workspace, cx| {
- let worktree = worktree.read(cx);
- let worktree_abs_path = worktree.abs_path();
- let entry_id = if abs_path == worktree_abs_path.as_ref() {
- worktree.root_entry()
- } else {
- abs_path
- .strip_prefix(worktree_abs_path.as_ref())
- .ok()
- .and_then(|relative_path| {
- worktree.entry_for_path(relative_path)
- })
- }
- .map(|entry| entry.id);
- if let Some(entry_id) = entry_id {
- workspace.project.update(cx, |_, cx| {
- cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
- })
- }
- })
- .log_err()?;
- None
- }
- });
- tasks.push(task);
- }
-
- futures::future::join_all(tasks).await
- })
- }
-
- fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
- let paths = cx.prompt_for_paths(PathPromptOptions {
- files: false,
- directories: true,
- multiple: true,
- });
- cx.spawn(|this, mut cx| async move {
- if let Some(paths) = paths.await.log_err().flatten() {
- let results = this
- .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))?
- .await;
- for result in results.into_iter().flatten() {
- result.log_err();
- }
- }
- anyhow::Ok(())
- })
- .detach_and_log_err(cx);
- }
-
- fn project_path_for_path(
- project: Model<Project>,
- abs_path: &Path,
- visible: bool,
- cx: &mut AppContext,
- ) -> Task<Result<(Model<Worktree>, ProjectPath)>> {
- let entry = project.update(cx, |project, cx| {
- project.find_or_create_local_worktree(abs_path, visible, cx)
- });
- cx.spawn(|mut cx| async move {
- let (worktree, path) = entry.await?;
- let worktree_id = worktree.update(&mut cx, |t, _| t.id())?;
- Ok((
- worktree,
- ProjectPath {
- worktree_id,
- path: path.into(),
- },
- ))
- })
- }
-
- pub fn items<'a>(
- &'a self,
- cx: &'a AppContext,
- ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
- self.panes.iter().flat_map(|pane| pane.read(cx).items())
- }
-
- pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<View<T>> {
- self.items_of_type(cx).max_by_key(|item| item.item_id())
- }
-
- pub fn items_of_type<'a, T: Item>(
- &'a self,
- cx: &'a AppContext,
- ) -> impl 'a + Iterator<Item = View<T>> {
- self.panes
- .iter()
- .flat_map(|pane| pane.read(cx).items_of_type())
- }
-
- pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
- self.active_pane().read(cx).active_item()
- }
-
- pub fn active_item_as<I: 'static>(&self, cx: &AppContext) -> Option<View<I>> {
- let item = self.active_item(cx)?;
- item.to_any().downcast::<I>().ok()
- }
-
- fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
- self.active_item(cx).and_then(|item| item.project_path(cx))
- }
-
- pub fn save_active_item(
- &mut self,
- save_intent: SaveIntent,
- cx: &mut ViewContext<Self>,
- ) -> Task<Result<()>> {
- let project = self.project.clone();
- let pane = self.active_pane();
- let item_ix = pane.read(cx).active_item_index();
- let item = pane.read(cx).active_item();
- let pane = pane.downgrade();
-
- cx.spawn(|_, mut cx| async move {
- if let Some(item) = item {
- Pane::save_item(project, &pane, item_ix, item.as_ref(), save_intent, &mut cx)
- .await
- .map(|_| ())
- } else {
- Ok(())
- }
- })
- }
-
- pub fn close_inactive_items_and_panes(
- &mut self,
- _: &CloseInactiveTabsAndPanes,
- cx: &mut ViewContext<Self>,
- ) {
- self.close_all_internal(true, SaveIntent::Close, cx)
- .map(|task| task.detach_and_log_err(cx));
- }
-
- pub fn close_all_items_and_panes(
- &mut self,
- action: &CloseAllItemsAndPanes,
- cx: &mut ViewContext<Self>,
- ) {
- self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx)
- .map(|task| task.detach_and_log_err(cx));
- }
-
- fn close_all_internal(
- &mut self,
- retain_active_pane: bool,
- save_intent: SaveIntent,
- cx: &mut ViewContext<Self>,
- ) -> Option<Task<Result<()>>> {
- let current_pane = self.active_pane();
-
- let mut tasks = Vec::new();
-
- if retain_active_pane {
- if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
- pane.close_inactive_items(&CloseInactiveItems, cx)
- }) {
- tasks.push(current_pane_close);
- };
- }
-
- for pane in self.panes() {
- if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
- continue;
- }
-
- if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
- pane.close_all_items(
- &CloseAllItems {
- save_intent: Some(save_intent),
- },
- cx,
- )
- }) {
- tasks.push(close_pane_items)
- }
- }
-
- if tasks.is_empty() {
- None
- } else {
- Some(cx.spawn(|_, _| async move {
- for task in tasks {
- task.await?
- }
- Ok(())
- }))
- }
- }
-
- pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
- let dock = match dock_side {
- DockPosition::Left => &self.left_dock,
- DockPosition::Bottom => &self.bottom_dock,
- DockPosition::Right => &self.right_dock,
- };
- let mut focus_center = false;
- let mut reveal_dock = false;
- dock.update(cx, |dock, cx| {
- let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
- let was_visible = dock.is_open() && !other_is_zoomed;
- dock.set_open(!was_visible, cx);
-
- if let Some(active_panel) = dock.active_panel() {
- if was_visible {
- if active_panel.focus_handle(cx).contains_focused(cx) {
- focus_center = true;
- }
- } else {
- let focus_handle = &active_panel.focus_handle(cx);
- cx.focus(focus_handle);
- reveal_dock = true;
- }
- }
- });
-
- if reveal_dock {
- self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
- }
-
- if focus_center {
- self.active_pane.update(cx, |pane, cx| pane.focus(cx))
- }
-
- cx.notify();
- self.serialize_workspace(cx);
- }
-
- pub fn close_all_docks(&mut self, cx: &mut ViewContext<Self>) {
- let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
-
- for dock in docks {
- dock.update(cx, |dock, cx| {
- dock.set_open(false, cx);
- });
- }
-
- // todo!("focus")
- // cx.focus_self();
- cx.notify();
- self.serialize_workspace(cx);
- }
-
- /// Transfer focus to the panel of the given type.
- pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<View<T>> {
- let panel = self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?;
- panel.to_any().downcast().ok()
- }
-
- /// Focus the panel of the given type if it isn't already focused. If it is
- /// already focused, then transfer focus back to the workspace center.
- pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
- self.focus_or_unfocus_panel::<T>(cx, |panel, cx| {
- !panel.focus_handle(cx).contains_focused(cx)
- });
- }
-
- /// Focus or unfocus the given panel type, depending on the given callback.
- fn focus_or_unfocus_panel<T: Panel>(
- &mut self,
- cx: &mut ViewContext<Self>,
- should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
- ) -> Option<Arc<dyn PanelHandle>> {
- for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
- if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
- let mut focus_center = false;
- let panel = dock.update(cx, |dock, cx| {
- dock.activate_panel(panel_index, cx);
-
- let panel = dock.active_panel().cloned();
- if let Some(panel) = panel.as_ref() {
- if should_focus(&**panel, cx) {
- dock.set_open(true, cx);
- panel.focus_handle(cx).focus(cx);
- } else {
- focus_center = true;
- }
- }
- panel
- });
-
- if focus_center {
- self.active_pane.update(cx, |pane, cx| pane.focus(cx))
- }
-
- self.serialize_workspace(cx);
- cx.notify();
- return panel;
- }
- }
- None
- }
-
- pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
- for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
- let dock = dock.read(cx);
- if let Some(panel) = dock.panel::<T>() {
- return Some(panel);
- }
- }
- None
- }
-
- // todo!("implement zoom")
- #[allow(unused)]
- fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
- for pane in &self.panes {
- pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
- }
-
- self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx));
- self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx));
- self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx));
- self.zoomed = None;
- self.zoomed_position = None;
-
- cx.notify();
- }
-
- // #[cfg(any(test, feature = "test-support"))]
- // pub fn zoomed_view(&self, cx: &AppContext) -> Option<AnyViewHandle> {
- // self.zoomed.and_then(|view| view.upgrade(cx))
- // }
-
- fn dismiss_zoomed_items_to_reveal(
- &mut self,
- dock_to_reveal: Option<DockPosition>,
- cx: &mut ViewContext<Self>,
- ) {
- // If a center pane is zoomed, unzoom it.
- for pane in &self.panes {
- if pane != &self.active_pane || dock_to_reveal.is_some() {
- pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
- }
- }
-
- // If another dock is zoomed, hide it.
- let mut focus_center = false;
- for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
- dock.update(cx, |dock, cx| {
- if Some(dock.position()) != dock_to_reveal {
- if let Some(panel) = dock.active_panel() {
- if panel.is_zoomed(cx) {
- focus_center |= panel.focus_handle(cx).contains_focused(cx);
- dock.set_open(false, cx);
- }
- }
- }
- });
- }
-
- if focus_center {
- self.active_pane.update(cx, |pane, cx| pane.focus(cx))
- }
-
- if self.zoomed_position != dock_to_reveal {
- self.zoomed = None;
- self.zoomed_position = None;
- }
-
- cx.notify();
- }
-
- fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
- let pane = cx.new_view(|cx| {
- Pane::new(
- self.weak_handle(),
- self.project.clone(),
- self.pane_history_timestamp.clone(),
- None,
- cx,
- )
- });
- cx.subscribe(&pane, Self::handle_pane_event).detach();
- self.panes.push(pane.clone());
- cx.focus_view(&pane);
- cx.emit(Event::PaneAdded(pane.clone()));
- pane
- }
-
- pub fn add_item_to_center(
- &mut self,
- item: Box<dyn ItemHandle>,
- cx: &mut ViewContext<Self>,
- ) -> bool {
- if let Some(center_pane) = self.last_active_center_pane.clone() {
- if let Some(center_pane) = center_pane.upgrade() {
- center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
- true
- } else {
- false
- }
- } else {
- false
- }
- }
-
- pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
- self.active_pane
- .update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
- }
-
- pub fn split_item(
- &mut self,
- split_direction: SplitDirection,
- item: Box<dyn ItemHandle>,
- cx: &mut ViewContext<Self>,
- ) {
- let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx);
- new_pane.update(cx, move |new_pane, cx| {
- new_pane.add_item(item, true, true, None, cx)
- })
- }
-
- pub fn open_abs_path(
- &mut self,
- abs_path: PathBuf,
- visible: bool,
- cx: &mut ViewContext<Self>,
- ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
- cx.spawn(|workspace, mut cx| async move {
- let open_paths_task_result = workspace
- .update(&mut cx, |workspace, cx| {
- workspace.open_paths(vec![abs_path.clone()], visible, cx)
- })
- .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
- .await;
- anyhow::ensure!(
- open_paths_task_result.len() == 1,
- "open abs path {abs_path:?} task returned incorrect number of results"
- );
- match open_paths_task_result
- .into_iter()
- .next()
- .expect("ensured single task result")
- {
- Some(open_result) => {
- open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
- }
- None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
- }
- })
- }
-
- pub fn split_abs_path(
- &mut self,
- abs_path: PathBuf,
- visible: bool,
- cx: &mut ViewContext<Self>,
- ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
- let project_path_task =
- Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
- cx.spawn(|this, mut cx| async move {
- let (_, path) = project_path_task.await?;
- this.update(&mut cx, |this, cx| this.split_path(path, cx))?
- .await
- })
- }
-
- pub fn open_path(
- &mut self,
- path: impl Into<ProjectPath>,
- pane: Option<WeakView<Pane>>,
- focus_item: bool,
- cx: &mut ViewContext<Self>,
- ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
- let pane = pane.unwrap_or_else(|| {
- self.last_active_center_pane.clone().unwrap_or_else(|| {
- self.panes
- .first()
- .expect("There must be an active pane")
- .downgrade()
- })
- });
-
- let task = self.load_path(path.into(), cx);
- cx.spawn(move |_, mut cx| async move {
- let (project_entry_id, build_item) = task.await?;
- pane.update(&mut cx, |pane, cx| {
- pane.open_item(project_entry_id, focus_item, cx, build_item)
- })
- })
- }
-
- pub fn split_path(
- &mut self,
- path: impl Into<ProjectPath>,
- cx: &mut ViewContext<Self>,
- ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
- let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
- self.panes
- .first()
- .expect("There must be an active pane")
- .downgrade()
- });
-
- if let Member::Pane(center_pane) = &self.center.root {
- if center_pane.read(cx).items_len() == 0 {
- return self.open_path(path, Some(pane), true, cx);
- }
- }
-
- let task = self.load_path(path.into(), cx);
- cx.spawn(|this, mut cx| async move {
- let (project_entry_id, build_item) = task.await?;
- this.update(&mut cx, move |this, cx| -> Option<_> {
- let pane = pane.upgrade()?;
- let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
- new_pane.update(cx, |new_pane, cx| {
- Some(new_pane.open_item(project_entry_id, true, cx, build_item))
- })
- })
- .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
- })
- }
-
- fn load_path(
- &mut self,
- path: ProjectPath,
- cx: &mut ViewContext<Self>,
- ) -> Task<
- Result<(
- Option<ProjectEntryId>,
- impl 'static + Send + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
- )>,
- > {
- let project = self.project().clone();
- let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
- cx.spawn(|_, mut cx| async move {
- let (project_entry_id, project_item) = project_item.await?;
- let build_item = cx.update(|_, cx| {
- cx.default_global::<ProjectItemBuilders>()
- .get(&project_item.entity_type())
- .ok_or_else(|| anyhow!("no item builder for project item"))
- .cloned()
- })??;
- let build_item =
- move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
- Ok((project_entry_id, build_item))
- })
- }
-
- pub fn open_project_item<T>(
- &mut self,
- project_item: Model<T::Item>,
- cx: &mut ViewContext<Self>,
- ) -> View<T>
- where
- T: ProjectItem,
- {
- use project::Item as _;
-
- let entry_id = project_item.read(cx).entry_id(cx);
- if let Some(item) = entry_id
- .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
- .and_then(|item| item.downcast())
- {
- self.activate_item(&item, cx);
- return item;
- }
-
- let item = cx.new_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
- self.add_item(Box::new(item.clone()), cx);
- item
- }
-
- pub fn split_project_item<T>(
- &mut self,
- project_item: Model<T::Item>,
- cx: &mut ViewContext<Self>,
- ) -> View<T>
- where
- T: ProjectItem,
- {
- use project::Item as _;
-
- let entry_id = project_item.read(cx).entry_id(cx);
- if let Some(item) = entry_id
- .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
- .and_then(|item| item.downcast())
- {
- self.activate_item(&item, cx);
- return item;
- }
-
- let item = cx.new_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
- self.split_item(SplitDirection::Right, Box::new(item.clone()), cx);
- item
- }
-
- pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
- if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
- self.active_pane.update(cx, |pane, cx| {
- pane.add_item(Box::new(shared_screen), false, true, None, cx)
- });
- }
- }
-
- pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
- let result = self.panes.iter().find_map(|pane| {
- pane.read(cx)
- .index_for_item(item)
- .map(|ix| (pane.clone(), ix))
- });
- if let Some((pane, ix)) = result {
- pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
- true
- } else {
- false
- }
- }
-
- fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
- let panes = self.center.panes();
- if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
- cx.focus_view(&pane);
- } else {
- self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
- }
- }
-
- pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
- let panes = self.center.panes();
- if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
- let next_ix = (ix + 1) % panes.len();
- let next_pane = panes[next_ix].clone();
- cx.focus_view(&next_pane);
- }
- }
-
- pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
- let panes = self.center.panes();
- if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
- let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
- let prev_pane = panes[prev_ix].clone();
- cx.focus_view(&prev_pane);
- }
- }
-
- pub fn activate_pane_in_direction(
- &mut self,
- direction: SplitDirection,
- cx: &mut ViewContext<Self>,
- ) {
- if let Some(pane) = self.find_pane_in_direction(direction, cx) {
- cx.focus_view(pane);
- }
- }
-
- pub fn swap_pane_in_direction(
- &mut self,
- direction: SplitDirection,
- cx: &mut ViewContext<Self>,
- ) {
- if let Some(to) = self
- .find_pane_in_direction(direction, cx)
- .map(|pane| pane.clone())
- {
- self.center.swap(&self.active_pane.clone(), &to);
- cx.notify();
- }
- }
-
- fn find_pane_in_direction(
- &mut self,
- direction: SplitDirection,
- cx: &mut ViewContext<Self>,
- ) -> Option<&View<Pane>> {
- let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else {
- return None;
- };
- let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
- let center = match cursor {
- Some(cursor) if bounding_box.contains(&cursor) => cursor,
- _ => bounding_box.center(),
- };
-
- let distance_to_next = 8.; //todo(pane dividers styling)
-
- let target = match direction {
- SplitDirection::Left => {
- Point::new(bounding_box.left() - distance_to_next.into(), center.y)
- }
- SplitDirection::Right => {
- Point::new(bounding_box.right() + distance_to_next.into(), center.y)
- }
- SplitDirection::Up => {
- Point::new(center.x, bounding_box.top() - distance_to_next.into())
- }
- SplitDirection::Down => {
- Point::new(center.x, bounding_box.bottom() + distance_to_next.into())
- }
- };
- self.center.pane_at_pixel_position(target)
- }
-
- fn handle_pane_focused(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
- if self.active_pane != pane {
- self.active_pane = pane.clone();
- self.status_bar.update(cx, |status_bar, cx| {
- status_bar.set_active_pane(&self.active_pane, cx);
- });
- self.active_item_path_changed(cx);
- self.last_active_center_pane = Some(pane.downgrade());
- }
-
- self.dismiss_zoomed_items_to_reveal(None, cx);
- if pane.read(cx).is_zoomed() {
- self.zoomed = Some(pane.downgrade().into());
- } else {
- self.zoomed = None;
- }
- self.zoomed_position = None;
- self.update_active_view_for_followers(cx);
-
- cx.notify();
- }
-
- fn handle_pane_event(
- &mut self,
- pane: View<Pane>,
- event: &pane::Event,
- cx: &mut ViewContext<Self>,
- ) {
- match event {
- pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx),
- pane::Event::Split(direction) => {
- self.split_and_clone(pane, *direction, cx);
- }
- pane::Event::Remove => self.remove_pane(pane, cx),
- pane::Event::ActivateItem { local } => {
- if *local {
- self.unfollow(&pane, cx);
- }
- if &pane == self.active_pane() {
- self.active_item_path_changed(cx);
- self.update_active_view_for_followers(cx);
- }
- }
- pane::Event::ChangeItemTitle => {
- if pane == self.active_pane {
- self.active_item_path_changed(cx);
- }
- self.update_window_edited(cx);
- }
- pane::Event::RemoveItem { item_id } => {
- self.update_window_edited(cx);
- if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
- if entry.get().entity_id() == pane.entity_id() {
- entry.remove();
- }
- }
- }
- pane::Event::Focus => {
- self.handle_pane_focused(pane.clone(), cx);
- }
- pane::Event::ZoomIn => {
- if pane == self.active_pane {
- pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
- if pane.read(cx).has_focus(cx) {
- self.zoomed = Some(pane.downgrade().into());
- self.zoomed_position = None;
- }
- cx.notify();
- }
- }
- pane::Event::ZoomOut => {
- pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
- if self.zoomed_position.is_none() {
- self.zoomed = None;
- }
- cx.notify();
- }
- }
-
- self.serialize_workspace(cx);
- }
-
- pub fn split_pane(
- &mut self,
- pane_to_split: View<Pane>,
- split_direction: SplitDirection,
- cx: &mut ViewContext<Self>,
- ) -> View<Pane> {
- let new_pane = self.add_pane(cx);
- self.center
- .split(&pane_to_split, &new_pane, split_direction)
- .unwrap();
- cx.notify();
- new_pane
- }
-
- pub fn split_and_clone(
- &mut self,
- pane: View<Pane>,
- direction: SplitDirection,
- cx: &mut ViewContext<Self>,
- ) -> Option<View<Pane>> {
- let item = pane.read(cx).active_item()?;
- let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
- let new_pane = self.add_pane(cx);
- new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
- self.center.split(&pane, &new_pane, direction).unwrap();
- Some(new_pane)
- } else {
- None
- };
- cx.notify();
- maybe_pane_handle
- }
-
- pub fn split_pane_with_item(
- &mut self,
- pane_to_split: WeakView<Pane>,
- split_direction: SplitDirection,
- from: WeakView<Pane>,
- item_id_to_move: EntityId,
- cx: &mut ViewContext<Self>,
- ) {
- let Some(pane_to_split) = pane_to_split.upgrade() else {
- return;
- };
- let Some(from) = from.upgrade() else {
- return;
- };
-
- let new_pane = self.add_pane(cx);
- self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
- self.center
- .split(&pane_to_split, &new_pane, split_direction)
- .unwrap();
- cx.notify();
- }
-
- pub fn split_pane_with_project_entry(
- &mut self,
- pane_to_split: WeakView<Pane>,
- split_direction: SplitDirection,
- project_entry: ProjectEntryId,
- cx: &mut ViewContext<Self>,
- ) -> Option<Task<Result<()>>> {
- let pane_to_split = pane_to_split.upgrade()?;
- let new_pane = self.add_pane(cx);
- self.center
- .split(&pane_to_split, &new_pane, split_direction)
- .unwrap();
-
- let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
- let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
- Some(cx.foreground_executor().spawn(async move {
- task.await?;
- Ok(())
- }))
- }
-
- pub fn move_item(
- &mut self,
- source: View<Pane>,
- destination: View<Pane>,
- item_id_to_move: EntityId,
- destination_index: usize,
- cx: &mut ViewContext<Self>,
- ) {
- let item_to_move = source
- .read(cx)
- .items()
- .enumerate()
- .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move);
-
- if item_to_move.is_none() {
- log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop");
- return;
- }
- let (item_ix, item_handle) = item_to_move.unwrap();
- let item_handle = item_handle.clone();
-
- if source != destination {
- // Close item from previous pane
- source.update(cx, |source, cx| {
- source.remove_item(item_ix, false, cx);
- });
- }
-
- // This automatically removes duplicate items in the pane
- destination.update(cx, |destination, cx| {
- destination.add_item(item_handle, true, true, Some(destination_index), cx);
- destination.focus(cx)
- });
- }
-
- fn remove_pane(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
- if self.center.remove(&pane).unwrap() {
- self.force_remove_pane(&pane, cx);
- self.unfollow(&pane, cx);
- self.last_leaders_by_pane.remove(&pane.downgrade());
- for removed_item in pane.read(cx).items() {
- self.panes_by_item.remove(&removed_item.item_id());
- }
-
- cx.notify();
- } else {
- self.active_item_path_changed(cx);
- }
- }
-
- pub fn panes(&self) -> &[View<Pane>] {
- &self.panes
- }
-
- pub fn active_pane(&self) -> &View<Pane> {
- &self.active_pane
- }
-
- pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<View<Pane>> {
- let weak_pane = self.panes_by_item.get(&handle.item_id())?;
- weak_pane.upgrade()
- }
-
- fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
- self.follower_states.retain(|_, state| {
- if state.leader_id == peer_id {
- for item in state.items_by_leader_view_id.values() {
- item.set_leader_peer_id(None, cx);
- }
- false
- } else {
- true
- }
- });
- cx.notify();
- }
-
- pub fn start_following(
- &mut self,
- leader_id: PeerId,
- cx: &mut ViewContext<Self>,
- ) -> Option<Task<Result<()>>> {
- let pane = self.active_pane().clone();
-
- self.last_leaders_by_pane
- .insert(pane.downgrade(), leader_id);
- self.unfollow(&pane, cx);
- self.follower_states.insert(
- pane.clone(),
- FollowerState {
- leader_id,
- active_view_id: None,
- items_by_leader_view_id: Default::default(),
- },
- );
- cx.notify();
-
- let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
- let project_id = self.project.read(cx).remote_id();
- let request = self.app_state.client.request(proto::Follow {
- room_id,
- project_id,
- leader_id: Some(leader_id),
- });
-
- Some(cx.spawn(|this, mut cx| async move {
- let response = request.await?;
- this.update(&mut cx, |this, _| {
- let state = this
- .follower_states
- .get_mut(&pane)
- .ok_or_else(|| anyhow!("following interrupted"))?;
- state.active_view_id = if let Some(active_view_id) = response.active_view_id {
- Some(ViewId::from_proto(active_view_id)?)
- } else {
- None
- };
- Ok::<_, anyhow::Error>(())
- })??;
- Self::add_views_from_leader(
- this.clone(),
- leader_id,
- vec![pane],
- response.views,
- &mut cx,
- )
- .await?;
- this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
- Ok(())
- }))
- }
-
- pub fn follow_next_collaborator(
- &mut self,
- _: &FollowNextCollaborator,
- cx: &mut ViewContext<Self>,
- ) {
- let collaborators = self.project.read(cx).collaborators();
- let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
- let mut collaborators = collaborators.keys().copied();
- for peer_id in collaborators.by_ref() {
- if peer_id == leader_id {
- break;
- }
- }
- collaborators.next()
- } else if let Some(last_leader_id) =
- self.last_leaders_by_pane.get(&self.active_pane.downgrade())
- {
- if collaborators.contains_key(last_leader_id) {
- Some(*last_leader_id)
- } else {
- None
- }
- } else {
- None
- };
-
- let pane = self.active_pane.clone();
- let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
- else {
- return;
- };
- if Some(leader_id) == self.unfollow(&pane, cx) {
- return;
- }
- self.start_following(leader_id, cx)
- .map(|task| task.detach_and_log_err(cx));
- }
-
- pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) {
- let Some(room) = ActiveCall::global(cx).read(cx).room() else {
- return;
- };
- let room = room.read(cx);
- let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
- return;
- };
-
- let project = self.project.read(cx);
-
- let other_project_id = match remote_participant.location {
- call::ParticipantLocation::External => None,
- call::ParticipantLocation::UnsharedProject => None,
- call::ParticipantLocation::SharedProject { project_id } => {
- if Some(project_id) == project.remote_id() {
- None
- } else {
- Some(project_id)
- }
- }
- };
-
- // if they are active in another project, follow there.
- if let Some(project_id) = other_project_id {
- let app_state = self.app_state.clone();
- crate::join_remote_project(project_id, remote_participant.user.id, app_state, cx)
- .detach_and_log_err(cx);
- }
-
- // if you're already following, find the right pane and focus it.
- for (pane, state) in &self.follower_states {
- if leader_id == state.leader_id {
- cx.focus_view(pane);
- return;
- }
- }
-
- // Otherwise, follow.
- self.start_following(leader_id, cx)
- .map(|task| task.detach_and_log_err(cx));
- }
-
- pub fn unfollow(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Self>) -> Option<PeerId> {
- let state = self.follower_states.remove(pane)?;
- let leader_id = state.leader_id;
- for (_, item) in state.items_by_leader_view_id {
- item.set_leader_peer_id(None, cx);
- }
-
- if self
- .follower_states
- .values()
- .all(|state| state.leader_id != state.leader_id)
- {
- let project_id = self.project.read(cx).remote_id();
- let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
- self.app_state
- .client
- .send(proto::Unfollow {
- room_id,
- project_id,
- leader_id: Some(leader_id),
- })
- .log_err();
- }
-
- cx.notify();
- Some(leader_id)
- }
-
- // pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
- // self.follower_states
- // .values()
- // .any(|state| state.leader_id == peer_id)
- // }
-
- fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
- let active_entry = self.active_project_path(cx);
- self.project
- .update(cx, |project, cx| project.set_active_path(active_entry, cx));
- self.update_window_title(cx);
- }
-
- fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
- let project = self.project().read(cx);
- let mut title = String::new();
-
- if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
- let filename = path
- .path
- .file_name()
- .map(|s| s.to_string_lossy())
- .or_else(|| {
- Some(Cow::Borrowed(
- project
- .worktree_for_id(path.worktree_id, cx)?
- .read(cx)
- .root_name(),
- ))
- });
-
- if let Some(filename) = filename {
- title.push_str(filename.as_ref());
- title.push_str(" β ");
- }
- }
-
- for (i, name) in project.worktree_root_names(cx).enumerate() {
- if i > 0 {
- title.push_str(", ");
- }
- title.push_str(name);
- }
-
- if title.is_empty() {
- title = "empty project".to_string();
- }
-
- if project.is_remote() {
- title.push_str(" β");
- } else if project.is_shared() {
- title.push_str(" β");
- }
-
- cx.set_window_title(&title);
- }
-
- fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
- let is_edited = !self.project.read(cx).is_read_only()
- && self
- .items(cx)
- .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
- if is_edited != self.window_edited {
- self.window_edited = is_edited;
- cx.set_window_edited(self.window_edited)
- }
- }
-
- fn render_notifications(&self, _cx: &ViewContext<Self>) -> Option<Div> {
- if self.notifications.is_empty() {
- None
- } else {
- Some(
- div()
- .absolute()
- .z_index(100)
- .right_3()
- .bottom_3()
- .w_96()
- .h_full()
- .flex()
- .flex_col()
- .justify_end()
- .gap_2()
- .children(
- self.notifications
- .iter()
- .map(|(_, _, notification)| notification.to_any()),
- ),
- )
- }
- }
-
- // RPC handlers
-
- fn handle_follow(
- &mut self,
- follower_project_id: Option<u64>,
- cx: &mut ViewContext<Self>,
- ) -> proto::FollowResponse {
- let client = &self.app_state.client;
- let project_id = self.project.read(cx).remote_id();
-
- let active_view_id = self.active_item(cx).and_then(|i| {
- Some(
- i.to_followable_item_handle(cx)?
- .remote_id(client, cx)?
- .to_proto(),
- )
- });
-
- cx.notify();
-
- self.last_active_view_id = active_view_id.clone();
- proto::FollowResponse {
- active_view_id,
- views: self
- .panes()
- .iter()
- .flat_map(|pane| {
- let leader_id = self.leader_for_pane(pane);
- pane.read(cx).items().filter_map({
- let cx = &cx;
- move |item| {
- let item = item.to_followable_item_handle(cx)?;
- if (project_id.is_none() || project_id != follower_project_id)
- && item.is_project_item(cx)
- {
- return None;
- }
- let id = item.remote_id(client, cx)?.to_proto();
- let variant = item.to_state_proto(cx)?;
- Some(proto::View {
- id: Some(id),
- leader_id,
- variant: Some(variant),
- })
- }
- })
- })
- .collect(),
- }
- }
-
- fn handle_update_followers(
- &mut self,
- leader_id: PeerId,
- message: proto::UpdateFollowers,
- _cx: &mut ViewContext<Self>,
- ) {
- self.leader_updates_tx
- .unbounded_send((leader_id, message))
- .ok();
- }
-
- async fn process_leader_update(
- this: &WeakView<Self>,
- leader_id: PeerId,
- update: proto::UpdateFollowers,
- cx: &mut AsyncWindowContext,
- ) -> Result<()> {
- match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
- proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
- this.update(cx, |this, _| {
- for (_, state) in &mut this.follower_states {
- if state.leader_id == leader_id {
- state.active_view_id =
- if let Some(active_view_id) = update_active_view.id.clone() {
- Some(ViewId::from_proto(active_view_id)?)
- } else {
- None
- };
- }
- }
- anyhow::Ok(())
- })??;
- }
- proto::update_followers::Variant::UpdateView(update_view) => {
- let variant = update_view
- .variant
- .ok_or_else(|| anyhow!("missing update view variant"))?;
- let id = update_view
- .id
- .ok_or_else(|| anyhow!("missing update view id"))?;
- let mut tasks = Vec::new();
- this.update(cx, |this, cx| {
- let project = this.project.clone();
- for (_, state) in &mut this.follower_states {
- if state.leader_id == leader_id {
- let view_id = ViewId::from_proto(id.clone())?;
- if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
- tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
- }
- }
- }
- anyhow::Ok(())
- })??;
- try_join_all(tasks).await.log_err();
- }
- proto::update_followers::Variant::CreateView(view) => {
- let panes = this.update(cx, |this, _| {
- this.follower_states
- .iter()
- .filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane))
- .cloned()
- .collect()
- })?;
- Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
- }
- }
- this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
- Ok(())
- }
-
- async fn add_views_from_leader(
- this: WeakView<Self>,
- leader_id: PeerId,
- panes: Vec<View<Pane>>,
- views: Vec<proto::View>,
- cx: &mut AsyncWindowContext,
- ) -> Result<()> {
- let this = this.upgrade().context("workspace dropped")?;
-
- let item_builders = cx.update(|_, cx| {
- cx.default_global::<FollowableItemBuilders>()
- .values()
- .map(|b| b.0)
- .collect::<Vec<_>>()
- })?;
-
- let mut item_tasks_by_pane = HashMap::default();
- for pane in panes {
- let mut item_tasks = Vec::new();
- let mut leader_view_ids = Vec::new();
- for view in &views {
- let Some(id) = &view.id else { continue };
- let id = ViewId::from_proto(id.clone())?;
- let mut variant = view.variant.clone();
- if variant.is_none() {
- Err(anyhow!("missing view variant"))?;
- }
- for build_item in &item_builders {
- let task = cx.update(|_, cx| {
- build_item(pane.clone(), this.clone(), id, &mut variant, cx)
- })?;
- if let Some(task) = task {
- item_tasks.push(task);
- leader_view_ids.push(id);
- break;
- } else {
- assert!(variant.is_some());
- }
- }
- }
-
- item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
- }
-
- for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
- let items = futures::future::try_join_all(item_tasks).await?;
- this.update(cx, |this, cx| {
- let state = this.follower_states.get_mut(&pane)?;
- for (id, item) in leader_view_ids.into_iter().zip(items) {
- item.set_leader_peer_id(Some(leader_id), cx);
- state.items_by_leader_view_id.insert(id, item);
- }
-
- Some(())
- })?;
- }
- Ok(())
- }
-
- fn update_active_view_for_followers(&mut self, cx: &mut ViewContext<Self>) {
- let mut is_project_item = true;
- let mut update = proto::UpdateActiveView::default();
-
- if let Some(item) = self.active_item(cx) {
- if item.focus_handle(cx).contains_focused(cx) {
- if let Some(item) = item.to_followable_item_handle(cx) {
- is_project_item = item.is_project_item(cx);
- update = proto::UpdateActiveView {
- id: item
- .remote_id(&self.app_state.client, cx)
- .map(|id| id.to_proto()),
- leader_id: self.leader_for_pane(&self.active_pane),
- };
- }
- }
- }
-
- if update.id != self.last_active_view_id {
- self.last_active_view_id = update.id.clone();
- self.update_followers(
- is_project_item,
- proto::update_followers::Variant::UpdateActiveView(update),
- cx,
- );
- }
- }
-
- fn update_followers(
- &self,
- project_only: bool,
- update: proto::update_followers::Variant,
- cx: &mut WindowContext,
- ) -> Option<()> {
- let project_id = if project_only {
- self.project.read(cx).remote_id()
- } else {
- None
- };
- self.app_state().workspace_store.update(cx, |store, cx| {
- store.update_followers(project_id, update, cx)
- })
- }
-
- pub fn leader_for_pane(&self, pane: &View<Pane>) -> Option<PeerId> {
- self.follower_states.get(pane).map(|state| state.leader_id)
- }
-
- fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
- cx.notify();
-
- let call = self.active_call()?;
- let room = call.read(cx).room()?.read(cx);
- let participant = room.remote_participant_for_peer_id(leader_id)?;
- let mut items_to_activate = Vec::new();
-
- let leader_in_this_app;
- let leader_in_this_project;
- match participant.location {
- call::ParticipantLocation::SharedProject { project_id } => {
- leader_in_this_app = true;
- leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
- }
- call::ParticipantLocation::UnsharedProject => {
- leader_in_this_app = true;
- leader_in_this_project = false;
- }
- call::ParticipantLocation::External => {
- leader_in_this_app = false;
- leader_in_this_project = false;
- }
- };
-
- for (pane, state) in &self.follower_states {
- if state.leader_id != leader_id {
- continue;
- }
- if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
- if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
- if leader_in_this_project || !item.is_project_item(cx) {
- items_to_activate.push((pane.clone(), item.boxed_clone()));
- }
- }
- continue;
- }
-
- if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
- items_to_activate.push((pane.clone(), Box::new(shared_screen)));
- }
- }
-
- for (pane, item) in items_to_activate {
- let pane_was_focused = pane.read(cx).has_focus(cx);
- if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
- pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
- } else {
- pane.update(cx, |pane, cx| {
- pane.add_item(item.boxed_clone(), false, false, None, cx)
- });
- }
-
- if pane_was_focused {
- pane.update(cx, |pane, cx| pane.focus_active_item(cx));
- }
- }
-
- None
- }
-
- fn shared_screen_for_peer(
- &self,
- peer_id: PeerId,
- pane: &View<Pane>,
- cx: &mut ViewContext<Self>,
- ) -> Option<View<SharedScreen>> {
- let call = self.active_call()?;
- let room = call.read(cx).room()?.read(cx);
- let participant = room.remote_participant_for_peer_id(peer_id)?;
- let track = participant.video_tracks.values().next()?.clone();
- let user = participant.user.clone();
-
- for item in pane.read(cx).items_of_type::<SharedScreen>() {
- if item.read(cx).peer_id == peer_id {
- return Some(item);
- }
- }
-
- Some(cx.new_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
- }
-
- pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
- if cx.is_window_active() {
- self.update_active_view_for_followers(cx);
- cx.background_executor()
- .spawn(persistence::DB.update_timestamp(self.database_id()))
- .detach();
- } else {
- for pane in &self.panes {
- pane.update(cx, |pane, cx| {
- if let Some(item) = pane.active_item() {
- item.workspace_deactivated(cx);
- }
- if matches!(
- WorkspaceSettings::get_global(cx).autosave,
- AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
- ) {
- for item in pane.items() {
- Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
- .detach_and_log_err(cx);
- }
- }
- });
- }
- }
- }
-
- fn active_call(&self) -> Option<&Model<ActiveCall>> {
- self.active_call.as_ref().map(|(call, _)| call)
- }
-
- fn on_active_call_event(
- &mut self,
- _: Model<ActiveCall>,
- event: &call::room::Event,
- cx: &mut ViewContext<Self>,
- ) {
- match event {
- call::room::Event::ParticipantLocationChanged { participant_id }
- | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
- self.leader_updated(*participant_id, cx);
- }
- _ => {}
- }
- }
-
- pub fn database_id(&self) -> WorkspaceId {
- self.database_id
- }
-
- fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
- let project = self.project().read(cx);
-
- if project.is_local() {
- Some(
- project
- .visible_worktrees(cx)
- .map(|worktree| worktree.read(cx).abs_path())
- .collect::<Vec<_>>()
- .into(),
- )
- } else {
- None
- }
- }
-
- fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
- match member {
- Member::Axis(PaneAxis { members, .. }) => {
- for child in members.iter() {
- self.remove_panes(child.clone(), cx)
- }
- }
- Member::Pane(pane) => {
- self.force_remove_pane(&pane, cx);
- }
- }
- }
-
- fn force_remove_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Workspace>) {
- self.panes.retain(|p| p != pane);
- self.panes
- .last()
- .unwrap()
- .update(cx, |pane, cx| pane.focus(cx));
- if self.last_active_center_pane == Some(pane.downgrade()) {
- self.last_active_center_pane = None;
- }
- cx.notify();
- }
-
- #[allow(unused)]
- fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
- self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
- cx.background_executor()
- .timer(Duration::from_millis(100))
- .await;
- this.update(&mut cx, |this, cx| this.serialize_workspace(cx))
- .log_err();
- }));
- }
-
- fn serialize_workspace(&self, cx: &mut ViewContext<Self>) {
- fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
- let (items, active) = {
- let pane = pane_handle.read(cx);
- let active_item_id = pane.active_item().map(|item| item.item_id());
- (
- pane.items()
- .filter_map(|item_handle| {
- Some(SerializedItem {
- kind: Arc::from(item_handle.serialized_item_kind()?),
- item_id: item_handle.item_id().as_u64(),
- active: Some(item_handle.item_id()) == active_item_id,
- })
- })
- .collect::<Vec<_>>(),
- pane.has_focus(cx),
- )
- };
-
- SerializedPane::new(items, active)
- }
-
- fn build_serialized_pane_group(
- pane_group: &Member,
- cx: &WindowContext,
- ) -> SerializedPaneGroup {
- match pane_group {
- Member::Axis(PaneAxis {
- axis,
- members,
- flexes,
- bounding_boxes: _,
- }) => SerializedPaneGroup::Group {
- axis: *axis,
- children: members
- .iter()
- .map(|member| build_serialized_pane_group(member, cx))
- .collect::<Vec<_>>(),
- flexes: Some(flexes.lock().clone()),
- },
- Member::Pane(pane_handle) => {
- SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
- }
- }
- }
-
- fn build_serialized_docks(
- this: &Workspace,
- cx: &mut ViewContext<Workspace>,
- ) -> DockStructure {
- let left_dock = this.left_dock.read(cx);
- let left_visible = left_dock.is_open();
- let left_active_panel = left_dock
- .visible_panel()
- .and_then(|panel| Some(panel.persistent_name().to_string()));
- let left_dock_zoom = left_dock
- .visible_panel()
- .map(|panel| panel.is_zoomed(cx))
- .unwrap_or(false);
-
- let right_dock = this.right_dock.read(cx);
- let right_visible = right_dock.is_open();
- let right_active_panel = right_dock
- .visible_panel()
- .and_then(|panel| Some(panel.persistent_name().to_string()));
- let right_dock_zoom = right_dock
- .visible_panel()
- .map(|panel| panel.is_zoomed(cx))
- .unwrap_or(false);
-
- let bottom_dock = this.bottom_dock.read(cx);
- let bottom_visible = bottom_dock.is_open();
- let bottom_active_panel = bottom_dock
- .visible_panel()
- .and_then(|panel| Some(panel.persistent_name().to_string()));
- let bottom_dock_zoom = bottom_dock
- .visible_panel()
- .map(|panel| panel.is_zoomed(cx))
- .unwrap_or(false);
-
- DockStructure {
- left: DockData {
- visible: left_visible,
- active_panel: left_active_panel,
- zoom: left_dock_zoom,
- },
- right: DockData {
- visible: right_visible,
- active_panel: right_active_panel,
- zoom: right_dock_zoom,
- },
- bottom: DockData {
- visible: bottom_visible,
- active_panel: bottom_active_panel,
- zoom: bottom_dock_zoom,
- },
- }
- }
-
- if let Some(location) = self.location(cx) {
- // Load bearing special case:
- // - with_local_workspace() relies on this to not have other stuff open
- // when you open your log
- if !location.paths().is_empty() {
- let center_group = build_serialized_pane_group(&self.center.root, cx);
- let docks = build_serialized_docks(self, cx);
-
- let serialized_workspace = SerializedWorkspace {
- id: self.database_id,
- location,
- center_group,
- bounds: Default::default(),
- display: Default::default(),
- docks,
- };
-
- cx.spawn(|_, _| persistence::DB.save_workspace(serialized_workspace))
- .detach();
- }
- }
- }
-
- pub(crate) fn load_workspace(
- serialized_workspace: SerializedWorkspace,
- paths_to_open: Vec<Option<ProjectPath>>,
- cx: &mut ViewContext<Workspace>,
- ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
- cx.spawn(|workspace, mut cx| async move {
- let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
-
- let mut center_group = None;
- let mut center_items = None;
-
- // Traverse the splits tree and add to things
- if let Some((group, active_pane, items)) = serialized_workspace
- .center_group
- .deserialize(
- &project,
- serialized_workspace.id,
- workspace.clone(),
- &mut cx,
- )
- .await
- {
- center_items = Some(items);
- center_group = Some((group, active_pane))
- }
-
- let mut items_by_project_path = cx.update(|_, cx| {
- center_items
- .unwrap_or_default()
- .into_iter()
- .filter_map(|item| {
- let item = item?;
- let project_path = item.project_path(cx)?;
- Some((project_path, item))
- })
- .collect::<HashMap<_, _>>()
- })?;
-
- let opened_items = paths_to_open
- .into_iter()
- .map(|path_to_open| {
- path_to_open
- .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
- })
- .collect::<Vec<_>>();
-
- // Remove old panes from workspace panes list
- workspace.update(&mut cx, |workspace, cx| {
- if let Some((center_group, active_pane)) = center_group {
- workspace.remove_panes(workspace.center.root.clone(), cx);
-
- // Swap workspace center group
- workspace.center = PaneGroup::with_root(center_group);
- workspace.last_active_center_pane = active_pane.as_ref().map(|p| p.downgrade());
- if let Some(active_pane) = active_pane {
- workspace.active_pane = active_pane;
- cx.focus_self();
- } else {
- workspace.active_pane = workspace.center.first_pane().clone();
- }
- }
-
- let docks = serialized_workspace.docks;
- workspace.left_dock.update(cx, |dock, cx| {
- dock.set_open(docks.left.visible, cx);
- if let Some(active_panel) = docks.left.active_panel {
- if let Some(ix) = dock.panel_index_for_persistent_name(&active_panel, cx) {
- dock.activate_panel(ix, cx);
- }
- }
- dock.active_panel()
- .map(|panel| panel.set_zoomed(docks.left.zoom, cx));
- if docks.left.visible && docks.left.zoom {
- cx.focus_self()
- }
- });
- // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
- workspace.right_dock.update(cx, |dock, cx| {
- dock.set_open(docks.right.visible, cx);
- if let Some(active_panel) = docks.right.active_panel {
- if let Some(ix) = dock.panel_index_for_persistent_name(&active_panel, cx) {
- dock.activate_panel(ix, cx);
- }
- }
- dock.active_panel()
- .map(|panel| panel.set_zoomed(docks.right.zoom, cx));
-
- if docks.right.visible && docks.right.zoom {
- cx.focus_self()
- }
- });
- workspace.bottom_dock.update(cx, |dock, cx| {
- dock.set_open(docks.bottom.visible, cx);
- if let Some(active_panel) = docks.bottom.active_panel {
- if let Some(ix) = dock.panel_index_for_persistent_name(&active_panel, cx) {
- dock.activate_panel(ix, cx);
- }
- }
-
- dock.active_panel()
- .map(|panel| panel.set_zoomed(docks.bottom.zoom, cx));
-
- if docks.bottom.visible && docks.bottom.zoom {
- cx.focus_self()
- }
- });
-
- cx.notify();
- })?;
-
- // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
- workspace.update(&mut cx, |workspace, cx| workspace.serialize_workspace(cx))?;
-
- Ok(opened_items)
- })
- }
-
- fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
- self.add_workspace_actions_listeners(div, cx)
- .on_action(cx.listener(Self::close_inactive_items_and_panes))
- .on_action(cx.listener(Self::close_all_items_and_panes))
- .on_action(cx.listener(Self::save_all))
- .on_action(cx.listener(Self::add_folder_to_project))
- .on_action(cx.listener(Self::follow_next_collaborator))
- .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
- let pane = workspace.active_pane().clone();
- workspace.unfollow(&pane, cx);
- }))
- .on_action(cx.listener(|workspace, action: &Save, cx| {
- workspace
- .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
- .detach_and_log_err(cx);
- }))
- .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
- workspace
- .save_active_item(SaveIntent::SaveAs, cx)
- .detach_and_log_err(cx);
- }))
- .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
- workspace.activate_previous_pane(cx)
- }))
- .on_action(
- cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
- )
- .on_action(
- cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
- workspace.activate_pane_in_direction(action.0, cx)
- }),
- )
- .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
- workspace.swap_pane_in_direction(action.0, cx)
- }))
- .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
- this.toggle_dock(DockPosition::Left, cx);
- }))
- .on_action(
- cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
- workspace.toggle_dock(DockPosition::Right, cx);
- }),
- )
- .on_action(
- cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
- workspace.toggle_dock(DockPosition::Bottom, cx);
- }),
- )
- .on_action(
- cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
- workspace.close_all_docks(cx);
- }),
- )
- .on_action(cx.listener(Workspace::open))
- .on_action(cx.listener(Workspace::close_window))
- .on_action(cx.listener(Workspace::activate_pane_at_index))
- .on_action(
- cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
- workspace.reopen_closed_item(cx).detach();
- }),
- )
- }
-
- #[cfg(any(test, feature = "test-support"))]
- pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
- use node_runtime::FakeNodeRuntime;
-
- let client = project.read(cx).client();
- let user_store = project.read(cx).user_store();
-
- let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
- let app_state = Arc::new(AppState {
- languages: project.read(cx).languages().clone(),
- workspace_store,
- client,
- user_store,
- fs: project.read(cx).fs().clone(),
- build_window_options: |_, _, _| Default::default(),
- node_runtime: FakeNodeRuntime::new(),
- });
- let workspace = Self::new(0, project, app_state, cx);
- workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
- workspace
- }
-
- // fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
- // let dock = match position {
- // DockPosition::Left => &self.left_dock,
- // DockPosition::Right => &self.right_dock,
- // DockPosition::Bottom => &self.bottom_dock,
- // };
- // let active_panel = dock.read(cx).visible_panel()?;
- // let element = if Some(active_panel.id()) == self.zoomed.as_ref().map(|zoomed| zoomed.id()) {
- // dock.read(cx).render_placeholder(cx)
- // } else {
- // ChildView::new(dock, cx).into_any()
- // };
-
- // Some(
- // element
- // .constrained()
- // .dynamically(move |constraint, _, cx| match position {
- // DockPosition::Left | DockPosition::Right => SizeConstraint::new(
- // Vector2F::new(20., constraint.min.y()),
- // Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()),
- // ),
- // DockPosition::Bottom => SizeConstraint::new(
- // Vector2F::new(constraint.min.x(), 20.),
- // Vector2F::new(constraint.max.x(), cx.window_size().y() * 0.8),
- // ),
- // })
- // .into_any(),
- // )
- // }
- // }
- pub fn register_action<A: Action>(
- &mut self,
- callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
- ) -> &mut Self {
- let callback = Arc::new(callback);
-
- self.workspace_actions.push(Box::new(move |div, cx| {
- let callback = callback.clone();
- div.on_action(
- cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
- )
- }));
- self
- }
-
- fn add_workspace_actions_listeners(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
- let mut div = div
- .on_action(cx.listener(Self::close_inactive_items_and_panes))
- .on_action(cx.listener(Self::close_all_items_and_panes))
- .on_action(cx.listener(Self::add_folder_to_project))
- .on_action(cx.listener(Self::save_all))
- .on_action(cx.listener(Self::open));
- for action in self.workspace_actions.iter() {
- div = (action)(div, cx)
- }
- div
- }
-
- pub fn active_modal<V: ManagedView + 'static>(
- &mut self,
- cx: &ViewContext<Self>,
- ) -> Option<View<V>> {
- self.modal_layer.read(cx).active_modal()
- }
-
- pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut ViewContext<Self>, build: B)
- where
- B: FnOnce(&mut ViewContext<V>) -> V,
- {
- self.modal_layer
- .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
- }
-}
-
-fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
- let display_origin = cx
- .update(|cx| Some(cx.displays().first()?.bounds().origin))
- .ok()??;
- ZED_WINDOW_POSITION
- .zip(*ZED_WINDOW_SIZE)
- .map(|(position, size)| {
- WindowBounds::Fixed(Bounds {
- origin: display_origin + position,
- size,
- })
- })
-}
-
-fn open_items(
- serialized_workspace: Option<SerializedWorkspace>,
- mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
- app_state: Arc<AppState>,
- cx: &mut ViewContext<Workspace>,
-) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
- let restored_items = serialized_workspace.map(|serialized_workspace| {
- Workspace::load_workspace(
- serialized_workspace,
- project_paths_to_open
- .iter()
- .map(|(_, project_path)| project_path)
- .cloned()
- .collect(),
- cx,
- )
- });
-
- cx.spawn(|workspace, mut cx| async move {
- let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
-
- if let Some(restored_items) = restored_items {
- let restored_items = restored_items.await?;
-
- let restored_project_paths = restored_items
- .iter()
- .filter_map(|item| {
- cx.update(|_, cx| item.as_ref()?.project_path(cx))
- .ok()
- .flatten()
- })
- .collect::<HashSet<_>>();
-
- for restored_item in restored_items {
- opened_items.push(restored_item.map(Ok));
- }
-
- project_paths_to_open
- .iter_mut()
- .for_each(|(_, project_path)| {
- if let Some(project_path_to_open) = project_path {
- if restored_project_paths.contains(project_path_to_open) {
- *project_path = None;
- }
- }
- });
- } else {
- for _ in 0..project_paths_to_open.len() {
- opened_items.push(None);
- }
- }
- assert!(opened_items.len() == project_paths_to_open.len());
-
- let tasks =
- project_paths_to_open
- .into_iter()
- .enumerate()
- .map(|(i, (abs_path, project_path))| {
- let workspace = workspace.clone();
- cx.spawn(|mut cx| {
- let fs = app_state.fs.clone();
- async move {
- let file_project_path = project_path?;
- if fs.is_file(&abs_path).await {
- Some((
- i,
- workspace
- .update(&mut cx, |workspace, cx| {
- workspace.open_path(file_project_path, None, true, cx)
- })
- .log_err()?
- .await,
- ))
- } else {
- None
- }
- }
- })
- });
-
- let tasks = tasks.collect::<Vec<_>>();
-
- let tasks = futures::future::join_all(tasks.into_iter());
- for maybe_opened_path in tasks.await.into_iter() {
- if let Some((i, path_open_result)) = maybe_opened_path {
- opened_items[i] = Some(path_open_result);
- }
- }
-
- Ok(opened_items)
- })
-}
-
-fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
- const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
-
- workspace
- .update(cx, |workspace, cx| {
- if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
- workspace.show_notification_once(0, cx, |cx| {
- cx.new_view(|_| {
- MessageNotification::new("Failed to load the database file.")
- .with_click_message("Click to let us know about this error")
- .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
- })
- });
- }
- })
- .log_err();
-}
-
-impl FocusableView for Workspace {
- fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
- self.active_pane.focus_handle(cx)
- }
-}
-
-#[derive(Clone, Render)]
-struct DraggedDock(DockPosition);
-
-impl Render for Workspace {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- let mut context = KeyContext::default();
- context.add("Workspace");
-
- let (ui_font, ui_font_size) = {
- let theme_settings = ThemeSettings::get_global(cx);
- (
- theme_settings.ui_font.family.clone(),
- theme_settings.ui_font_size.clone(),
- )
- };
-
- let theme = cx.theme().clone();
- let colors = theme.colors();
- cx.set_rem_size(ui_font_size);
-
- self.actions(div(), cx)
- .key_context(context)
- .relative()
- .size_full()
- .flex()
- .flex_col()
- .font(ui_font)
- .gap_0()
- .justify_start()
- .items_start()
- .text_color(colors.text)
- .bg(colors.background)
- .border()
- .border_color(colors.border)
- .children(self.titlebar_item.clone())
- .child(
- div()
- .id("workspace")
- .relative()
- .flex_1()
- .w_full()
- .flex()
- .flex_col()
- .overflow_hidden()
- .border_t()
- .border_b()
- .border_color(colors.border)
- .child(
- canvas(cx.listener(|workspace, bounds, _| {
- workspace.bounds = *bounds;
- }))
- .absolute()
- .size_full(),
- )
- .on_drag_move(
- cx.listener(|workspace, e: &DragMoveEvent<DraggedDock>, cx| {
- match e.drag(cx).0 {
- DockPosition::Left => {
- let size = workspace.bounds.left() + e.event.position.x;
- workspace.left_dock.update(cx, |left_dock, cx| {
- left_dock.resize_active_panel(Some(size), cx);
- });
- }
- DockPosition::Right => {
- let size = workspace.bounds.right() - e.event.position.x;
- workspace.right_dock.update(cx, |right_dock, cx| {
- right_dock.resize_active_panel(Some(size), cx);
- });
- }
- DockPosition::Bottom => {
- let size = workspace.bounds.bottom() - e.event.position.y;
- workspace.bottom_dock.update(cx, |bottom_dock, cx| {
- bottom_dock.resize_active_panel(Some(size), cx);
- });
- }
- }
- }),
- )
- .child(self.modal_layer.clone())
- .child(
- div()
- .flex()
- .flex_row()
- .h_full()
- // Left Dock
- .children(self.zoomed_position.ne(&Some(DockPosition::Left)).then(
- || {
- div()
- .flex()
- .flex_none()
- .overflow_hidden()
- .child(self.left_dock.clone())
- },
- ))
- // Panes
- .child(
- div()
- .flex()
- .flex_col()
- .flex_1()
- .overflow_hidden()
- .child(self.center.render(
- &self.project,
- &self.follower_states,
- self.active_call(),
- &self.active_pane,
- self.zoomed.as_ref(),
- &self.app_state,
- cx,
- ))
- .children(
- self.zoomed_position
- .ne(&Some(DockPosition::Bottom))
- .then(|| self.bottom_dock.clone()),
- ),
- )
- // Right Dock
- .children(self.zoomed_position.ne(&Some(DockPosition::Right)).then(
- || {
- div()
- .flex()
- .flex_none()
- .overflow_hidden()
- .child(self.right_dock.clone())
- },
- )),
- )
- .children(self.render_notifications(cx))
- .children(self.zoomed.as_ref().and_then(|view| {
- let zoomed_view = view.upgrade()?;
- let div = div()
- .z_index(1)
- .absolute()
- .overflow_hidden()
- .border_color(colors.border)
- .bg(colors.background)
- .child(zoomed_view)
- .inset_0()
- .shadow_lg();
-
- Some(match self.zoomed_position {
- Some(DockPosition::Left) => div.right_2().border_r(),
- Some(DockPosition::Right) => div.left_2().border_l(),
- Some(DockPosition::Bottom) => div.top_2().border_t(),
- None => div.top_2().bottom_2().left_2().right_2().border(),
- })
- })),
- )
- .child(self.status_bar.clone())
- .children(if self.project.read(cx).is_read_only() {
- Some(DisconnectedOverlay)
- } else {
- None
- })
- }
-}
-
-impl WorkspaceStore {
- pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
- Self {
- workspaces: Default::default(),
- followers: Default::default(),
- _subscriptions: vec![
- client.add_request_handler(cx.weak_model(), Self::handle_follow),
- client.add_message_handler(cx.weak_model(), Self::handle_unfollow),
- client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
- ],
- client,
- }
- }
-
- pub fn update_followers(
- &self,
- project_id: Option<u64>,
- update: proto::update_followers::Variant,
- cx: &AppContext,
- ) -> Option<()> {
- if !cx.has_global::<Model<ActiveCall>>() {
- return None;
- }
-
- let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id();
- let follower_ids: Vec<_> = self
- .followers
- .iter()
- .filter_map(|follower| {
- if follower.project_id == project_id || project_id.is_none() {
- Some(follower.peer_id.into())
- } else {
- None
- }
- })
- .collect();
- if follower_ids.is_empty() {
- return None;
- }
- self.client
- .send(proto::UpdateFollowers {
- room_id,
- project_id,
- follower_ids,
- variant: Some(update),
- })
- .log_err()
- }
-
- pub async fn handle_follow(
- this: Model<Self>,
- envelope: TypedEnvelope<proto::Follow>,
- _: Arc<Client>,
- mut cx: AsyncAppContext,
- ) -> Result<proto::FollowResponse> {
- this.update(&mut cx, |this, cx| {
- let follower = Follower {
- project_id: envelope.payload.project_id,
- peer_id: envelope.original_sender_id()?,
- };
- let active_project = ActiveCall::global(cx).read(cx).location().cloned();
-
- let mut response = proto::FollowResponse::default();
- this.workspaces.retain(|workspace| {
- workspace
- .update(cx, |workspace, cx| {
- let handler_response = workspace.handle_follow(follower.project_id, cx);
- if response.views.is_empty() {
- response.views = handler_response.views;
- } else {
- response.views.extend_from_slice(&handler_response.views);
- }
-
- if let Some(active_view_id) = handler_response.active_view_id.clone() {
- if response.active_view_id.is_none()
- || Some(workspace.project.downgrade()) == active_project
- {
- response.active_view_id = Some(active_view_id);
- }
- }
- })
- .is_ok()
- });
-
- if let Err(ix) = this.followers.binary_search(&follower) {
- this.followers.insert(ix, follower);
- }
-
- Ok(response)
- })?
- }
-
- async fn handle_unfollow(
- model: Model<Self>,
- envelope: TypedEnvelope<proto::Unfollow>,
- _: Arc<Client>,
- mut cx: AsyncAppContext,
- ) -> Result<()> {
- model.update(&mut cx, |this, _| {
- let follower = Follower {
- project_id: envelope.payload.project_id,
- peer_id: envelope.original_sender_id()?,
- };
- if let Ok(ix) = this.followers.binary_search(&follower) {
- this.followers.remove(ix);
- }
- Ok(())
- })?
- }
-
- async fn handle_update_followers(
- this: Model<Self>,
- envelope: TypedEnvelope<proto::UpdateFollowers>,
- _: Arc<Client>,
- mut cx: AsyncAppContext,
- ) -> Result<()> {
- let leader_id = envelope.original_sender_id()?;
- let update = envelope.payload;
-
- this.update(&mut cx, |this, cx| {
- this.workspaces.retain(|workspace| {
- workspace
- .update(cx, |workspace, cx| {
- let project_id = workspace.project.read(cx).remote_id();
- if update.project_id != project_id && update.project_id.is_some() {
- return;
- }
- workspace.handle_update_followers(leader_id, update.clone(), cx);
- })
- .is_ok()
- });
- Ok(())
- })?
- }
-}
-
-impl ViewId {
- pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
- Ok(Self {
- creator: message
- .creator
- .ok_or_else(|| anyhow!("creator is missing"))?,
- id: message.id,
- })
- }
-
- pub(crate) fn to_proto(&self) -> proto::ViewId {
- proto::ViewId {
- creator: Some(self.creator),
- id: self.id,
- }
- }
-}
-
-pub trait WorkspaceHandle {
- fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
-}
-
-impl WorkspaceHandle for View<Workspace> {
- fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
- self.read(cx)
- .worktrees(cx)
- .flat_map(|worktree| {
- let worktree_id = worktree.read(cx).id();
- worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
- worktree_id,
- path: f.path.clone(),
- })
- })
- .collect::<Vec<_>>()
- }
-}
-
-impl std::fmt::Debug for OpenPaths {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.debug_struct("OpenPaths")
- .field("paths", &self.paths)
- .finish()
- }
-}
-
-pub fn activate_workspace_for_project(
- cx: &mut AppContext,
- predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
-) -> Option<WindowHandle<Workspace>> {
- for window in cx.windows() {
- let Some(workspace) = window.downcast::<Workspace>() else {
- continue;
- };
-
- let predicate = workspace
- .update(cx, |workspace, cx| {
- let project = workspace.project.read(cx);
- if predicate(project, cx) {
- cx.activate_window();
- true
- } else {
- false
- }
- })
- .log_err()
- .unwrap_or(false);
-
- if predicate {
- return Some(workspace);
- }
- }
-
- None
-}
-
-pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
- DB.last_workspace().await.log_err().flatten()
-}
-
-async fn join_channel_internal(
- channel_id: u64,
- app_state: &Arc<AppState>,
- requesting_window: Option<WindowHandle<Workspace>>,
- active_call: &Model<ActiveCall>,
- cx: &mut AsyncAppContext,
-) -> Result<bool> {
- let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
- let Some(room) = active_call.room().map(|room| room.read(cx)) else {
- return (false, None);
- };
-
- let already_in_channel = room.channel_id() == Some(channel_id);
- let should_prompt = room.is_sharing_project()
- && room.remote_participants().len() > 0
- && !already_in_channel;
- let open_room = if already_in_channel {
- active_call.room().cloned()
- } else {
- None
- };
- (should_prompt, open_room)
- })?;
-
- if let Some(room) = open_room {
- let task = room.update(cx, |room, cx| {
- if let Some((project, host)) = room.most_active_project(cx) {
- return Some(join_remote_project(project, host, app_state.clone(), cx));
- }
-
- None
- })?;
- if let Some(task) = task {
- task.await?;
- }
- return anyhow::Ok(true);
- }
-
- if should_prompt {
- if let Some(workspace) = requesting_window {
- let answer = workspace.update(cx, |_, cx| {
- cx.prompt(
- PromptLevel::Warning,
- "Leaving this call will unshare your current project.\nDo you want to switch channels?",
- &["Yes, Join Channel", "Cancel"],
- )
- })?.await;
-
- if answer == Ok(1) {
- return Ok(false);
- }
- } else {
- return Ok(false); // unreachable!() hopefully
- }
- }
-
- let client = cx.update(|cx| active_call.read(cx).client())?;
-
- let mut client_status = client.status();
-
- // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
- 'outer: loop {
- let Some(status) = client_status.recv().await else {
- return Err(anyhow!("error connecting"));
- };
-
- match status {
- Status::Connecting
- | Status::Authenticating
- | Status::Reconnecting
- | Status::Reauthenticating => continue,
- Status::Connected { .. } => break 'outer,
- Status::SignedOut => return Err(anyhow!("not signed in")),
- Status::UpgradeRequired => return Err(anyhow!("zed is out of date")),
- Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
- return Err(anyhow!("zed is offline"))
- }
- }
- }
-
- let room = active_call
- .update(cx, |active_call, cx| {
- active_call.join_channel(channel_id, cx)
- })?
- .await?;
-
- let Some(room) = room else {
- return anyhow::Ok(true);
- };
-
- room.update(cx, |room, _| room.room_update_completed())?
- .await;
-
- let task = room.update(cx, |room, cx| {
- if let Some((project, host)) = room.most_active_project(cx) {
- return Some(join_remote_project(project, host, app_state.clone(), cx));
- }
-
- None
- })?;
- if let Some(task) = task {
- task.await?;
- return anyhow::Ok(true);
- }
- anyhow::Ok(false)
-}
-
-pub fn join_channel(
- channel_id: u64,
- app_state: Arc<AppState>,
- requesting_window: Option<WindowHandle<Workspace>>,
- cx: &mut AppContext,
-) -> Task<Result<()>> {
- let active_call = ActiveCall::global(cx);
- cx.spawn(|mut cx| async move {
- let result = join_channel_internal(
- channel_id,
- &app_state,
- requesting_window,
- &active_call,
- &mut cx,
- )
- .await;
-
- // join channel succeeded, and opened a window
- if matches!(result, Ok(true)) {
- return anyhow::Ok(());
- }
-
- if requesting_window.is_some() {
- return anyhow::Ok(());
- }
-
- // find an existing workspace to focus and show call controls
- let mut active_window = activate_any_workspace_window(&mut cx);
- if active_window.is_none() {
- // no open workspaces, make one to show the error in (blergh)
- cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), requesting_window, cx))?
- .await?;
- }
-
- active_window = activate_any_workspace_window(&mut cx);
- let Some(active_window) = active_window else {
- return anyhow::Ok(());
- };
-
- if let Err(err) = result {
- active_window
- .update(&mut cx, |_, cx| {
- cx.prompt(
- PromptLevel::Critical,
- &format!("Failed to join channel: {}", err),
- &["Ok"],
- )
- })?
- .await
- .ok();
- }
-
- // return ok, we showed the error to the user.
- return anyhow::Ok(());
- })
-}
-
-pub async fn get_any_active_workspace(
- app_state: Arc<AppState>,
- mut cx: AsyncAppContext,
-) -> anyhow::Result<WindowHandle<Workspace>> {
- // find an existing workspace to focus and show call controls
- let active_window = activate_any_workspace_window(&mut cx);
- if active_window.is_none() {
- cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx))?
- .await?;
- }
- activate_any_workspace_window(&mut cx)
- .context("could not open zed")?
- .downcast::<Workspace>()
- .context("could not open zed workspace window")
-}
-
-fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<AnyWindowHandle> {
- cx.update(|cx| {
- for window in cx.windows() {
- let is_workspace = window.downcast::<Workspace>().is_some();
- if is_workspace {
- window.update(cx, |_, cx| cx.activate_window()).ok();
- return Some(window);
- }
- }
- None
- })
- .ok()
- .flatten()
-}
-
-#[allow(clippy::type_complexity)]
-pub fn open_paths(
- abs_paths: &[PathBuf],
- app_state: &Arc<AppState>,
- requesting_window: Option<WindowHandle<Workspace>>,
- cx: &mut AppContext,
-) -> Task<
- anyhow::Result<(
- WindowHandle<Workspace>,
- Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
- )>,
-> {
- let app_state = app_state.clone();
- let abs_paths = abs_paths.to_vec();
- // Open paths in existing workspace if possible
- let existing = activate_workspace_for_project(cx, {
- let abs_paths = abs_paths.clone();
- move |project, cx| project.contains_paths(&abs_paths, cx)
- });
- cx.spawn(move |mut cx| async move {
- if let Some(existing) = existing {
- Ok((
- existing.clone(),
- existing
- .update(&mut cx, |workspace, cx| {
- workspace.open_paths(abs_paths, true, cx)
- })?
- .await,
- ))
- } else {
- cx.update(move |cx| {
- Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
- })?
- .await
- }
- })
-}
-
-pub fn open_new(
- app_state: &Arc<AppState>,
- cx: &mut AppContext,
- init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
-) -> Task<()> {
- let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
- cx.spawn(|mut cx| async move {
- if let Some((workspace, opened_paths)) = task.await.log_err() {
- workspace
- .update(&mut cx, |workspace, cx| {
- if opened_paths.is_empty() {
- init(workspace, cx)
- }
- })
- .log_err();
- }
- })
-}
-
-pub fn create_and_open_local_file(
- path: &'static Path,
- cx: &mut ViewContext<Workspace>,
- default_content: impl 'static + Send + FnOnce() -> Rope,
-) -> Task<Result<Box<dyn ItemHandle>>> {
- cx.spawn(|workspace, mut cx| async move {
- let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
- if !fs.is_file(path).await {
- fs.create_file(path, Default::default()).await?;
- fs.save(path, &default_content(), Default::default())
- .await?;
- }
-
- let mut items = workspace
- .update(&mut cx, |workspace, cx| {
- workspace.with_local_workspace(cx, |workspace, cx| {
- workspace.open_paths(vec![path.to_path_buf()], false, cx)
- })
- })?
- .await?
- .await;
-
- let item = items.pop().flatten();
- item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
- })
-}
-
-pub fn join_remote_project(
- project_id: u64,
- follow_user_id: u64,
- app_state: Arc<AppState>,
- cx: &mut AppContext,
-) -> Task<Result<()>> {
- let windows = cx.windows();
- cx.spawn(|mut cx| async move {
- let existing_workspace = windows.into_iter().find_map(|window| {
- window.downcast::<Workspace>().and_then(|window| {
- window
- .update(&mut cx, |workspace, cx| {
- if workspace.project().read(cx).remote_id() == Some(project_id) {
- Some(window)
- } else {
- None
- }
- })
- .unwrap_or(None)
- })
- });
-
- let workspace = if let Some(existing_workspace) = existing_workspace {
- existing_workspace
- } else {
- let active_call = cx.update(|cx| ActiveCall::global(cx))?;
- let room = active_call
- .read_with(&cx, |call, _| call.room().cloned())?
- .ok_or_else(|| anyhow!("not in a call"))?;
- let project = room
- .update(&mut cx, |room, cx| {
- room.join_project(
- project_id,
- app_state.languages.clone(),
- app_state.fs.clone(),
- cx,
- )
- })?
- .await?;
-
- let window_bounds_override = window_bounds_env_override(&cx);
- cx.update(|cx| {
- let options = (app_state.build_window_options)(window_bounds_override, None, cx);
- cx.open_window(options, |cx| {
- cx.new_view(|cx| Workspace::new(0, project, app_state.clone(), cx))
- })
- })?
- };
-
- workspace.update(&mut cx, |workspace, cx| {
- cx.activate(true);
- cx.activate_window();
-
- if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
- let follow_peer_id = room
- .read(cx)
- .remote_participants()
- .iter()
- .find(|(_, participant)| participant.user.id == follow_user_id)
- .map(|(_, p)| p.peer_id)
- .or_else(|| {
- // If we couldn't follow the given user, follow the host instead.
- let collaborator = workspace
- .project()
- .read(cx)
- .collaborators()
- .values()
- .find(|collaborator| collaborator.replica_id == 0)?;
- Some(collaborator.peer_id)
- });
-
- if let Some(follow_peer_id) = follow_peer_id {
- workspace.follow(follow_peer_id, cx);
- }
- }
- })?;
-
- anyhow::Ok(())
- })
-}
-
-pub fn restart(_: &Restart, cx: &mut AppContext) {
- let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
- let mut workspace_windows = cx
- .windows()
- .into_iter()
- .filter_map(|window| window.downcast::<Workspace>())
- .collect::<Vec<_>>();
-
- // If multiple windows have unsaved changes, and need a save prompt,
- // prompt in the active window before switching to a different window.
- workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
-
- let mut prompt = None;
- if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
- prompt = window
- .update(cx, |_, cx| {
- cx.prompt(
- PromptLevel::Info,
- "Are you sure you want to restart?",
- &["Restart", "Cancel"],
- )
- })
- .ok();
- }
-
- cx.spawn(|mut cx| async move {
- if let Some(prompt) = prompt {
- let answer = prompt.await?;
- if answer != 0 {
- return Ok(());
- }
- }
-
- // If the user cancels any save prompt, then keep the app open.
- for window in workspace_windows {
- if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
- workspace.prepare_to_close(true, cx)
- }) {
- if !should_close.await? {
- return Ok(());
- }
- }
- }
-
- cx.update(|cx| cx.restart())
- })
- .detach_and_log_err(cx);
-}
-
-fn parse_pixel_position_env_var(value: &str) -> Option<Point<GlobalPixels>> {
- let mut parts = value.split(',');
- let x: usize = parts.next()?.parse().ok()?;
- let y: usize = parts.next()?.parse().ok()?;
- Some(point((x as f64).into(), (y as f64).into()))
-}
-
-fn parse_pixel_size_env_var(value: &str) -> Option<Size<GlobalPixels>> {
- let mut parts = value.split(',');
- let width: usize = parts.next()?.parse().ok()?;
- let height: usize = parts.next()?.parse().ok()?;
- Some(size((width as f64).into(), (height as f64).into()))
-}
-
-struct DisconnectedOverlay;
-
-impl Element for DisconnectedOverlay {
- type State = AnyElement;
-
- fn request_layout(
- &mut self,
- _: Option<Self::State>,
- cx: &mut WindowContext,
- ) -> (LayoutId, Self::State) {
- let mut background = cx.theme().colors().elevated_surface_background;
- background.fade_out(0.2);
- let mut overlay = div()
- .bg(background)
- .absolute()
- .left_0()
- .top_0()
- .size_full()
- .flex()
- .items_center()
- .justify_center()
- .capture_any_mouse_down(|_, cx| cx.stop_propagation())
- .capture_any_mouse_up(|_, cx| cx.stop_propagation())
- .child(Label::new(
- "Your connection to the remote project has been lost.",
- ))
- .into_any();
- (overlay.request_layout(cx), overlay)
- }
-
- fn paint(&mut self, bounds: Bounds<Pixels>, overlay: &mut Self::State, cx: &mut WindowContext) {
- cx.with_z_index(u8::MAX, |cx| {
- cx.add_opaque_layer(bounds);
- overlay.paint(cx);
- })
- }
-}
-
-impl IntoElement for DisconnectedOverlay {
- type Element = Self;
-
- fn element_id(&self) -> Option<ui::prelude::ElementId> {
- None
- }
-
- fn into_element(self) -> Self::Element {
- self
- }
-}
-
-#[cfg(test)]
-mod tests {
- use std::{cell::RefCell, rc::Rc};
-
- use super::*;
- use crate::item::{
- test::{TestItem, TestProjectItem},
- ItemEvent,
- };
- use fs::FakeFs;
- use gpui::TestAppContext;
- use project::{Project, ProjectEntryId};
- use serde_json::json;
- use settings::SettingsStore;
-
- #[gpui::test]
- async fn test_tab_disambiguation(cx: &mut TestAppContext) {
- init_test(cx);
-
- let fs = FakeFs::new(cx.executor());
- let project = Project::test(fs, [], cx).await;
- let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
-
- // Adding an item with no ambiguity renders the tab without detail.
- let item1 = cx.new_view(|cx| {
- let mut item = TestItem::new(cx);
- item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
- item
- });
- workspace.update(cx, |workspace, cx| {
- workspace.add_item(Box::new(item1.clone()), cx);
- });
- item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
-
- // Adding an item that creates ambiguity increases the level of detail on
- // both tabs.
- let item2 = cx.new_view(|cx| {
- let mut item = TestItem::new(cx);
- item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
- item
- });
- workspace.update(cx, |workspace, cx| {
- workspace.add_item(Box::new(item2.clone()), cx);
- });
- item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
- item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
-
- // Adding an item that creates ambiguity increases the level of detail only
- // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
- // we stop at the highest detail available.
- let item3 = cx.new_view(|cx| {
- let mut item = TestItem::new(cx);
- item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
- item
- });
- workspace.update(cx, |workspace, cx| {
- workspace.add_item(Box::new(item3.clone()), cx);
- });
- item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
- item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
- item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
- }
-
- #[gpui::test]
- async fn test_tracking_active_path(cx: &mut TestAppContext) {
- init_test(cx);
-
- let fs = FakeFs::new(cx.executor());
- fs.insert_tree(
- "/root1",
- json!({
- "one.txt": "",
- "two.txt": "",
- }),
- )
- .await;
- fs.insert_tree(
- "/root2",
- json!({
- "three.txt": "",
- }),
- )
- .await;
-
- let project = Project::test(fs, ["root1".as_ref()], cx).await;
- let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
- let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
- let worktree_id = project.update(cx, |project, cx| {
- project.worktrees().next().unwrap().read(cx).id()
- });
-
- let item1 = cx.new_view(|cx| {
- TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
- });
- let item2 = cx.new_view(|cx| {
- TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
- });
-
- // Add an item to an empty pane
- workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
- project.update(cx, |project, cx| {
- assert_eq!(
- project.active_entry(),
- project
- .entry_for_path(&(worktree_id, "one.txt").into(), cx)
- .map(|e| e.id)
- );
- });
- assert_eq!(cx.window_title().as_deref(), Some("one.txt β root1"));
-
- // Add a second item to a non-empty pane
- workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
- assert_eq!(cx.window_title().as_deref(), Some("two.txt β root1"));
- project.update(cx, |project, cx| {
- assert_eq!(
- project.active_entry(),
- project
- .entry_for_path(&(worktree_id, "two.txt").into(), cx)
- .map(|e| e.id)
- );
- });
-
- // Close the active item
- pane.update(cx, |pane, cx| {
- pane.close_active_item(&Default::default(), cx).unwrap()
- })
- .await
- .unwrap();
- assert_eq!(cx.window_title().as_deref(), Some("one.txt β root1"));
- project.update(cx, |project, cx| {
- assert_eq!(
- project.active_entry(),
- project
- .entry_for_path(&(worktree_id, "one.txt").into(), cx)
- .map(|e| e.id)
- );
- });
-
- // Add a project folder
- project
- .update(cx, |project, cx| {
- project.find_or_create_local_worktree("/root2", true, cx)
- })
- .await
- .unwrap();
- assert_eq!(cx.window_title().as_deref(), Some("one.txt β root1, root2"));
-
- // Remove a project folder
- project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
- assert_eq!(cx.window_title().as_deref(), Some("one.txt β root2"));
- }
-
- #[gpui::test]
- async fn test_close_window(cx: &mut TestAppContext) {
- init_test(cx);
-
- let fs = FakeFs::new(cx.executor());
- fs.insert_tree("/root", json!({ "one": "" })).await;
-
- let project = Project::test(fs, ["root".as_ref()], cx).await;
- let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
-
- // When there are no dirty items, there's nothing to do.
- let item1 = cx.new_view(|cx| TestItem::new(cx));
- workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
- let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
- assert!(task.await.unwrap());
-
- // When there are dirty untitled items, prompt to save each one. If the user
- // cancels any prompt, then abort.
- let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
- let item3 = cx.new_view(|cx| {
- TestItem::new(cx)
- .with_dirty(true)
- .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
- });
- workspace.update(cx, |w, cx| {
- w.add_item(Box::new(item2.clone()), cx);
- w.add_item(Box::new(item3.clone()), cx);
- });
- let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
- cx.executor().run_until_parked();
- cx.simulate_prompt_answer(2); // cancel save all
- cx.executor().run_until_parked();
- cx.simulate_prompt_answer(2); // cancel save all
- cx.executor().run_until_parked();
- assert!(!cx.has_pending_prompt());
- assert!(!task.await.unwrap());
- }
-
- #[gpui::test]
- async fn test_close_pane_items(cx: &mut TestAppContext) {
- init_test(cx);
-
- let fs = FakeFs::new(cx.executor());
-
- let project = Project::test(fs, None, cx).await;
- let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
-
- let item1 = cx.new_view(|cx| {
- TestItem::new(cx)
- .with_dirty(true)
- .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
- });
- let item2 = cx.new_view(|cx| {
- TestItem::new(cx)
- .with_dirty(true)
- .with_conflict(true)
- .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
- });
- let item3 = cx.new_view(|cx| {
- TestItem::new(cx)
- .with_dirty(true)
- .with_conflict(true)
- .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
- });
- let item4 = cx.new_view(|cx| {
- TestItem::new(cx)
- .with_dirty(true)
- .with_project_items(&[TestProjectItem::new_untitled(cx)])
- });
- let pane = workspace.update(cx, |workspace, cx| {
- workspace.add_item(Box::new(item1.clone()), cx);
- workspace.add_item(Box::new(item2.clone()), cx);
- workspace.add_item(Box::new(item3.clone()), cx);
- workspace.add_item(Box::new(item4.clone()), cx);
- workspace.active_pane().clone()
- });
-
- let close_items = pane.update(cx, |pane, cx| {
- pane.activate_item(1, true, true, cx);
- assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
- let item1_id = item1.item_id();
- let item3_id = item3.item_id();
- let item4_id = item4.item_id();
- pane.close_items(cx, SaveIntent::Close, move |id| {
- [item1_id, item3_id, item4_id].contains(&id)
- })
- });
- cx.executor().run_until_parked();
-
- assert!(cx.has_pending_prompt());
- // Ignore "Save all" prompt
- cx.simulate_prompt_answer(2);
- cx.executor().run_until_parked();
- // There's a prompt to save item 1.
- pane.update(cx, |pane, _| {
- assert_eq!(pane.items_len(), 4);
- assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
- });
- // Confirm saving item 1.
- cx.simulate_prompt_answer(0);
- cx.executor().run_until_parked();
-
- // Item 1 is saved. There's a prompt to save item 3.
- pane.update(cx, |pane, cx| {
- assert_eq!(item1.read(cx).save_count, 1);
- assert_eq!(item1.read(cx).save_as_count, 0);
- assert_eq!(item1.read(cx).reload_count, 0);
- assert_eq!(pane.items_len(), 3);
- assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
- });
- assert!(cx.has_pending_prompt());
-
- // Cancel saving item 3.
- cx.simulate_prompt_answer(1);
- cx.executor().run_until_parked();
-
- // Item 3 is reloaded. There's a prompt to save item 4.
- pane.update(cx, |pane, cx| {
- assert_eq!(item3.read(cx).save_count, 0);
- assert_eq!(item3.read(cx).save_as_count, 0);
- assert_eq!(item3.read(cx).reload_count, 1);
- assert_eq!(pane.items_len(), 2);
- assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
- });
- assert!(cx.has_pending_prompt());
-
- // Confirm saving item 4.
- cx.simulate_prompt_answer(0);
- cx.executor().run_until_parked();
-
- // There's a prompt for a path for item 4.
- cx.simulate_new_path_selection(|_| Some(Default::default()));
- close_items.await.unwrap();
-
- // The requested items are closed.
- pane.update(cx, |pane, cx| {
- assert_eq!(item4.read(cx).save_count, 0);
- assert_eq!(item4.read(cx).save_as_count, 1);
- assert_eq!(item4.read(cx).reload_count, 0);
- assert_eq!(pane.items_len(), 1);
- assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
- });
- }
-
- #[gpui::test]
- async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
- init_test(cx);
-
- let fs = FakeFs::new(cx.executor());
- let project = Project::test(fs, [], cx).await;
- let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
-
- // Create several workspace items with single project entries, and two
- // workspace items with multiple project entries.
- let single_entry_items = (0..=4)
- .map(|project_entry_id| {
- cx.new_view(|cx| {
- TestItem::new(cx)
- .with_dirty(true)
- .with_project_items(&[TestProjectItem::new(
- project_entry_id,
- &format!("{project_entry_id}.txt"),
- cx,
- )])
- })
- })
- .collect::<Vec<_>>();
- let item_2_3 = cx.new_view(|cx| {
- TestItem::new(cx)
- .with_dirty(true)
- .with_singleton(false)
- .with_project_items(&[
- single_entry_items[2].read(cx).project_items[0].clone(),
- single_entry_items[3].read(cx).project_items[0].clone(),
- ])
- });
- let item_3_4 = cx.new_view(|cx| {
- TestItem::new(cx)
- .with_dirty(true)
- .with_singleton(false)
- .with_project_items(&[
- single_entry_items[3].read(cx).project_items[0].clone(),
- single_entry_items[4].read(cx).project_items[0].clone(),
- ])
- });
-
- // Create two panes that contain the following project entries:
- // left pane:
- // multi-entry items: (2, 3)
- // single-entry items: 0, 1, 2, 3, 4
- // right pane:
- // single-entry items: 1
- // multi-entry items: (3, 4)
- let left_pane = workspace.update(cx, |workspace, cx| {
- let left_pane = workspace.active_pane().clone();
- workspace.add_item(Box::new(item_2_3.clone()), cx);
- for item in single_entry_items {
- workspace.add_item(Box::new(item), cx);
- }
- left_pane.update(cx, |pane, cx| {
- pane.activate_item(2, true, true, cx);
- });
-
- let right_pane = workspace
- .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
- .unwrap();
-
- right_pane.update(cx, |pane, cx| {
- pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
- });
-
- left_pane
- });
-
- cx.focus_view(&left_pane);
-
- // When closing all of the items in the left pane, we should be prompted twice:
- // once for project entry 0, and once for project entry 2. Project entries 1,
- // 3, and 4 are all still open in the other paten. After those two
- // prompts, the task should complete.
-
- let close = left_pane.update(cx, |pane, cx| {
- pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
- });
- cx.executor().run_until_parked();
-
- // Discard "Save all" prompt
- cx.simulate_prompt_answer(2);
-
- cx.executor().run_until_parked();
- left_pane.update(cx, |pane, cx| {
- assert_eq!(
- pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
- &[ProjectEntryId::from_proto(0)]
- );
- });
- cx.simulate_prompt_answer(0);
-
- cx.executor().run_until_parked();
- left_pane.update(cx, |pane, cx| {
- assert_eq!(
- pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
- &[ProjectEntryId::from_proto(2)]
- );
- });
- cx.simulate_prompt_answer(0);
-
- cx.executor().run_until_parked();
- close.await.unwrap();
- left_pane.update(cx, |pane, _| {
- assert_eq!(pane.items_len(), 0);
- });
- }
-
- #[gpui::test]
- async fn test_autosave(cx: &mut gpui::TestAppContext) {
- init_test(cx);
-
- let fs = FakeFs::new(cx.executor());
- let project = Project::test(fs, [], cx).await;
- let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
- let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
-
- let item = cx.new_view(|cx| {
- TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
- });
- let item_id = item.entity_id();
- workspace.update(cx, |workspace, cx| {
- workspace.add_item(Box::new(item.clone()), cx);
- });
-
- // Autosave on window change.
- item.update(cx, |item, cx| {
- cx.update_global(|settings: &mut SettingsStore, cx| {
- settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
- settings.autosave = Some(AutosaveSetting::OnWindowChange);
- })
- });
- item.is_dirty = true;
- });
-
- // Deactivating the window saves the file.
- cx.simulate_deactivation();
- cx.executor().run_until_parked();
- item.update(cx, |item, _| assert_eq!(item.save_count, 1));
-
- // Autosave on focus change.
- item.update(cx, |item, cx| {
- cx.focus_self();
- cx.update_global(|settings: &mut SettingsStore, cx| {
- settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
- settings.autosave = Some(AutosaveSetting::OnFocusChange);
- })
- });
- item.is_dirty = true;
- });
-
- // Blurring the item saves the file.
- item.update(cx, |_, cx| cx.blur());
- cx.executor().run_until_parked();
- item.update(cx, |item, _| assert_eq!(item.save_count, 2));
-
- // Deactivating the window still saves the file.
- cx.simulate_activation();
- item.update(cx, |item, cx| {
- cx.focus_self();
- item.is_dirty = true;
- });
- cx.simulate_deactivation();
-
- cx.executor().run_until_parked();
- item.update(cx, |item, _| assert_eq!(item.save_count, 3));
-
- // Autosave after delay.
- item.update(cx, |item, cx| {
- cx.update_global(|settings: &mut SettingsStore, cx| {
- settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
- settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
- })
- });
- item.is_dirty = true;
- cx.emit(ItemEvent::Edit);
- });
-
- // Delay hasn't fully expired, so the file is still dirty and unsaved.
- cx.executor().advance_clock(Duration::from_millis(250));
- item.update(cx, |item, _| assert_eq!(item.save_count, 3));
-
- // After delay expires, the file is saved.
- cx.executor().advance_clock(Duration::from_millis(250));
- item.update(cx, |item, _| assert_eq!(item.save_count, 4));
-
- // Autosave on focus change, ensuring closing the tab counts as such.
- item.update(cx, |item, cx| {
- cx.update_global(|settings: &mut SettingsStore, cx| {
- settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
- settings.autosave = Some(AutosaveSetting::OnFocusChange);
- })
- });
- item.is_dirty = true;
- });
-
- pane.update(cx, |pane, cx| {
- pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
- })
- .await
- .unwrap();
- assert!(!cx.has_pending_prompt());
- item.update(cx, |item, _| assert_eq!(item.save_count, 5));
-
- // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
- workspace.update(cx, |workspace, cx| {
- workspace.add_item(Box::new(item.clone()), cx);
- });
- item.update(cx, |item, cx| {
- item.project_items[0].update(cx, |item, _| {
- item.entry_id = None;
- });
- item.is_dirty = true;
- cx.blur();
- });
- cx.run_until_parked();
- item.update(cx, |item, _| assert_eq!(item.save_count, 5));
-
- // Ensure autosave is prevented for deleted files also when closing the buffer.
- let _close_items = pane.update(cx, |pane, cx| {
- pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
- });
- cx.run_until_parked();
- assert!(cx.has_pending_prompt());
- item.update(cx, |item, _| assert_eq!(item.save_count, 5));
- }
-
- #[gpui::test]
- async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
- init_test(cx);
-
- let fs = FakeFs::new(cx.executor());
-
- let project = Project::test(fs, [], cx).await;
- let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
-
- let item = cx.new_view(|cx| {
- TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
- });
- let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
- let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
- let toolbar_notify_count = Rc::new(RefCell::new(0));
-
- workspace.update(cx, |workspace, cx| {
- workspace.add_item(Box::new(item.clone()), cx);
- let toolbar_notification_count = toolbar_notify_count.clone();
- cx.observe(&toolbar, move |_, _, _| {
- *toolbar_notification_count.borrow_mut() += 1
- })
- .detach();
- });
-
- pane.update(cx, |pane, _| {
- assert!(!pane.can_navigate_backward());
- assert!(!pane.can_navigate_forward());
- });
-
- item.update(cx, |item, cx| {
- item.set_state("one".to_string(), cx);
- });
-
- // Toolbar must be notified to re-render the navigation buttons
- assert_eq!(*toolbar_notify_count.borrow(), 1);
-
- pane.update(cx, |pane, _| {
- assert!(pane.can_navigate_backward());
- assert!(!pane.can_navigate_forward());
- });
-
- workspace
- .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
- .await
- .unwrap();
-
- assert_eq!(*toolbar_notify_count.borrow(), 2);
- pane.update(cx, |pane, _| {
- assert!(!pane.can_navigate_backward());
- assert!(pane.can_navigate_forward());
- });
- }
-
- // #[gpui::test]
- // async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
- // init_test(cx);
- // let fs = FakeFs::new(cx.executor());
-
- // let project = Project::test(fs, [], cx).await;
- // let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
-
- // let panel = workspace.update(cx, |workspace, cx| {
- // let panel = cx.build_view(|cx| TestPanel::new(DockPosition::Right, cx));
- // workspace.add_panel(panel.clone(), cx);
-
- // workspace
- // .right_dock()
- // .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
-
- // panel
- // });
-
- // let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
- // pane.update(cx, |pane, cx| {
- // let item = cx.build_view(|cx| TestItem::new(cx));
- // pane.add_item(Box::new(item), true, true, None, cx);
- // });
-
- // // Transfer focus from center to panel
- // workspace.update(cx, |workspace, cx| {
- // workspace.toggle_panel_focus::<TestPanel>(cx);
- // });
-
- // workspace.update(cx, |workspace, cx| {
- // assert!(workspace.right_dock().read(cx).is_open());
- // assert!(!panel.is_zoomed(cx));
- // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
- // });
-
- // // Transfer focus from panel to center
- // workspace.update(cx, |workspace, cx| {
- // workspace.toggle_panel_focus::<TestPanel>(cx);
- // });
-
- // workspace.update(cx, |workspace, cx| {
- // assert!(workspace.right_dock().read(cx).is_open());
- // assert!(!panel.is_zoomed(cx));
- // assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
- // });
-
- // // Close the dock
- // workspace.update(cx, |workspace, cx| {
- // workspace.toggle_dock(DockPosition::Right, cx);
- // });
-
- // workspace.update(cx, |workspace, cx| {
- // assert!(!workspace.right_dock().read(cx).is_open());
- // assert!(!panel.is_zoomed(cx));
- // assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
- // });
-
- // // Open the dock
- // workspace.update(cx, |workspace, cx| {
- // workspace.toggle_dock(DockPosition::Right, cx);
- // });
-
- // workspace.update(cx, |workspace, cx| {
- // assert!(workspace.right_dock().read(cx).is_open());
- // assert!(!panel.is_zoomed(cx));
- // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
- // });
-
- // // Focus and zoom panel
- // panel.update(cx, |panel, cx| {
- // cx.focus_self();
- // panel.set_zoomed(true, cx)
- // });
-
- // workspace.update(cx, |workspace, cx| {
- // assert!(workspace.right_dock().read(cx).is_open());
- // assert!(panel.is_zoomed(cx));
- // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
- // });
-
- // // Transfer focus to the center closes the dock
- // workspace.update(cx, |workspace, cx| {
- // workspace.toggle_panel_focus::<TestPanel>(cx);
- // });
-
- // workspace.update(cx, |workspace, cx| {
- // assert!(!workspace.right_dock().read(cx).is_open());
- // assert!(panel.is_zoomed(cx));
- // assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
- // });
-
- // // Transferring focus back to the panel keeps it zoomed
- // workspace.update(cx, |workspace, cx| {
- // workspace.toggle_panel_focus::<TestPanel>(cx);
- // });
-
- // workspace.update(cx, |workspace, cx| {
- // assert!(workspace.right_dock().read(cx).is_open());
- // assert!(panel.is_zoomed(cx));
- // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
- // });
-
- // // Close the dock while it is zoomed
- // workspace.update(cx, |workspace, cx| {
- // workspace.toggle_dock(DockPosition::Right, cx)
- // });
-
- // workspace.update(cx, |workspace, cx| {
- // assert!(!workspace.right_dock().read(cx).is_open());
- // assert!(panel.is_zoomed(cx));
- // assert!(workspace.zoomed.is_none());
- // assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
- // });
-
- // // Opening the dock, when it's zoomed, retains focus
- // workspace.update(cx, |workspace, cx| {
- // workspace.toggle_dock(DockPosition::Right, cx)
- // });
-
- // workspace.update(cx, |workspace, cx| {
- // assert!(workspace.right_dock().read(cx).is_open());
- // assert!(panel.is_zoomed(cx));
- // assert!(workspace.zoomed.is_some());
- // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
- // });
-
- // // Unzoom and close the panel, zoom the active pane.
- // panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
- // workspace.update(cx, |workspace, cx| {
- // workspace.toggle_dock(DockPosition::Right, cx)
- // });
- // pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
-
- // // Opening a dock unzooms the pane.
- // workspace.update(cx, |workspace, cx| {
- // workspace.toggle_dock(DockPosition::Right, cx)
- // });
- // workspace.update(cx, |workspace, cx| {
- // let pane = pane.read(cx);
- // assert!(!pane.is_zoomed());
- // assert!(!pane.focus_handle(cx).is_focused(cx));
- // assert!(workspace.right_dock().read(cx).is_open());
- // assert!(workspace.zoomed.is_none());
- // });
- // }
-
- // #[gpui::test]
- // async fn test_panels(cx: &mut gpui::TestAppContext) {
- // init_test(cx);
- // let fs = FakeFs::new(cx.executor());
-
- // let project = Project::test(fs, [], cx).await;
- // let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
-
- // let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
- // // Add panel_1 on the left, panel_2 on the right.
- // let panel_1 = cx.build_view(|cx| TestPanel::new(DockPosition::Left, cx));
- // workspace.add_panel(panel_1.clone(), cx);
- // workspace
- // .left_dock()
- // .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
- // let panel_2 = cx.build_view(|cx| TestPanel::new(DockPosition::Right, cx));
- // workspace.add_panel(panel_2.clone(), cx);
- // workspace
- // .right_dock()
- // .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
-
- // let left_dock = workspace.left_dock();
- // assert_eq!(
- // left_dock.read(cx).visible_panel().unwrap().panel_id(),
- // panel_1.panel_id()
- // );
- // assert_eq!(
- // left_dock.read(cx).active_panel_size(cx).unwrap(),
- // panel_1.size(cx)
- // );
-
- // left_dock.update(cx, |left_dock, cx| {
- // left_dock.resize_active_panel(Some(1337.), cx)
- // });
- // assert_eq!(
- // workspace
- // .right_dock()
- // .read(cx)
- // .visible_panel()
- // .unwrap()
- // .panel_id(),
- // panel_2.panel_id(),
- // );
-
- // (panel_1, panel_2)
- // });
-
- // // Move panel_1 to the right
- // panel_1.update(cx, |panel_1, cx| {
- // panel_1.set_position(DockPosition::Right, cx)
- // });
-
- // workspace.update(cx, |workspace, cx| {
- // // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
- // // Since it was the only panel on the left, the left dock should now be closed.
- // assert!(!workspace.left_dock().read(cx).is_open());
- // assert!(workspace.left_dock().read(cx).visible_panel().is_none());
- // let right_dock = workspace.right_dock();
- // assert_eq!(
- // right_dock.read(cx).visible_panel().unwrap().panel_id(),
- // panel_1.panel_id()
- // );
- // assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
-
- // // Now we move panel_2Β to the left
- // panel_2.set_position(DockPosition::Left, cx);
- // });
-
- // workspace.update(cx, |workspace, cx| {
- // // Since panel_2 was not visible on the right, we don't open the left dock.
- // assert!(!workspace.left_dock().read(cx).is_open());
- // // And the right dock is unaffected in it's displaying of panel_1
- // assert!(workspace.right_dock().read(cx).is_open());
- // assert_eq!(
- // workspace
- // .right_dock()
- // .read(cx)
- // .visible_panel()
- // .unwrap()
- // .panel_id(),
- // panel_1.panel_id(),
- // );
- // });
-
- // // Move panel_1 back to the left
- // panel_1.update(cx, |panel_1, cx| {
- // panel_1.set_position(DockPosition::Left, cx)
- // });
-
- // workspace.update(cx, |workspace, cx| {
- // // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
- // let left_dock = workspace.left_dock();
- // assert!(left_dock.read(cx).is_open());
- // assert_eq!(
- // left_dock.read(cx).visible_panel().unwrap().panel_id(),
- // panel_1.panel_id()
- // );
- // assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
- // // And right the dock should be closed as it no longer has any panels.
- // assert!(!workspace.right_dock().read(cx).is_open());
-
- // // Now we move panel_1 to the bottom
- // panel_1.set_position(DockPosition::Bottom, cx);
- // });
-
- // workspace.update(cx, |workspace, cx| {
- // // Since panel_1 was visible on the left, we close the left dock.
- // assert!(!workspace.left_dock().read(cx).is_open());
- // // The bottom dock is sized based on the panel's default size,
- // // since the panel orientation changed from vertical to horizontal.
- // let bottom_dock = workspace.bottom_dock();
- // assert_eq!(
- // bottom_dock.read(cx).active_panel_size(cx).unwrap(),
- // panel_1.size(cx),
- // );
- // // Close bottom dock and move panel_1 back to the left.
- // bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
- // panel_1.set_position(DockPosition::Left, cx);
- // });
-
- // // Emit activated event on panel 1
- // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
-
- // // Now the left dock is open and panel_1 is active and focused.
- // workspace.update(cx, |workspace, cx| {
- // let left_dock = workspace.left_dock();
- // assert!(left_dock.read(cx).is_open());
- // assert_eq!(
- // left_dock.read(cx).visible_panel().unwrap().panel_id(),
- // panel_1.panel_id(),
- // );
- // assert!(panel_1.focus_handle(cx).is_focused(cx));
- // });
-
- // // Emit closed event on panel 2, which is not active
- // panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
-
- // // Wo don't close the left dock, because panel_2 wasn't the active panel
- // workspace.update(cx, |workspace, cx| {
- // let left_dock = workspace.left_dock();
- // assert!(left_dock.read(cx).is_open());
- // assert_eq!(
- // left_dock.read(cx).visible_panel().unwrap().panel_id(),
- // panel_1.panel_id(),
- // );
- // });
-
- // // Emitting a ZoomIn event shows the panel as zoomed.
- // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
- // workspace.update(cx, |workspace, _| {
- // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
- // assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
- // });
-
- // // Move panel to another dock while it is zoomed
- // panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
- // workspace.update(cx, |workspace, _| {
- // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
-
- // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
- // });
-
- // // If focus is transferred to another view that's not a panel or another pane, we still show
- // // the panel as zoomed.
- // let other_focus_handle = cx.update(|cx| cx.focus_handle());
- // cx.update(|cx| cx.focus(&other_focus_handle));
- // workspace.update(cx, |workspace, _| {
- // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
- // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
- // });
-
- // // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
- // workspace.update(cx, |_, cx| cx.focus_self());
- // workspace.update(cx, |workspace, _| {
- // assert_eq!(workspace.zoomed, None);
- // assert_eq!(workspace.zoomed_position, None);
- // });
-
- // // If focus is transferred again to another view that's not a panel or a pane, we won't
- // // show the panel as zoomed because it wasn't zoomed before.
- // cx.update(|cx| cx.focus(&other_focus_handle));
- // workspace.update(cx, |workspace, _| {
- // assert_eq!(workspace.zoomed, None);
- // assert_eq!(workspace.zoomed_position, None);
- // });
-
- // // When focus is transferred back to the panel, it is zoomed again.
- // panel_1.update(cx, |_, cx| cx.focus_self());
- // workspace.update(cx, |workspace, _| {
- // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
- // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
- // });
-
- // // Emitting a ZoomOut event unzooms the panel.
- // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
- // workspace.update(cx, |workspace, _| {
- // assert_eq!(workspace.zoomed, None);
- // assert_eq!(workspace.zoomed_position, None);
- // });
-
- // // Emit closed event on panel 1, which is active
- // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
-
- // // Now the left dock is closed, because panel_1 was the active panel
- // workspace.update(cx, |workspace, cx| {
- // let right_dock = workspace.right_dock();
- // assert!(!right_dock.read(cx).is_open());
- // });
- // }
-
- pub fn init_test(cx: &mut TestAppContext) {
- cx.update(|cx| {
- let settings_store = SettingsStore::test(cx);
- cx.set_global(settings_store);
- theme::init(theme::LoadThemes::JustBase, cx);
- language::init(cx);
- crate::init_settings(cx);
- Project::init_settings(cx);
- });
- }
-}
@@ -1,56 +0,0 @@
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::Settings;
-
-#[derive(Deserialize)]
-pub struct WorkspaceSettings {
- pub active_pane_magnification: f32,
- pub confirm_quit: bool,
- pub show_call_status_icon: bool,
- pub autosave: AutosaveSetting,
-}
-
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
-pub struct WorkspaceSettingsContent {
- pub active_pane_magnification: Option<f32>,
- pub confirm_quit: Option<bool>,
- pub show_call_status_icon: Option<bool>,
- pub autosave: Option<AutosaveSetting>,
-}
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum AutosaveSetting {
- Off,
- AfterDelay { milliseconds: u64 },
- OnFocusChange,
- OnWindowChange,
-}
-
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
-pub struct GitSettings {
- pub git_gutter: Option<GitGutterSetting>,
- pub gutter_debounce: Option<u64>,
-}
-
-#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum GitGutterSetting {
- #[default]
- TrackedFiles,
- Hide,
-}
-
-impl Settings for WorkspaceSettings {
- const KEY: Option<&'static str> = None;
-
- type FileContent = WorkspaceSettingsContent;
-
- 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)
- }
-}
@@ -71,7 +71,7 @@ theme_selector = { path = "../theme_selector" }
util = { path = "../util" }
semantic_index = { package = "semantic_index2", path = "../semantic_index2" }
vim = { path = "../vim" }
-workspace = { package = "workspace2", path = "../workspace2" }
+workspace = { path = "../workspace" }
welcome = { path = "../welcome" }
zed_actions = {package = "zed_actions2", path = "../zed_actions2"}
anyhow.workspace = true