From 789ce8dd7559b94edd38322de6443a99aa40db80 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 3 Jan 2024 11:08:51 -0800 Subject: [PATCH] Remove 2 suffix for workspace Co-authored-by: Mikayla --- Cargo.lock | 100 +- Cargo.toml | 2 +- crates/activity_indicator/Cargo.toml | 2 +- crates/assistant2/Cargo.toml | 2 +- crates/auto_update/Cargo.toml | 2 +- crates/breadcrumbs/Cargo.toml | 4 +- crates/collab2/Cargo.toml | 2 +- crates/collab_ui/Cargo.toml | 4 +- crates/command_palette/Cargo.toml | 4 +- crates/command_palette2/Cargo.toml | 2 +- crates/copilot_button/Cargo.toml | 2 +- crates/diagnostics/Cargo.toml | 4 +- crates/editor/Cargo.toml | 4 +- crates/feedback/Cargo.toml | 2 +- crates/file_finder/Cargo.toml | 4 +- crates/go_to_line/Cargo.toml | 2 +- crates/journal2/Cargo.toml | 2 +- crates/journal2/src/journal2.rs | 4 +- crates/language_selector/Cargo.toml | 2 +- crates/language_tools/Cargo.toml | 2 +- crates/outline/Cargo.toml | 2 +- crates/picker/Cargo.toml | 2 +- crates/project_panel/Cargo.toml | 4 +- crates/project_symbols/Cargo.toml | 14 +- crates/quick_action_bar/Cargo.toml | 4 +- crates/recent_projects/Cargo.toml | 2 +- crates/search/Cargo.toml | 4 +- crates/semantic_index2/Cargo.toml | 4 +- crates/terminal_view/Cargo.toml | 4 +- crates/theme_selector/Cargo.toml | 2 +- crates/vcs_menu/Cargo.toml | 2 +- crates/vim/Cargo.toml | 4 +- crates/welcome/Cargo.toml | 2 +- crates/workspace/Cargo.toml | 42 +- crates/workspace/src/dock.rs | 818 +-- crates/workspace/src/item.rs | 560 +- .../src/modal_layer.rs | 0 crates/workspace/src/notifications.rs | 393 +- crates/workspace/src/pane.rs | 2452 ++++---- .../src/pane/dragged_item_receiver.rs | 7 +- crates/workspace/src/pane_group.rs | 1016 ++-- crates/workspace/src/persistence.rs | 37 +- crates/workspace/src/persistence/model.rs | 39 +- crates/workspace/src/searchable.rs | 61 +- crates/workspace/src/shared_screen.rs | 105 +- crates/workspace/src/status_bar.rs | 203 +- crates/workspace/src/toolbar.rs | 340 +- crates/workspace/src/workspace.rs | 3758 ++++++------ crates/workspace/src/workspace_settings.rs | 6 +- crates/workspace/test.db | Bin 32768 -> 0 bytes crates/workspace2/Cargo.toml | 66 - crates/workspace2/src/dock.rs | 783 --- crates/workspace2/src/item.rs | 1070 ---- crates/workspace2/src/notifications.rs | 395 -- crates/workspace2/src/pane.rs | 2759 --------- .../src/pane/dragged_item_receiver.rs | 239 - crates/workspace2/src/pane_group.rs | 865 --- crates/workspace2/src/persistence.rs | 973 --- crates/workspace2/src/persistence/model.rs | 335 -- crates/workspace2/src/searchable.rs | 277 - crates/workspace2/src/shared_screen.rs | 128 - crates/workspace2/src/status_bar.rs | 194 - crates/workspace2/src/toolbar.rs | 321 - crates/workspace2/src/workspace2.rs | 5277 ----------------- crates/workspace2/src/workspace_settings.rs | 56 - crates/zed/Cargo.toml | 2 +- 66 files changed, 4758 insertions(+), 19021 deletions(-) rename crates/{workspace2 => workspace}/src/modal_layer.rs (100%) delete mode 100644 crates/workspace/test.db delete mode 100644 crates/workspace2/Cargo.toml delete mode 100644 crates/workspace2/src/dock.rs delete mode 100644 crates/workspace2/src/item.rs delete mode 100644 crates/workspace2/src/notifications.rs delete mode 100644 crates/workspace2/src/pane.rs delete mode 100644 crates/workspace2/src/pane/dragged_item_receiver.rs delete mode 100644 crates/workspace2/src/pane_group.rs delete mode 100644 crates/workspace2/src/persistence.rs delete mode 100644 crates/workspace2/src/persistence/model.rs delete mode 100644 crates/workspace2/src/searchable.rs delete mode 100644 crates/workspace2/src/shared_screen.rs delete mode 100644 crates/workspace2/src/status_bar.rs delete mode 100644 crates/workspace2/src/toolbar.rs delete mode 100644 crates/workspace2/src/workspace2.rs delete mode 100644 crates/workspace2/src/workspace_settings.rs diff --git a/Cargo.lock b/Cargo.lock index 0104dcf118..a0b41534db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/Cargo.toml b/Cargo.toml index 495f0221b5..2620f2495a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,7 +105,7 @@ members = [ "crates/story", "crates/vim", "crates/vcs_menu", - "crates/workspace2", + "crates/workspace", "crates/welcome", "crates/xtask", "crates/zed", diff --git a/crates/activity_indicator/Cargo.toml b/crates/activity_indicator/Cargo.toml index 0b4889f2bd..f4796569d9 100644 --- a/crates/activity_indicator/Cargo.toml +++ b/crates/activity_indicator/Cargo.toml @@ -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 diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index 2a46b157f7..fbe6a41b77 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -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 diff --git a/crates/auto_update/Cargo.toml b/crates/auto_update/Cargo.toml index cbc1b5581e..f04dc2f213 100644 --- a/crates/auto_update/Cargo.toml +++ b/crates/auto_update/Cargo.toml @@ -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 diff --git a/crates/breadcrumbs/Cargo.toml b/crates/breadcrumbs/Cargo.toml index a3ef6b170e..a54096f650 100644 --- a/crates/breadcrumbs/Cargo.toml +++ b/crates/breadcrumbs/Cargo.toml @@ -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"] } diff --git a/crates/collab2/Cargo.toml b/crates/collab2/Cargo.toml index 21aa51fb90..1f8349c42e 100644 --- a/crates/collab2/Cargo.toml +++ b/crates/collab2/Cargo.toml @@ -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"] } diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 642e78eb2d..6417df97f5 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -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 diff --git a/crates/command_palette/Cargo.toml b/crates/command_palette/Cargo.toml index 387a238734..47dbda3b28 100644 --- a/crates/command_palette/Cargo.toml +++ b/crates/command_palette/Cargo.toml @@ -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 diff --git a/crates/command_palette2/Cargo.toml b/crates/command_palette2/Cargo.toml index 9e8615a876..0c772834e8 100644 --- a/crates/command_palette2/Cargo.toml +++ b/crates/command_palette2/Cargo.toml @@ -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 diff --git a/crates/copilot_button/Cargo.toml b/crates/copilot_button/Cargo.toml index 2602a17626..a36769d9b4 100644 --- a/crates/copilot_button/Cargo.toml +++ b/crates/copilot_button/Cargo.toml @@ -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 diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml index 643173b3d5..b31b2a051b 100644 --- a/crates/diagnostics/Cargo.toml +++ b/crates/diagnostics/Cargo.toml @@ -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 diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index fbbf58d67b..937be1c27a 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -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 diff --git a/crates/feedback/Cargo.toml b/crates/feedback/Cargo.toml index 0f6873335e..fd2bd89f10 100644 --- a/crates/feedback/Cargo.toml +++ b/crates/feedback/Cargo.toml @@ -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" diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index 77174dd764..b0c2d85498 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -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 diff --git a/crates/go_to_line/Cargo.toml b/crates/go_to_line/Cargo.toml index 631721ce6b..c75b821147 100644 --- a/crates/go_to_line/Cargo.toml +++ b/crates/go_to_line/Cargo.toml @@ -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" } diff --git a/crates/journal2/Cargo.toml b/crates/journal2/Cargo.toml index 72da3deb69..f43d90fc85 100644 --- a/crates/journal2/Cargo.toml +++ b/crates/journal2/Cargo.toml @@ -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 diff --git a/crates/journal2/src/journal2.rs b/crates/journal2/src/journal2.rs index 95ef17a358..1db1846efb 100644 --- a/crates/journal2/src/journal2.rs +++ b/crates/journal2/src/journal2.rs @@ -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, 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 diff --git a/crates/language_selector/Cargo.toml b/crates/language_selector/Cargo.toml index b897b9062e..c933118080 100644 --- a/crates/language_selector/Cargo.toml +++ b/crates/language_selector/Cargo.toml @@ -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] diff --git a/crates/language_tools/Cargo.toml b/crates/language_tools/Cargo.toml index 0dd7387fc2..37ef5e0816 100644 --- a/crates/language_tools/Cargo.toml +++ b/crates/language_tools/Cargo.toml @@ -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" } diff --git a/crates/outline/Cargo.toml b/crates/outline/Cargo.toml index 879494d5b4..2fea9867f1 100644 --- a/crates/outline/Cargo.toml +++ b/crates/outline/Cargo.toml @@ -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 diff --git a/crates/picker/Cargo.toml b/crates/picker/Cargo.toml index 33401e4cab..4f5f1c12ee 100644 --- a/crates/picker/Cargo.toml +++ b/crates/picker/Cargo.toml @@ -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 diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index ff75d05552..3a490f7aad 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -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 diff --git a/crates/project_symbols/Cargo.toml b/crates/project_symbols/Cargo.toml index 5841c1d050..2a3321de0f 100644 --- a/crates/project_symbols/Cargo.toml +++ b/crates/project_symbols/Cargo.toml @@ -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"] } diff --git a/crates/quick_action_bar/Cargo.toml b/crates/quick_action_bar/Cargo.toml index 0fb06fb98c..8614cfd69f 100644 --- a/crates/quick_action_bar/Cargo.toml +++ b/crates/quick_action_bar/Cargo.toml @@ -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"] } diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index 7d13d5967b..7721a95767 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -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 diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index b03e0727cc..e005425cd5 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -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 diff --git a/crates/semantic_index2/Cargo.toml b/crates/semantic_index2/Cargo.toml index 65ffb05ca5..70b784f043 100644 --- a/crates/semantic_index2/Cargo.toml +++ b/crates/semantic_index2/Cargo.toml @@ -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" } diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml index 74a0d26d2e..76a5a10d33 100644 --- a/crates/terminal_view/Cargo.toml +++ b/crates/terminal_view/Cargo.toml @@ -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 diff --git a/crates/theme_selector/Cargo.toml b/crates/theme_selector/Cargo.toml index ee17388ffa..b165750256 100644 --- a/crates/theme_selector/Cargo.toml +++ b/crates/theme_selector/Cargo.toml @@ -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 diff --git a/crates/vcs_menu/Cargo.toml b/crates/vcs_menu/Cargo.toml index 2c7a8f0c13..6a02d85fd7 100644 --- a/crates/vcs_menu/Cargo.toml +++ b/crates/vcs_menu/Cargo.toml @@ -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 diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 72cddae989..677e1710fb 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -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"] } diff --git a/crates/welcome/Cargo.toml b/crates/welcome/Cargo.toml index c7ebc5c2a9..7abf1ee851 100644 --- a/crates/welcome/Cargo.toml +++ b/crates/welcome/Cargo.toml @@ -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 diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 99f19ed9d0..e1ec1621e9 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -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 diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 63c41ed373..bd965f63d4 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -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}; -pub trait Panel: View { +const RESIZE_HANDLE_SIZE: Pixels = Pixels(6.); + +pub enum PanelEvent { + ChangePosition, + ZoomIn, + ZoomOut, + Activate, + Close, + Focus, +} + +pub trait Panel: FocusableView + EventEmitter { + 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); - fn size(&self, cx: &WindowContext) -> f32; - fn set_size(&mut self, size: Option, cx: &mut ViewContext); - fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>; - fn icon_tooltip(&self) -> (String, Option>); + fn size(&self, cx: &WindowContext) -> Pixels; + fn set_size(&mut self, size: Option, cx: &mut ViewContext); + fn icon(&self, cx: &WindowContext) -> Option; + fn icon_tooltip(&self, cx: &WindowContext) -> Option<&'static str>; + fn toggle_action(&self) -> Box; fn icon_label(&self, _: &WindowContext) -> Option { 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) {} fn set_active(&mut self, _active: bool, _cx: &mut ViewContext) {} - 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, cx: &mut WindowContext); - fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>; - fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option>); + fn size(&self, cx: &WindowContext) -> Pixels; + fn set_size(&self, size: Option, cx: &mut WindowContext); + fn icon(&self, cx: &WindowContext) -> Option; + fn icon_tooltip(&self, cx: &WindowContext) -> Option<&'static str>; + fn toggle_action(&self, cx: &WindowContext) -> Box; fn icon_label(&self, cx: &WindowContext) -> Option; - 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 PanelHandle for ViewHandle +impl PanelHandle for View 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, 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>) { - self.read(cx).icon_tooltip() + fn set_size(&self, size: Option, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.set_size(size, cx)) + } + + fn icon(&self, cx: &WindowContext) -> Option { + 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 { + self.read(cx).toggle_action() } fn icon_label(&self, cx: &WindowContext) -> Option { 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, 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, - context_menu: ViewHandle, + panel: Arc, + // todo!() + // context_menu: View, _subscriptions: [Subscription; 2], } pub struct PanelButtons { - dock: ViewHandle, - workspace: WeakViewHandle, + dock: View, } 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) -> View { + let focus_handle = cx.focus_handle(); + + let dock = cx.new_view(|cx: &mut ViewContext| { + 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(&self) -> Option> { + pub fn panel(&self) -> Option> { 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(&self) -> Option { self.panel_entries .iter() - .position(|entry| entry.panel.as_any().is::()) + .position(|entry| entry.panel.to_any().downcast::().is_ok()) } - pub fn panel_index_for_ui_name(&self, ui_name: &str, cx: &AppContext) -> Option { - 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 { + 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, - ) { + pub fn set_panel_zoomed(&mut self, panel: &AnyView, zoomed: bool, cx: &mut ViewContext) { 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(&mut self, panel: ViewHandle, cx: &mut ViewContext) { + pub(crate) fn add_panel( + &mut self, + panel: View, + workspace: WeakView, + cx: &mut ViewContext, + ) { 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); } - } 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::Close => { + if this + .visible_panel() + .map_or(false, |p| p.panel_id() == Entity::entity_id(&panel)) + { + 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(&mut self, panel: &ViewHandle, cx: &mut ViewContext) { + pub fn remove_panel(&mut self, panel: &View, cx: &mut ViewContext) { 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> { + pub fn visible_panel(&self) -> Option<&Arc> { let entry = self.visible_entry()?; Some(&entry.panel) } - pub fn active_panel(&self) -> Option<&Rc> { + pub fn active_panel(&self) -> Option<&Arc> { Some(&self.panel_entries.get(self.active_panel_index)?.panel) } @@ -349,7 +466,7 @@ impl Dock { } } - pub fn zoomed_panel(&self, cx: &WindowContext) -> Option> { + pub fn zoomed_panel(&self, cx: &WindowContext) -> Option> { 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 { + pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option { 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 { + pub fn active_panel_size(&self, cx: &WindowContext) -> Option { 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, cx: &mut ViewContext) { + pub fn resize_active_panel(&mut self, size: Option, cx: &mut ViewContext) { 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 { - if let Some(active_entry) = self.visible_entry() { - Empty::new() - .into_any() - .contained() - .with_style(self.style(cx)) - .resizable::( - self.position.to_resize_handle_side(), - active_entry.panel.size(cx), - |_, _, _| {}, - ) - .into_any() - } else { - Empty::new().into_any() + pub fn toggle_action(&self) -> Box { + 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::(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) -> impl IntoElement { + if let Some(entry) = self.visible_entry() { + let size = entry.panel.size(cx); -impl View for Dock { - fn ui_name() -> &'static str { - "Dock" - } + 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(); - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - 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::( - self.position.to_resize_handle_side(), - active_entry.panel.size(cx), - |dock: &mut Self, size, cx| dock.resize_active_panel(size, cx), - ) - .into_any() - } else { - Empty::new().into_any() - } - } - - fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - if cx.is_self_focused() { - if let Some(active_entry) = self.visible_entry() { - cx.focus(active_entry.panel.as_any()); - } else { - cx.focus_parent(); + 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: ViewHandle, - workspace: WeakViewHandle, - cx: &mut ViewContext, - ) -> Self { + pub fn new(dock: View, cx: &mut ViewContext) -> 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) -> AnyElement { - let theme = &settings::get::(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) -> 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::>(); - 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::(panel_ix, cx, |state, cx| { - let style = button_style.in_state(is_active); + .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 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 - }) - .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(); - } - } - }) - .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, - ]; + let is_active_button = i == active_index && is_open; - 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); + 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()) }) - .with_tooltip::( - 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, + _active_pane_item: Option<&dyn crate::ItemHandle>, + _cx: &mut ViewContext, ) { + // 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 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 View for TestPanel { - fn ui_name() -> &'static str { - "TestPanel" - } - - fn render(&mut self, _: &mut ViewContext<'_, '_, Self>) -> AnyElement { - Empty::new().into_any() - } - - fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - self.has_focus = true; - cx.emit(TestPanelEvent::Focus); - } - - fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext) { - self.has_focus = false; + impl Render for TestPanel { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + div() } } impl Panel for TestPanel { + fn persistent_name() -> &'static str { + "TestPanel" + } + fn position(&self, _: &gpui::WindowContext) -> super::DockPosition { self.position } @@ -683,7 +739,27 @@ pub mod test { fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { self.position = position; - cx.emit(TestPanelEvent::PositionChanged); + cx.emit(PanelEvent::ChangePosition); + } + + fn size(&self, _: &WindowContext) -> Pixels { + self.size + } + + fn set_size(&mut self, size: Option, _: &mut ViewContext) { + self.size = size.unwrap_or(px(300.)); + } + + fn icon(&self, _: &WindowContext) -> Option { + None + } + + fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { + None + } + + fn toggle_action(&self) -> Box { + ToggleTestPanel.boxed_clone() } fn is_zoomed(&self, _: &WindowContext) -> bool { @@ -697,49 +773,11 @@ pub mod test { fn set_active(&mut self, active: bool, _cx: &mut ViewContext) { self.active = active; } + } - fn size(&self, _: &WindowContext) -> f32 { - self.size - } - - fn set_size(&mut self, size: Option, _: &mut ViewContext) { - 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>) { - ("Test Panel".into(), None) - } - - fn should_change_position_on_event(event: &Self::Event) -> bool { - matches!(event, TestPanelEvent::PositionChanged) - } - - fn should_zoom_in_on_event(event: &Self::Event) -> bool { - matches!(event, TestPanelEvent::ZoomIn) - } - - fn should_zoom_out_on_event(event: &Self::Event) -> bool { - matches!(event, TestPanelEvent::ZoomOut) - } - - fn should_activate_on_event(event: &Self::Event) -> bool { - matches!(event, TestPanelEvent::Activated) - } - - fn should_close_on_event(event: &Self::Event) -> bool { - matches!(event, TestPanelEvent::Closed) - } - - fn has_focus(&self, _cx: &WindowContext) -> bool { - self.has_focus - } - - 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() } } } diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index f96c19c9ac..38b7663030 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -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, } -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 { + _: &mut AppContext, + ) -> Result { 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, HighlightStyle)>>, } -pub trait Item: View { +pub trait Item: FocusableView + EventEmitter { + type Event; + fn deactivated(&mut self, _: &mut ViewContext) {} fn workspace_deactivated(&mut self, _: &mut ViewContext) {} fn navigate(&mut self, _: Box, _: &mut ViewContext) -> bool { false } - fn tab_tooltip_text(&self, _: &AppContext) -> Option> { + fn tab_tooltip_text(&self, _: &AppContext) -> Option { None } - fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option> { + fn tab_description(&self, _: usize, _: &AppContext) -> Option { None } - fn tab_content( + fn tab_content(&self, detail: Option, selected: bool, cx: &WindowContext) -> AnyElement; + + /// (model id, Item) + fn for_each_project_item( &self, - detail: Option, - style: &theme::Tab, - cx: &AppContext, - ) -> AnyElement; - 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) {} - fn clone_on_split(&self, _workspace_id: WorkspaceId, _: &mut ViewContext) -> Option + fn clone_on_split( + &self, + _workspace_id: WorkspaceId, + _: &mut ViewContext, + ) -> Option> 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, - _cx: &mut ViewContext, - ) -> Task> { + fn save(&mut self, _project: Model, _cx: &mut ViewContext) -> Task> { unimplemented!("save() must be implemented if can_save() returns true") } fn save_as( &mut self, - _project: ModelHandle, + _project: Model, _abs_path: PathBuf, _cx: &mut ViewContext, ) -> Task> { @@ -147,35 +151,28 @@ pub trait Item: View { } fn reload( &mut self, - _project: ModelHandle, + _project: Model, _cx: &mut ViewContext, ) -> Task> { 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_handle: &'a View, _: &'a AppContext, - ) -> Option<&AnyViewHandle> { + ) -> Option { if TypeId::of::() == type_id { - Some(self_handle) + Some(self_handle.clone().into()) } else { None } } - fn as_searchable(&self, _: &ViewHandle) -> Option> { + fn as_searchable(&self, _: &View) -> Option> { None } @@ -194,12 +191,12 @@ pub trait Item: View { } fn deserialize( - _project: ModelHandle, - _workspace: WeakViewHandle, + _project: Model, + _workspace: WeakView, _workspace_id: WorkspaceId, _item_id: ItemId, _cx: &mut ViewContext, - ) -> Task>> { + ) -> Task>> { 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 { + fn pixel_position_of_cursor(&self, _: &AppContext) -> Option> { None } } -pub trait ItemHandle: 'static + fmt::Debug { +pub trait ItemHandle: 'static + Send { fn subscribe_to_item_events( &self, cx: &mut WindowContext, handler: Box, ) -> gpui::Subscription; - fn tab_tooltip_text<'a>(&self, cx: &'a AppContext) -> Option>; - fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option>; - fn tab_content( - &self, - detail: Option, - style: &theme::Tab, - cx: &AppContext, - ) -> AnyElement; - fn dragged_tab_content( - &self, - detail: Option, - style: &theme::Tab, - cx: &AppContext, - ) -> AnyElement; + fn focus_handle(&self, cx: &WindowContext) -> FocusHandle; + fn tab_tooltip_text(&self, cx: &AppContext) -> Option; + fn tab_description(&self, detail: usize, cx: &AppContext) -> Option; + fn tab_content(&self, detail: Option, selected: bool, cx: &WindowContext) -> AnyElement; + fn dragged_tab_content(&self, detail: Option, cx: &WindowContext) -> AnyElement; fn project_path(&self, cx: &AppContext) -> Option; 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; 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: View, cx: &mut ViewContext, ); fn deactivated(&self, cx: &mut WindowContext); fn workspace_deactivated(&self, cx: &mut WindowContext); fn navigate(&self, data: Box, 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, cx: &mut WindowContext) -> Task>; + fn save(&self, project: Model, cx: &mut WindowContext) -> Task>; fn save_as( &self, - project: ModelHandle, + project: Model, abs_path: PathBuf, cx: &mut WindowContext, ) -> Task>; - fn reload(&self, project: ModelHandle, cx: &mut WindowContext) -> Task>; - fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a AppContext) -> Option<&'a AnyViewHandle>; + fn reload(&self, project: Model, cx: &mut WindowContext) -> Task>; + fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option; fn to_followable_item_handle(&self, cx: &AppContext) -> Option>; fn on_release( &self, cx: &mut AppContext, - callback: Box, + callback: Box, ) -> gpui::Subscription; fn to_searchable_item_handle(&self, cx: &AppContext) -> Option>; fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation; fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option>; fn serialized_item_kind(&self) -> Option<&'static str>; fn show_toolbar(&self, cx: &AppContext) -> bool; - fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option; + fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option>; } -pub trait WeakItemHandle { - fn id(&self) -> usize; - fn window(&self) -> AnyWindowHandle; - fn upgrade(&self, cx: &AppContext) -> Option>; +pub trait WeakItemHandle: Send + Sync { + fn id(&self) -> EntityId; + fn upgrade(&self) -> Option>; } impl dyn ItemHandle { - pub fn downcast(&self) -> Option> { - self.as_any().clone().downcast() + pub fn downcast(&self) -> Option> { + self.to_any().downcast().ok() } - pub fn act_as(&self, cx: &AppContext) -> Option> { - self.act_as_type(TypeId::of::(), cx) - .and_then(|t| t.clone().downcast()) + pub fn act_as(&self, cx: &AppContext) -> Option> { + self.act_as_type(TypeId::of::(), cx) + .and_then(|t| t.downcast().ok()) } } -impl ItemHandle for ViewHandle { +impl ItemHandle for View { fn subscribe_to_item_events( &self, cx: &mut WindowContext, handler: Box, ) -> 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> { + fn focus_handle(&self, cx: &WindowContext) -> FocusHandle { + self.focus_handle(cx) + } + + fn tab_tooltip_text(&self, cx: &AppContext) -> Option { self.read(cx).tab_tooltip_text(cx) } - fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option> { + fn tab_description(&self, detail: usize, cx: &AppContext) -> Option { self.read(cx).tab_description(detail, cx) } - fn tab_content( - &self, - detail: Option, - style: &theme::Tab, - cx: &AppContext, - ) -> AnyElement { - self.read(cx).tab_content(detail, style, cx) + fn tab_content(&self, detail: Option, selected: bool, cx: &WindowContext) -> AnyElement { + self.read(cx).tab_content(detail, selected, cx) } - fn dragged_tab_content( - &self, - detail: Option, - style: &theme::Tab, - cx: &AppContext, - ) -> AnyElement { - self.read(cx).tab_content(detail, style, cx) + fn dragged_tab_content(&self, detail: Option, cx: &WindowContext) -> AnyElement { + self.read(cx).tab_content(detail, true, cx) } fn project_path(&self, cx: &AppContext) -> Option { @@ -358,7 +340,7 @@ impl ItemHandle for ViewHandle { 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 ItemHandle for ViewHandle { 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 ItemHandle for ViewHandle { workspace_id: WorkspaceId, cx: &mut WindowContext, ) -> Option> { - self.update(cx, |item, cx| { - cx.add_option_view(|cx| item.clone_on_split(workspace_id, cx)) - }) - .map(|handle| Box::new(handle) as Box) + self.update(cx, |item, cx| item.clone_on_split(workspace_id, cx)) + .map(|handle| Box::new(handle) as Box) } fn added_to_pane( &self, workspace: &mut Workspace, - pane: ViewHandle, + pane: View, cx: &mut ViewContext, ) { + 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 ItemHandle for ViewHandle { 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 ItemHandle for ViewHandle { 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 ItemHandle for ViewHandle { ) && !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 ItemHandle for ViewHandle { } } - 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 = 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) }); } - - ItemEvent::Edit => { - let autosave = settings::get::(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::(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 ItemHandle for ViewHandle { 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 ItemHandle for ViewHandle { self.read(cx).can_save(cx) } - fn save(&self, project: ModelHandle, cx: &mut WindowContext) -> Task> { + fn save(&self, project: Model, cx: &mut WindowContext) -> Task> { self.update(cx, |item, cx| item.save(project, cx)) } fn save_as( &self, - project: ModelHandle, + project: Model, abs_path: PathBuf, cx: &mut WindowContext, ) -> Task> { self.update(cx, |item, cx| item.save_as(project, abs_path, cx)) } - fn reload(&self, project: ModelHandle, cx: &mut WindowContext) -> Task> { + fn reload(&self, project: Model, cx: &mut WindowContext) -> Task> { 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 { self.read(cx).act_as_type(type_id, self, cx) } fn to_followable_item_handle(&self, cx: &AppContext) -> Option> { if cx.has_global::() { let builders = cx.global::(); - 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 ItemHandle for ViewHandle { fn on_release( &self, cx: &mut AppContext, - callback: Box, + callback: Box, ) -> gpui::Subscription { cx.observe_release(self, move |_, cx| callback(cx)) } @@ -629,20 +610,20 @@ impl ItemHandle for ViewHandle { self.read(cx).show_toolbar() } - fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { + fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option> { self.read(cx).pixel_position_of_cursor(cx) } } -impl From> for AnyViewHandle { +impl From> for AnyView { fn from(val: Box) -> Self { - val.as_any().clone() + val.to_any() } } -impl From<&Box> for AnyViewHandle { +impl From<&Box> for AnyView { fn from(val: &Box) -> Self { - val.as_any().clone() + val.to_any() } } @@ -652,84 +633,85 @@ impl Clone for Box { } } -impl WeakItemHandle for WeakViewHandle { - fn id(&self) -> usize { - self.id() +impl WeakItemHandle for WeakView { + fn id(&self) -> EntityId { + self.entity_id() } - fn window(&self) -> AnyWindowHandle { - self.window() - } - - fn upgrade(&self, cx: &AppContext) -> Option> { - self.upgrade(cx).map(|v| Box::new(v) as Box) + fn upgrade(&self) -> Option> { + self.upgrade().map(|v| Box::new(v) as Box) } } pub trait ProjectItem: Item { - type Item: project::Item + gpui::Entity; + type Item: project::Item; fn for_project_item( - project: ModelHandle, - item: ModelHandle, + project: Model, + item: Model, cx: &mut ViewContext, - ) -> Self; + ) -> Self + where + Self: Sized; +} + +pub enum FollowEvent { + Unfollow, } pub trait FollowableItem: Item { fn remote_id(&self) -> Option; - fn to_state_proto(&self, cx: &AppContext) -> Option; + fn to_state_proto(&self, cx: &WindowContext) -> Option; fn from_state_proto( - pane: ViewHandle, - project: ViewHandle, + pane: View, + project: View, id: ViewId, state: &mut Option, - cx: &mut AppContext, - ) -> Option>>>; + cx: &mut WindowContext, + ) -> Option>>>; + fn to_follow_event(event: &Self::Event) -> Option; fn add_event_to_update_proto( &self, event: &Self::Event, update: &mut Option, - cx: &AppContext, + cx: &WindowContext, ) -> bool; fn apply_update_proto( &mut self, - project: &ModelHandle, + project: &Model, message: proto::update_view::Variant, cx: &mut ViewContext, ) -> Task>; - 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, cx: &mut ViewContext); - fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool; } pub trait FollowableItemHandle: ItemHandle { - fn remote_id(&self, client: &Arc, cx: &AppContext) -> Option; + fn remote_id(&self, client: &Arc, cx: &WindowContext) -> Option; fn set_leader_peer_id(&self, leader_peer_id: Option, cx: &mut WindowContext); - fn to_state_proto(&self, cx: &AppContext) -> Option; + fn to_state_proto(&self, cx: &WindowContext) -> Option; fn add_event_to_update_proto( &self, event: &dyn Any, update: &mut Option, - cx: &AppContext, + cx: &WindowContext, ) -> bool; + fn to_follow_event(&self, event: &dyn Any) -> Option; fn apply_update_proto( &self, - project: &ModelHandle, + project: &Model, message: proto::update_view::Variant, cx: &mut WindowContext, ) -> Task>; - 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 FollowableItemHandle for ViewHandle { - fn remote_id(&self, client: &Arc, cx: &AppContext) -> Option { +impl FollowableItemHandle for View { + fn remote_id(&self, client: &Arc, cx: &WindowContext) -> Option { 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 FollowableItemHandle for ViewHandle { self.update(cx, |this, cx| this.set_leader_peer_id(leader_peer_id, cx)) } - fn to_state_proto(&self, cx: &AppContext) -> Option { + fn to_state_proto(&self, cx: &WindowContext) -> Option { self.read(cx).to_state_proto(cx) } @@ -746,7 +728,7 @@ impl FollowableItemHandle for ViewHandle { &self, event: &dyn Any, update: &mut Option, - 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 FollowableItemHandle for ViewHandle { } } + fn to_follow_event(&self, event: &dyn Any) -> Option { + T::to_follow_event(event.downcast_ref()?) + } + fn apply_update_proto( &self, - project: &ModelHandle, + project: &Model, message: proto::update_view::Variant, cx: &mut WindowContext, ) -> Task> { 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, @@ -804,14 +782,11 @@ pub mod test { pub is_dirty: bool, pub is_singleton: bool, pub has_conflict: bool, - pub project_items: Vec>, + pub project_items: Vec>, pub nav_history: Option, pub tab_descriptions: Option>, pub tab_detail: Cell>, - } - - 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 { + pub fn new(id: u64, path: &str, cx: &mut AppContext) -> Model { 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 { - cx.add_model(|_| Self { + pub fn new_untitled(cx: &mut AppContext) -> Model { + 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 { 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 { + 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]) -> Self { + pub fn with_project_items(mut self, items: &[Model]) -> 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) -> impl IntoElement { + gpui::div().track_focus(&self.focus_handle) + } } - impl View for TestItem { - fn ui_name() -> &'static str { - "TestItem" - } + impl EventEmitter for TestItem {} - fn render(&mut self, _: &mut ViewContext) -> AnyElement { - 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> { + 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 { self.tab_descriptions.as_ref().and_then(|descriptions| { let description = *descriptions.get(detail).or_else(|| descriptions.last())?; Some(description.into()) }) } - fn tab_content( + fn tab_content( &self, detail: Option, - _: &theme::Tab, - _: &AppContext, - ) -> AnyElement { + _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 { @@ -999,12 +982,27 @@ pub mod test { fn clone_on_split( &self, _workspace_id: WorkspaceId, - _: &mut ViewContext, - ) -> Option + cx: &mut ViewContext, + ) -> Option> where Self: Sized, { - Some(self.clone()) + 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 { @@ -1025,7 +1023,7 @@ pub mod test { fn save( &mut self, - _: ModelHandle, + _: Model, _: &mut ViewContext, ) -> Task> { self.save_count += 1; @@ -1035,7 +1033,7 @@ pub mod test { fn save_as( &mut self, - _: ModelHandle, + _: Model, _: std::path::PathBuf, _: &mut ViewContext, ) -> Task> { @@ -1046,7 +1044,7 @@ pub mod test { fn reload( &mut self, - _: ModelHandle, + _: Model, _: &mut ViewContext, ) -> Task> { self.reload_count += 1; @@ -1054,22 +1052,18 @@ pub mod test { Task::ready(Ok(())) } - fn to_item_events(_: &Self::Event) -> SmallVec<[ItemEvent; 2]> { - [ItemEvent::UpdateTab, ItemEvent::Edit].into() - } - fn serialized_item_kind() -> Option<&'static str> { Some("TestItem") } fn deserialize( - _project: ModelHandle, - _workspace: WeakViewHandle, + _project: Model, + _workspace: WeakView, workspace_id: WorkspaceId, _item_id: ItemId, cx: &mut ViewContext, - ) -> Task>> { - let view = cx.add_view(|_cx| Self::new_deserialized(workspace_id)); + ) -> Task>> { + let view = cx.new_view(|cx| Self::new_deserialized(workspace_id, cx)); Task::Ready(Some(anyhow::Ok(view))) } } diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace/src/modal_layer.rs similarity index 100% rename from crates/workspace2/src/modal_layer.rs rename to crates/workspace/src/modal_layer.rs diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 36865eb22c..85ecf52a84 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -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: &::Event) -> bool; +pub trait Notification: EventEmitter + Render {} + +impl + Render> Notification for V {} + +pub trait NotificationHandle: Send { + fn id(&self) -> EntityId; + fn to_any(&self) -> AnyView; } -pub trait NotificationHandle { - fn id(&self) -> usize; - fn as_any(&self) -> &AnyViewHandle; -} - -impl NotificationHandle for ViewHandle { - fn id(&self) -> usize { - self.id() +impl NotificationHandle for View { + 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, - build_notification: impl FnOnce(&mut ViewContext) -> ViewHandle, + build_notification: impl FnOnce(&mut ViewContext) -> View, ) { if !self.has_shown_notification_once::(id, cx) { - cx.update_global::(|tracker, _| { - let entry = tracker.entry(TypeId::of::()).or_default(); - entry.push(id); - }); - + let tracker = cx.global_mut::(); + let entry = tracker.entry(TypeId::of::()).or_default(); + entry.push(id); self.show_notification::(id, cx, build_notification) } } @@ -91,7 +93,7 @@ impl Workspace { &mut self, id: usize, cx: &mut ViewContext, - build_notification: impl FnOnce(&mut ViewContext) -> ViewHandle, + build_notification: impl FnOnce(&mut ViewContext) -> View, ) { let type_id = TypeId::of::(); 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(&mut self, err: &E, cx: &mut ViewContext) + 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(&mut self, id: usize, cx: &mut ViewContext) { let type_id = TypeId::of::(); @@ -123,7 +134,7 @@ impl Workspace { pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext) { self.dismiss_notification::(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>>(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| { - cx.platform().open_url(open_action.0.as_ref()); - }, - ) - } - - enum NotificationMessage { - Text(Cow<'static, str>), - Element(fn(TextStyle, &AppContext) -> AnyElement), - } + 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)>>, - click_message: Option>, + click_message: Option, } - pub enum MessageNotificationEvent { - Dismiss, - } - - impl Entity for MessageNotification { - type Event = MessageNotificationEvent; - } + impl EventEmitter for MessageNotification {} impl MessageNotification { pub fn new(message: S) -> MessageNotification where - S: Into>, + S: Into, { Self { - message: NotificationMessage::Text(message.into()), - on_click: None, - click_message: None, - } - } - - pub fn new_element( - message: fn(TextStyle, &AppContext) -> AnyElement, - ) -> 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(mut self, message: S) -> Self where - S: Into>, + S: Into, { 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) { - cx.emit(MessageNotificationEvent::Dismiss); + pub fn dismiss(&mut self, cx: &mut ViewContext) { + cx.emit(DismissEvent); } } - impl View for MessageNotification { - fn ui_name() -> &'static str { - "MessageNotification" - } - - fn render(&mut self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { - 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::(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) -> 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::( - 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() + .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" + // } - impl Notification for MessageNotification { - fn should_dismiss_notification_on_event(&self, event: &::Event) -> bool { - match event { - MessageNotificationEvent::Dismiss => true, - } - } - } + // fn render(&mut self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { + // 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::(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::( + // 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, ) -> Option; + + fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option; } impl NotifyResultExt for Result @@ -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 { + match self { + Ok(value) => Some(value), + Err(err) => { + log::error!("TODO {err:?}"); + cx.update(|view, cx| { + if let Ok(workspace) = view.downcast::() { + workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx)) + } + }) + .ok(); None } } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index c32dca936e..7664231e26 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -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, -} +// #[derive(Clone, PartialEq)] +// pub struct CloseItemById { +// pub item_id: usize, +// pub pane: WeakView, +// } -#[derive(Clone, PartialEq)] -pub struct CloseItemsToTheLeftById { - pub item_id: usize, - pub pane: WeakViewHandle, -} +// #[derive(Clone, PartialEq)] +// pub struct CloseItemsToTheLeftById { +// pub item_id: usize, +// pub pane: WeakView, +// } -#[derive(Clone, PartialEq)] -pub struct CloseItemsToTheRightById { - pub item_id: usize, - pub pane: WeakViewHandle, -} +// #[derive(Clone, PartialEq)] +// pub struct CloseItemsToTheRightById { +// pub item_id: usize, +// pub pane: WeakView, +// } #[derive(Clone, PartialEq, Debug, Deserialize, Default)] #[serde(rename_all = "camelCase")] @@ -90,7 +81,7 @@ pub struct CloseActiveItem { pub save_intent: Option, } -#[derive(Clone, PartialEq, Debug, Deserialize)] +#[derive(Clone, PartialEq, Debug, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct CloseAllItems { pub save_intent: Option, @@ -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 }, 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>, - activation_history: Vec, + activation_history: Vec, zoomed: bool, + was_focused: bool, active_item_index: usize, - last_focused_view_by_item: HashMap, - autoscroll: bool, + last_focused_view_by_item: HashMap, nav_history: NavHistory, - toolbar: ViewHandle, - tab_bar_context_menu: TabBarContextMenu, - tab_context_menu: ViewHandle, - _background_actions: BackgroundActions, - workspace: WeakViewHandle, - project: ModelHandle, - has_focus: bool, - can_drop: Rc, &WindowContext) -> bool>, + toolbar: View, + new_item_menu: Option>, + split_item_menu: Option>, + // tab_context_menu: View, + workspace: WeakView, + project: Model, + drag_split_direction: Option, + can_drop_predicate: Option bool>>, can_split: bool, - render_tab_bar_buttons: Rc) -> AnyElement>, + render_tab_bar_buttons: Rc) -> AnyElement>, + _subscriptions: Vec, + tab_bar_scroll_handle: ScrollHandle, + display_nav_history_buttons: bool, } pub struct ItemNavHistory { history: NavHistory, - item: Rc, + item: Arc, } #[derive(Clone)] -pub struct NavHistory(Rc>); +pub struct NavHistory(Arc>); struct NavHistoryState { mode: NavigationMode, backward_stack: VecDeque, forward_stack: VecDeque, closed_stack: VecDeque, - paths_by_item: HashMap)>, - pane: WeakViewHandle, + paths_by_item: HashMap)>, + pane: WeakView, next_timestamp: Arc, } @@ -237,115 +224,135 @@ impl Default for NavigationMode { } pub struct NavigationEntry { - pub item: Rc, - pub data: Option>, + pub item: Arc, + pub data: Option>, pub timestamp: usize, } -pub struct DraggedItem { - pub handle: Box, - pub pane: WeakViewHandle, +#[derive(Clone)] +pub struct DraggedTab { + pub pane: View, + pub ix: usize, + pub item_id: EntityId, + pub detail: usize, + pub is_active: bool, } -pub enum ReorderBehavior { - None, - MoveAfterActive, - MoveToIndex(usize), -} +// pub struct DraggedItem { +// pub handle: Box, +// pub pane: WeakView, +// } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum TabBarContextMenuKind { - New, - Split, -} +// pub enum ReorderBehavior { +// None, +// MoveAfterActive, +// MoveToIndex(usize), +// } -struct TabBarContextMenu { - kind: TabBarContextMenuKind, - handle: ViewHandle, -} +// #[derive(Debug, Clone, Copy, PartialEq, Eq)] +// enum TabBarContextMenuKind { +// New, +// Split, +// } -impl TabBarContextMenu { - fn handle_if_kind(&self, kind: TabBarContextMenuKind) -> Option> { - if self.kind == kind { - return Some(self.handle.clone()); - } - None - } -} +// struct TabBarContextMenu { +// kind: TabBarContextMenuKind, +// handle: View, +// } -#[allow(clippy::too_many_arguments)] -fn nav_button)>( - svg_path: &'static str, - style: theme::Interactive, - nav_button_height: f32, - tooltip_style: TooltipStyle, - enabled: bool, - on_click: F, - tooltip_action: A, - action_name: &str, - cx: &mut ViewContext, -) -> AnyElement { - MouseEventHandler::new::(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::( - 0, - action_name.to_string(), - Some(Box::new(tooltip_action)), - tooltip_style, - cx, - ) - .contained() - .into_any_named("nav button") -} +// impl TabBarContextMenu { +// fn handle_if_kind(&self, kind: TabBarContextMenuKind) -> Option> { +// if self.kind == kind { +// return Some(self.handle.clone()); +// } +// None +// } +// } + +// #[allow(clippy::too_many_arguments)] +// fn nav_button)>( +// svg_path: &'static str, +// style: theme2::Interactive, +// nav_button_height: f32, +// tooltip_style: TooltipStyle, +// enabled: bool, +// on_click: F, +// tooltip_action: A, +// action_name: &str, +// cx: &mut ViewContext, +// ) -> AnyElement { +// MouseEventHandler::new::(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::( +// 0, +// action_name.to_string(), +// Some(Box::new(tooltip_action)), +// tooltip_style, +// cx, +// ) +// .contained() +// .into_any_named("nav button") +// } + +impl EventEmitter for Pane {} impl Pane { pub fn new( - workspace: WeakViewHandle, - project: ModelHandle, - background_actions: BackgroundActions, + workspace: WeakView, + project: Model, next_timestamp: Arc, + can_drop_predicate: Option bool + 'static>>, cx: &mut ViewContext, ) -> 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)) }) - .into_any() + .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(crate) fn workspace(&self) -> &WeakViewHandle { - &self.workspace + pub fn has_focus(&self, cx: &WindowContext) -> bool { + // todo!(); // inline this manually + self.focus_handle.contains_focused(cx) } - pub fn has_focus(&self) -> bool { - self.has_focus + fn focus_in(&mut self, cx: &mut ViewContext) { + 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) -> 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.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(&mut self, can_drop: F) - where - F: 'static + Fn(&DragAndDrop, &WindowContext) -> bool, - { - self.can_drop = Rc::new(can_drop); - } + // pub fn on_can_drop(&mut self, can_drop: F) + // where + // F: 'static + Fn(&DragAndDrop, &WindowContext) -> bool, + // { + // self.can_drop = Rc::new(can_drop); + // } pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext) { self.can_split = can_split; @@ -459,16 +521,16 @@ impl Pane { pub fn set_render_tab_bar_buttons(&mut self, cx: &mut ViewContext, render: F) where - F: 'static + Fn(&mut Pane, &mut ViewContext) -> AnyElement, + F: 'static + Fn(&mut Pane, &mut ViewContext) -> AnyElement, { self.render_tab_bar_buttons = Rc::new(render); cx.notify(); } - pub fn nav_history_for_item(&self, item: &ViewHandle) -> ItemNavHistory { + pub fn nav_history_for_item(&self, item: &View) -> 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) { + 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) { + 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) { @@ -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(&self) -> impl '_ + Iterator> { + pub fn items_of_type(&self) -> impl '_ + Iterator> { 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> { self.items.get(self.active_item_index).cloned() } - pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { + pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option> { 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 { - self.items.iter().position(|i| i.id() == item.id()) + 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) { - // 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(); - 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, ) -> Task> { @@ -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, ) -> Task> { 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, ) -> Task> { 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, mut save_intent: SaveIntent, - should_close: impl 'static + Fn(usize) -> bool, + should_close: impl Fn(EntityId) -> bool, ) -> Task> { // 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, _ => {} } } @@ -956,7 +1042,7 @@ impl Pane { // 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.read_with(&cx, |pane, cx| { + 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 { @@ -968,11 +1054,11 @@ impl Pane { // 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.read_with(&cx, |workspace, cx| { + 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.id() == item.id()) + .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)); @@ -1000,13 +1086,18 @@ impl Pane { // Remove the item from the pane. pane.update(&mut cx, |pane, cx| { - if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) { + 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())?; + pane.update(&mut cx, |_, cx| cx.notify()).ok(); Ok(()) }) } @@ -1018,7 +1109,7 @@ impl Pane { cx: &mut ViewContext, ) { self.activation_history - .retain(|&history_entry| history_entry != self.items[item_index].id()); + .retain(|&history_entry| history_entry != self.items[item_index].item_id()); if item_index == self.active_item_index { let index_to_activate = self @@ -1026,20 +1117,26 @@ impl Pane { .pop() .and_then(|last_activated_item| { self.items.iter().enumerate().find_map(|(index, item)| { - (item.id() == last_activated_item).then_some(index) + (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; - self.activate_item(index_to_activate, should_activate, should_activate, cx); + 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.id() }); + cx.emit(Event::RemoveItem { + item_id: item.item_id(), + }); if self.items.is_empty() { item.deactivated(cx); self.update_toolbar(cx); @@ -1058,22 +1155,22 @@ impl Pane { let abs_path = self .nav_history .0 - .borrow() + .lock() .paths_by_item - .get(&item.id()) + .get(&item.item_id()) .and_then(|(_, abs_path)| abs_path.clone()); self.nav_history .0 - .borrow_mut() + .lock() .paths_by_item - .insert(item.id(), (path, abs_path)); + .insert(item.item_id(), (path, abs_path)); } else { self.nav_history .0 - .borrow_mut() + .lock() .paths_by_item - .remove(&item.id()); + .remove(&item.item_id()); } if self.items.is_empty() && self.zoomed { @@ -1084,28 +1181,28 @@ impl Pane { } pub async fn save_item( - project: ModelHandle, - pane: &WeakViewHandle, + project: Model, + pane: &WeakView, item_ix: usize, item: &dyn ItemHandle, save_intent: SaveIntent, - cx: &mut AsyncAppContext, + cx: &mut AsyncWindowContext, ) -> Result { const CONFLICT_MESSAGE: &str = - "This file has changed on disk since you started editing it. Do you want to overwrite it?"; + "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.read(|cx| { + 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 { @@ -1123,7 +1220,7 @@ impl Pane { } if has_conflict && can_save { - let mut answer = pane.update(cx, |pane, cx| { + let answer = pane.update(cx, |pane, cx| { pane.activate_item(item_ix, true, true, cx); cx.prompt( PromptLevel::Warning, @@ -1131,21 +1228,21 @@ impl Pane { &["Overwrite", "Discard", "Cancel"], ) })?; - match answer.next().await { - Some(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?, - Some(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?, + 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.read(|cx| { + let will_autosave = cx.update(|_, cx| { matches!( - settings::get::(cx).autosave, + WorkspaceSettings::get_global(cx).autosave, AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange ) && Self::can_autosave_item(&*item, cx) - }); + })?; if !will_autosave { - let mut answer = pane.update(cx, |pane, cx| { + 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( @@ -1154,10 +1251,10 @@ impl Pane { &["Save", "Don't Save", "Cancel"], ) })?; - match answer.next().await { - Some(0) => {} - Some(1) => return Ok(true), // Don't save his file - _ => return Ok(false), // Cancel + match answer.await { + Ok(0) => {} + Ok(1) => return Ok(true), // Don't save this file + _ => return Ok(false), // Cancel } } } @@ -1166,14 +1263,14 @@ impl Pane { pane.update(cx, |_, cx| item.save(project, cx))?.await?; } else if can_save_as { let start_abs_path = project - .read_with(cx, |project, cx| { + .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 mut abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path)); - if let Some(abs_path) = abs_path.next().await.flatten() { + 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 { @@ -1191,7 +1288,7 @@ impl Pane { pub fn autosave_item( item: &dyn ItemHandle, - project: ModelHandle, + project: Model, cx: &mut WindowContext, ) -> Task> { if Self::can_autosave_item(item, cx) { @@ -1201,9 +1298,14 @@ impl Pane { } } + pub fn focus(&mut self, cx: &mut ViewContext) { + cx.focus(&self.focus_handle); + } + pub fn focus_active_item(&mut self, cx: &mut ViewContext) { if let Some(active_item) = self.active_item() { - cx.focus(active_item.as_any()); + let focus_handle = active_item.focus_handle(cx); + cx.focus(&focus_handle); } } @@ -1211,133 +1313,126 @@ impl Pane { cx.emit(Event::Split(direction)); } - fn deploy_split_menu(&mut self, cx: &mut ViewContext) { - 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, - ); - }); + // fn deploy_split_menu(&mut self, cx: &mut ViewContext) { + // 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; - } + // self.tab_bar_context_menu.kind = TabBarContextMenuKind::Split; + // } - fn deploy_new_menu(&mut self, cx: &mut ViewContext) { - 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, - ); - }); + // fn deploy_new_menu(&mut self, cx: &mut ViewContext) { + // 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; - } + // self.tab_bar_context_menu.kind = TabBarContextMenuKind::New; + // } - fn deploy_tab_context_menu( - &mut self, - position: Vector2F, - target_item_id: usize, - cx: &mut ViewContext, - ) { - let active_item_id = self.items[self.active_item_index].id(); - let single_entry_to_resolve = - self.items() - .find(|i| i.id() == target_item_id) - .and_then(|i| { - let item_entries = i.project_entry_ids(cx); - if item_entries.len() == 1 { - Some(item_entries[0]) - } else { - None - } - }); - let is_active_item = target_item_id == active_item_id; - let target_pane = cx.weak_handle(); + // fn deploy_tab_context_menu( + // &mut self, + // position: Vector2F, + // target_item_id: usize, + // cx: &mut ViewContext, + // ) { + // 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| { - let mut menu_items = 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 }), - ] - }; + // // 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 - if let Some(entry) = single_entry_to_resolve { - menu_items.push(ContextMenuItem::Separator); - menu_items.push(ContextMenuItem::action( - "Reveal In Project Panel", - RevealInProjectPanel { - entry_id: entry.to_proto(), - }, - )); - } + // 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, + // ); + // }); + // } - menu.show(position, AnchorCorner::TopLeft, menu_items, cx); - }); - } - - pub fn toolbar(&self) -> &ViewHandle { + pub fn toolbar(&self) -> &View { &self.toolbar } @@ -1348,7 +1443,7 @@ impl 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.id())) + Some((i, item.item_id())) } else { None } @@ -1370,163 +1465,278 @@ impl Pane { }); } - fn render_tabs(&mut self, cx: &mut ViewContext) -> impl Element { - let theme = theme::current(cx).clone(); + fn update_status_bar(&mut self, cx: &mut ViewContext) { + let workspace = self.workspace.clone(); + let pane = cx.view().clone(); - let pane = cx.handle().downgrade(); - let autoscroll = if mem::take(&mut self.autoscroll) { - Some(self.active_item_index) - } else { - None + 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, + 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::(|tab| tab.bg(cx.theme().colors().drop_target_background)) + .drag_over::(|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) + .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_active = self.has_focus; + 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)); + }), + ); - enum Tabs {} - let mut row = Flex::row().scrollable::(1, autoscroll, cx); - for (ix, (item, detail)) in self - .items - .iter() - .cloned() - .zip(self.tab_details(cx)) - .enumerate() - { - let git_status = item - .project_path(cx) - .and_then(|path| self.project.read(cx).entry_for_path(&path, cx)) - .and_then(|entry| entry.git_status()); - - let detail = if detail == 0 { None } else { Some(detail) }; - let tab_active = ix == self.active_item_index; - - row.add_child({ - enum TabDragReceiver {} - let mut receiver = - dragged_item_receiver::(self, ix, ix, true, None, cx, { - let item = item.clone(); - let pane = pane.clone(); - let detail = detail.clone(); - - let theme = theme::current(cx).clone(); - let mut tooltip_theme = theme.tooltip.clone(); - tooltip_theme.max_text_width = None; - let tab_tooltip_text = - item.tab_tooltip_text(cx).map(|text| text.into_owned()); - - let mut tab_style = theme - .workspace - .tab_bar - .tab_style(pane_active, tab_active) - .clone(); - let should_show_status = settings::get::(cx).git_status; - if should_show_status && git_status != None { - tab_style.label.text.color = match git_status.unwrap() { - GitFileStatus::Added => tab_style.git.inserted, - GitFileStatus::Modified => tab_style.git.modified, - GitFileStatus::Conflict => tab_style.git.conflict, - }; - } - - move |mouse_state, cx| { - let hovered = mouse_state.hovered(); - - enum Tab {} - let mouse_event_handler = - MouseEventHandler::new::(ix, cx, |_, cx| { - Self::render_tab( - &item, - pane.clone(), - ix == 0, - detail, - hovered, - &tab_style, - cx, - ) - }) - .on_down(MouseButton::Left, move |_, this, cx| { - this.activate_item(ix, true, true, cx); - }) - .on_click(MouseButton::Middle, { - let item_id = item.id(); - move |_, pane, cx| { - pane.close_item_by_id(item_id, SaveIntent::Close, cx) - .detach_and_log_err(cx); - } - }) - .on_down( - MouseButton::Right, - move |event, pane, cx| { - pane.deploy_tab_context_menu(event.position, item.id(), cx); - }, - ); - - if let Some(tab_tooltip_text) = tab_tooltip_text { - mouse_event_handler - .with_tooltip::( - ix, - tab_tooltip_text, - None, - tooltip_theme, - cx, - ) - .into_any() - } else { - mouse_event_handler.into_any() - } - } - }); - - if !pane_active || !tab_active { - receiver = receiver.with_cursor_style(CursorStyle::PointingHand); + 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), + )) + }); + }), + ); + } } - receiver.as_draggable( - DraggedItem { - handle: item, - pane: pane.clone(), - }, - { - let theme = theme::current(cx).clone(); + menu + }) + }) + } - let detail = detail.clone(); - move |_, dragged_item: &DraggedItem, cx: &mut ViewContext| { - let tab_style = &theme.workspace.tab_bar.dragged_tab; - Self::render_dragged_tab( - &dragged_item.handle, - dragged_item.pane.clone(), - false, - detail, - false, - &tab_style, - cx, - ) - } - }, + 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)), + ), ) }) - } - - // Use the inactive tab style along with the current pane's active status to decide how to render - // the filler - let filler_index = self.items.len(); - let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false); - enum Filler {} - row.add_child( - dragged_item_receiver::(self, 0, filler_index, true, None, cx, |_, _| { - Empty::new() - .contained() - .with_style(filler_style.container) - .with_border(filler_style.container.border) + .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) + }) }) - .flex(1., true) - .into_any_named("filler"), - ); + .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::(|bar| { + bar.bg(cx.theme().colors().drop_target_background) + }) + .drag_over::(|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) + })), + ) + } - row + fn render_menu_overlay(menu: &View) -> 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 { - let mut tab_details = (0..self.items.len()).map(|_| 0).collect::>(); + let mut tab_details = self.items.iter().map(|_| 0).collect::>(); let mut tab_descriptions = HashMap::default(); let mut done = false; @@ -1547,7 +1757,7 @@ impl Pane { } } - // If two or more items have the same tab description, increase their level + // 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 { @@ -1562,192 +1772,6 @@ impl Pane { tab_details } - fn render_tab( - item: &Box, - pane: WeakViewHandle, - first: bool, - detail: Option, - hovered: bool, - tab_style: &theme::Tab, - cx: &mut ViewContext, - ) -> AnyElement { - let title = item.tab_content(detail, &tab_style, cx); - Self::render_tab_with_title(title, item, pane, first, hovered, tab_style, cx) - } - - fn render_dragged_tab( - item: &Box, - pane: WeakViewHandle, - first: bool, - detail: Option, - hovered: bool, - tab_style: &theme::Tab, - cx: &mut ViewContext, - ) -> AnyElement { - let title = item.dragged_tab_content(detail, &tab_style, cx); - Self::render_tab_with_title(title, item, pane, first, hovered, tab_style, cx) - } - - fn render_tab_with_title( - title: AnyElement, - item: &Box, - pane: WeakViewHandle, - first: bool, - hovered: bool, - tab_style: &theme::Tab, - cx: &mut ViewContext, - ) -> AnyElement { - let mut container = tab_style.container.clone(); - if first { - container.border.left = false; - } - - let buffer_jewel_element = { - let diameter = 7.0; - let icon_color = if item.has_conflict(cx) { - Some(tab_style.icon_conflict) - } else if item.is_dirty(cx) { - Some(tab_style.icon_dirty) - } else { - None - }; - - Canvas::new(move |bounds, _, _, cx| { - if let Some(color) = icon_color { - let square = RectF::new(bounds.origin(), vec2f(diameter, diameter)); - cx.scene().push_quad(Quad { - bounds: square, - background: Some(color), - border: Default::default(), - corner_radii: (diameter / 2.).into(), - }); - } - }) - .constrained() - .with_width(diameter) - .with_height(diameter) - .aligned() - }; - - let title_element = title.aligned().contained().with_style(ContainerStyle { - margin: Margin { - left: tab_style.spacing, - right: tab_style.spacing, - ..Default::default() - }, - ..Default::default() - }); - - let close_element = if hovered { - let item_id = item.id(); - enum TabCloseButton {} - let icon = Svg::new("icons/x.svg"); - MouseEventHandler::new::(item_id, cx, |mouse_state, _| { - if mouse_state.hovered() { - icon.with_color(tab_style.icon_close_active) - } else { - icon.with_color(tab_style.icon_close) - } - }) - .with_padding(Padding::uniform(4.)) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, { - let pane = pane.clone(); - move |_, _, cx| { - let pane = pane.clone(); - cx.window_context().defer(move |cx| { - if let Some(pane) = pane.upgrade(cx) { - pane.update(cx, |pane, cx| { - pane.close_item_by_id(item_id, SaveIntent::Close, cx) - .detach_and_log_err(cx); - }); - } - }); - } - }) - .into_any_named("close-tab-icon") - .constrained() - } else { - Empty::new().constrained() - } - .with_width(tab_style.close_icon_width) - .aligned(); - - let close_right = settings::get::(cx).close_position.right(); - - if close_right { - Flex::row() - .with_child(buffer_jewel_element) - .with_child(title_element) - .with_child(close_element) - } else { - Flex::row() - .with_child(close_element) - .with_child(title_element) - .with_child(buffer_jewel_element) - } - .contained() - .with_style(container) - .constrained() - .with_height(tab_style.height) - .into_any() - } - - pub fn render_tab_bar_button< - F1: 'static + Fn(&mut Pane, &mut EventContext), - F2: 'static + Fn(&mut Pane, &mut EventContext), - >( - index: usize, - icon: &'static str, - is_active: bool, - tooltip: Option<(&'static str, Option>)>, - cx: &mut ViewContext, - on_click: F1, - on_down: F2, - context_menu: Option>, - ) -> AnyElement { - enum TabBarButton {} - - let mut button = MouseEventHandler::new::(index, cx, |mouse_state, cx| { - let theme = &settings::get::(cx).theme.workspace.tab_bar; - let style = theme.pane_button.in_state(is_active).style_for(mouse_state); - Svg::new(icon) - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .constrained() - .with_width(style.button_width) - .with_height(style.button_width) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_down(MouseButton::Left, move |_, pane, cx| on_down(pane, cx)) - .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx)) - .into_any(); - if let Some((tooltip, action)) = tooltip { - let tooltip_style = settings::get::(cx).theme.tooltip.clone(); - button = button - .with_tooltip::(index, tooltip, action, tooltip_style, cx) - .into_any(); - } - - Stack::new() - .with_child(button) - .with_children( - context_menu.map(|menu| ChildView::new(&menu, cx).aligned().bottom().right()), - ) - .flex(1., false) - .into_any_named("tab bar button") - } - - fn render_blank_pane(&self, theme: &Theme, _cx: &mut ViewContext) -> AnyElement { - let background = theme.workspace.background; - Empty::new() - .contained() - .with_background_color(background) - .into_any() - } - pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { self.zoomed = zoomed; cx.notify(); @@ -1756,246 +1780,266 @@ impl Pane { pub fn is_zoomed(&self) -> bool { self.zoomed } -} -impl Entity for Pane { - type Event = Event; -} - -impl View for Pane { - fn ui_name() -> &'static str { - "Pane" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - enum MouseNavigationHandler {} - - MouseEventHandler::new::(0, cx, |_, cx| { - let active_item_index = self.active_item_index; - - if let Some(active_item) = self.active_item() { - Flex::column() - .with_child({ - let theme = theme::current(cx).clone(); - - let mut stack = Stack::new(); - - enum TabBarEventHandler {} - stack.add_child( - MouseEventHandler::new::(0, cx, |_, _| { - Empty::new() - .contained() - .with_style(theme.workspace.tab_bar.container) - }) - .on_down( - MouseButton::Left, - move |_, this, cx| { - this.activate_item(active_item_index, true, true, cx); - }, - ), - ); - let tooltip_style = theme.tooltip.clone(); - let tab_bar_theme = theme.workspace.tab_bar.clone(); - - let nav_button_height = tab_bar_theme.height; - let button_style = tab_bar_theme.nav_button; - let border_for_nav_buttons = tab_bar_theme - .tab_style(false, false) - .container - .border - .clone(); - - let mut tab_row = Flex::row() - .with_child(nav_button( - "icons/arrow_left.svg", - button_style.clone(), - nav_button_height, - tooltip_style.clone(), - self.can_navigate_backward(), - { - move |pane, cx| { - if let Some(workspace) = pane.workspace.upgrade(cx) { - let pane = cx.weak_handle(); - cx.window_context().defer(move |cx| { - workspace.update(cx, |workspace, cx| { - workspace - .go_back(pane, cx) - .detach_and_log_err(cx) - }) - }) - } - } - }, - super::GoBack, - "Go Back", - cx, - )) - .with_child( - nav_button( - "icons/arrow_right.svg", - button_style.clone(), - nav_button_height, - tooltip_style, - self.can_navigate_forward(), - { - move |pane, cx| { - if let Some(workspace) = pane.workspace.upgrade(cx) { - let pane = cx.weak_handle(); - cx.window_context().defer(move |cx| { - workspace.update(cx, |workspace, cx| { - workspace - .go_forward(pane, cx) - .detach_and_log_err(cx) - }) - }) - } - } - }, - super::GoForward, - "Go Forward", - cx, - ) - .contained() - .with_border(border_for_nav_buttons), - ) - .with_child(self.render_tabs(cx).flex(1., true).into_any_named("tabs")); - - if self.has_focus { - let render_tab_bar_buttons = self.render_tab_bar_buttons.clone(); - tab_row.add_child( - (render_tab_bar_buttons)(self, cx) - .contained() - .with_style(theme.workspace.tab_bar.pane_button_container) - .flex(1., false) - .into_any(), - ) - } - - stack.add_child(tab_row); - stack - .constrained() - .with_height(theme.workspace.tab_bar.height) - .flex(1., false) - .into_any_named("tab bar") - }) - .with_child({ - enum PaneContentTabDropTarget {} - dragged_item_receiver::( - self, - 0, - self.active_item_index + 1, - !self.can_split, - if self.can_split { Some(100.) } else { None }, - cx, - { - let toolbar = self.toolbar.clone(); - let toolbar_hidden = toolbar.read(cx).hidden(); - move |_, cx| { - Flex::column() - .with_children( - (!toolbar_hidden) - .then(|| ChildView::new(&toolbar, cx).expanded()), - ) - .with_child( - ChildView::new(active_item.as_any(), cx).flex(1., true), - ) - } - }, - ) - .flex(1., true) - }) - .with_child(ChildView::new(&self.tab_context_menu, cx)) - .into_any() - } else { - enum EmptyPane {} - let theme = theme::current(cx).clone(); - - dragged_item_receiver::(self, 0, 0, false, None, cx, |_, cx| { - self.render_blank_pane(&theme, cx) - }) - .on_down(MouseButton::Left, |_, _, cx| { - cx.focus_parent(); - }) - .into_any() - } - }) - .on_down( - MouseButton::Navigate(NavigationDirection::Back), - move |_, pane, cx| { - if let Some(workspace) = pane.workspace.upgrade(cx) { - let pane = cx.weak_handle(); - cx.window_context().defer(move |cx| { - workspace.update(cx, |workspace, cx| { - workspace.go_back(pane, cx).detach_and_log_err(cx) - }) - }) - } - }, - ) - .on_down(MouseButton::Navigate(NavigationDirection::Forward), { - move |_, pane, cx| { - if let Some(workspace) = pane.workspace.upgrade(cx) { - let pane = cx.weak_handle(); - cx.window_context().defer(move |cx| { - workspace.update(cx, |workspace, cx| { - workspace.go_forward(pane, cx).detach_and_log_err(cx) - }) - }) - } - } - }) - .into_any_named("pane") - } - - fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext) { - if !self.has_focus { - self.has_focus = true; - cx.emit(Event::Focus); - cx.notify(); + fn handle_drag_move(&mut self, event: &DragMoveEvent, cx: &mut ViewContext) { + if !self.can_split { + return; } - self.toolbar.update(cx, |toolbar, cx| { - toolbar.focus_changed(true, cx); - }); + 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 let Some(active_item) = self.active_item() { - if cx.is_self_focused() { - // 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.id()) - { - if let Some(last_focused_view) = weak_last_focused_view.upgrade(cx) { - cx.focus(&last_focused_view); - return; - } else { - self.last_focused_view_by_item.remove(&active_item.id()); + 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); } - } - - cx.focus(active_item.as_any()); - } else if focused != self.tab_bar_context_menu.handle { - self.last_focused_view_by_item - .insert(active_item.id(), focused.downgrade()); - } - } + workspace.move_item(from_pane, to_pane, item_id, ix, cx); + }); + }) + .log_err(); } - fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - self.has_focus = false; - self.toolbar.update(cx, |toolbar, cx| { - toolbar.focus_changed(false, cx); - }); - cx.notify(); + 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(); } - fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) { - Self::reset_to_default_keymap_context(keymap); + 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) -> 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::(cx.listener(Self::handle_drag_move)) + .on_drag_move::(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::("", |style| style.visible()) + .group_drag_over::("", |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(&mut self, data: Option, cx: &mut WindowContext) { + pub fn push(&mut self, data: Option, cx: &mut WindowContext) { self.history.push(data, self.item.clone(), cx); } @@ -2014,7 +2058,7 @@ impl NavHistory { cx: &AppContext, mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option)), ) { - let borrowed_history = self.0.borrow(); + let borrowed_history = self.0.lock(); borrowed_history .forward_stack .iter() @@ -2025,7 +2069,7 @@ impl NavHistory { borrowed_history.paths_by_item.get(&entry.item.id()) { f(entry, project_and_abs_path.clone()); - } else if let Some(item) = entry.item.upgrade(cx) { + } else if let Some(item) = entry.item.upgrade() { if let Some(path) = item.project_path(cx) { f(entry, (path, None)); } @@ -2034,23 +2078,23 @@ impl NavHistory { } pub fn set_mode(&mut self, mode: NavigationMode) { - self.0.borrow_mut().mode = mode; + self.0.lock().mode = mode; } pub fn mode(&self) -> NavigationMode { - self.0.borrow().mode + self.0.lock().mode } pub fn disable(&mut self) { - self.0.borrow_mut().mode = NavigationMode::Disabled; + self.0.lock().mode = NavigationMode::Disabled; } pub fn enable(&mut self) { - self.0.borrow_mut().mode = NavigationMode::Normal; + self.0.lock().mode = NavigationMode::Normal; } pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option { - let mut state = self.0.borrow_mut(); + let mut state = self.0.lock(); let entry = match mode { NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => { return None @@ -2066,13 +2110,13 @@ impl NavHistory { entry } - pub fn push( + pub fn push( &mut self, data: Option, - item: Rc, + item: Arc, cx: &mut WindowContext, ) { - let state = &mut *self.0.borrow_mut(); + let state = &mut *self.0.lock(); match state.mode { NavigationMode::Disabled => {} NavigationMode::Normal | NavigationMode::ReopeningClosedItem => { @@ -2081,7 +2125,7 @@ impl NavHistory { } state.backward_stack.push_back(NavigationEntry { item, - data: data.map(|data| Box::new(data) as Box), + data: data.map(|data| Box::new(data) as Box), timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), }); state.forward_stack.clear(); @@ -2092,7 +2136,7 @@ impl NavHistory { } state.forward_stack.push_back(NavigationEntry { item, - data: data.map(|data| Box::new(data) as Box), + data: data.map(|data| Box::new(data) as Box), timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), }); } @@ -2102,7 +2146,7 @@ impl NavHistory { } state.backward_stack.push_back(NavigationEntry { item, - data: data.map(|data| Box::new(data) as Box), + data: data.map(|data| Box::new(data) as Box), timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), }); } @@ -2112,7 +2156,7 @@ impl NavHistory { } state.closed_stack.push_back(NavigationEntry { item, - data: data.map(|data| Box::new(data) as Box), + data: data.map(|data| Box::new(data) as Box), timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), }); } @@ -2120,8 +2164,8 @@ impl NavHistory { state.did_update(cx); } - pub fn remove_item(&mut self, item_id: usize) { - let mut state = self.0.borrow_mut(); + 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 @@ -2134,14 +2178,14 @@ impl NavHistory { .retain(|entry| entry.item.id() != item_id); } - pub fn path_for_item(&self, item_id: usize) -> Option<(ProjectPath, Option)> { - self.0.borrow().paths_by_item.get(&item_id).cloned() + pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option)> { + 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) { + if let Some(pane) = self.pane.upgrade() { cx.defer(move |cx| { pane.update(cx, |pane, cx| pane.history_updated(cx)); }); @@ -2149,98 +2193,6 @@ impl NavHistoryState { } } -pub struct PaneBackdrop { - child_view: usize, - child: AnyElement, -} - -impl PaneBackdrop { - pub fn new(pane_item_view: usize, child: AnyElement) -> Self { - PaneBackdrop { - child, - child_view: pane_item_view, - } - } -} - -impl Element for PaneBackdrop { - type LayoutState = (); - - type PaintState = (); - - fn layout( - &mut self, - constraint: gpui::SizeConstraint, - view: &mut V, - cx: &mut ViewContext, - ) -> (Vector2F, Self::LayoutState) { - let size = self.child.layout(constraint, view, cx); - (size, ()) - } - - fn paint( - &mut self, - bounds: RectF, - visible_bounds: RectF, - _: &mut Self::LayoutState, - view: &mut V, - cx: &mut ViewContext, - ) -> Self::PaintState { - let background = theme::current(cx).editor.background; - - let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); - - cx.scene().push_quad(gpui::Quad { - bounds: RectF::new(bounds.origin(), bounds.size()), - background: Some(background), - ..Default::default() - }); - - let child_view_id = self.child_view; - cx.scene().push_mouse_region( - MouseRegion::new::(child_view_id, 0, visible_bounds).on_down( - gpui::platform::MouseButton::Left, - move |_, _: &mut V, cx| { - let window = cx.window(); - cx.app_context().focus(window, Some(child_view_id)) - }, - ), - ); - - cx.scene().push_layer(Some(bounds)); - self.child.paint(bounds.origin(), visible_bounds, view, cx); - cx.scene().pop_layer(); - } - - fn rect_for_text_range( - &self, - range_utf16: std::ops::Range, - _bounds: RectF, - _visible_bounds: RectF, - _layout: &Self::LayoutState, - _paint: &Self::PaintState, - view: &V, - cx: &gpui::ViewContext, - ) -> Option { - self.child.rect_for_text_range(range_utf16, view, cx) - } - - fn debug( - &self, - _bounds: RectF, - _layout: &Self::LayoutState, - _paint: &Self::PaintState, - view: &V, - cx: &gpui::ViewContext, - ) -> serde_json::Value { - gpui::json::json!({ - "type": "Pane Back Drop", - "view": self.child_view, - "child": self.child.debug(view, cx), - }) - } -} - fn dirty_message_for(buffer_path: Option) -> String { let path = buffer_path .as_ref() @@ -2254,19 +2206,19 @@ fn dirty_message_for(buffer_path: Option) -> String { mod tests { use super::*; use crate::item::test::{TestItem, TestProjectItem}; - use gpui::TestAppContext; + 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.background()); + let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, None, cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let workspace = window.root(cx); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + 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 @@ -2277,21 +2229,19 @@ mod tests { #[gpui::test] async fn test_add_item_with_new_item(cx: &mut TestAppContext) { - cx.foreground().forbid_parking(); init_test(cx); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, None, cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let workspace = window.root(cx); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + 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.add_view(|_| TestItem::new().with_label("D"))), + Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))), false, false, Some(0), @@ -2304,7 +2254,7 @@ mod tests { set_labeled_items(&pane, ["A", "B*", "C"], cx); pane.update(cx, |pane, cx| { pane.add_item( - Box::new(cx.add_view(|_| TestItem::new().with_label("D"))), + Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))), false, false, Some(2), @@ -2317,7 +2267,7 @@ mod tests { set_labeled_items(&pane, ["A", "B*", "C"], cx); pane.update(cx, |pane, cx| { pane.add_item( - Box::new(cx.add_view(|_| TestItem::new().with_label("D"))), + Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))), false, false, Some(5), @@ -2331,7 +2281,7 @@ mod tests { set_labeled_items(&pane, ["A*", "B", "C"], cx); pane.update(cx, |pane, cx| { pane.add_item( - Box::new(cx.add_view(|_| TestItem::new().with_label("D"))), + Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))), false, false, None, @@ -2344,7 +2294,7 @@ mod tests { set_labeled_items(&pane, ["A", "B", "C*"], cx); pane.update(cx, |pane, cx| { pane.add_item( - Box::new(cx.add_view(|_| TestItem::new().with_label("D"))), + Box::new(cx.new_view(|cx| TestItem::new(cx).with_label("D"))), false, false, None, @@ -2356,14 +2306,12 @@ mod tests { #[gpui::test] async fn test_add_item_with_existing_item(cx: &mut TestAppContext) { - cx.foreground().forbid_parking(); init_test(cx); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, None, cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let workspace = window.root(cx); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + 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 @@ -2433,66 +2381,95 @@ mod tests { #[gpui::test] async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) { - cx.foreground().forbid_parking(); init_test(cx); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, None, cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let workspace = window.root(cx); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + 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| { - let item = TestItem::new() - .with_singleton(true) - .with_label("buffer 1") - .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)]); - - pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, 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| { - let item = TestItem::new() - .with_singleton(true) - .with_label("buffer 1") - .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]); - - pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, 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| { - let item = TestItem::new() - .with_singleton(true) - .with_label("buffer 2") - .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)]); - pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, 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| { - let item = TestItem::new() - .with_singleton(false) - .with_label("multibuffer 1") - .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]); - - pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, 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| { - let item = TestItem::new() - .with_singleton(false) - .with_label("multibuffer 1b") - .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]); - - pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, 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, @@ -2504,12 +2481,11 @@ mod tests { #[gpui::test] async fn test_remove_item_ordering(cx: &mut TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, None, cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let workspace = window.root(cx); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + 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); @@ -2560,12 +2536,11 @@ mod tests { #[gpui::test] async fn test_close_inactive_items(cx: &mut TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, None, cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let workspace = window.root(cx); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + 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); @@ -2581,12 +2556,11 @@ mod tests { #[gpui::test] async fn test_close_clean_items(cx: &mut TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, None, cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let workspace = window.root(cx); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + 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); @@ -2605,12 +2579,11 @@ mod tests { #[gpui::test] async fn test_close_items_to_the_left(cx: &mut TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, None, cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let workspace = window.root(cx); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + 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); @@ -2626,12 +2599,11 @@ mod tests { #[gpui::test] async fn test_close_items_to_the_right(cx: &mut TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, None, cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let workspace = window.root(cx); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + 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); @@ -2647,12 +2619,11 @@ mod tests { #[gpui::test] async fn test_close_all_items(cx: &mut TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, None, cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let workspace = window.root(cx); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + 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); @@ -2678,40 +2649,42 @@ mod tests { }) .unwrap(); - cx.foreground().run_until_parked(); - window.simulate_prompt_answer(2, cx); + 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| { - cx.set_global(SettingsStore::test(cx)); - theme::init((), 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: &ViewHandle, + pane: &View, label: &str, is_dirty: bool, - cx: &mut TestAppContext, - ) -> Box> { + cx: &mut VisualTestContext, + ) -> Box> { pane.update(cx, |pane, cx| { - let labeled_item = - Box::new(cx.add_view(|_| TestItem::new().with_label(label).with_dirty(is_dirty))); + 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( - pane: &ViewHandle, + pane: &View, labels: [&str; COUNT], - cx: &mut TestAppContext, - ) -> [Box>; COUNT] { + cx: &mut VisualTestContext, + ) -> [Box>; COUNT] { pane.update(cx, |pane, cx| { pane.items.clear(); let mut active_item_index = 0; @@ -2723,7 +2696,7 @@ mod tests { active_item_index = index; } - let labeled_item = Box::new(cx.add_view(|_| TestItem::new().with_label(label))); + 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 @@ -2737,19 +2710,19 @@ mod tests { // Assert the item label, with the active item label suffixed with a '*' fn assert_item_labels( - pane: &ViewHandle, + pane: &View, expected_states: [&str; COUNT], - cx: &mut TestAppContext, + cx: &mut VisualTestContext, ) { - pane.read_with(cx, |pane, cx| { + pane.update(cx, |pane, cx| { let actual_states = pane .items .iter() .enumerate() .map(|(ix, item)| { let mut state = item - .as_any() - .downcast_ref::() + .to_any() + .downcast::() .unwrap() .read(cx) .label @@ -2771,3 +2744,16 @@ mod tests { }) } } + +impl Render for DraggedTab { + fn render(&mut self, cx: &mut ViewContext) -> 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) + } +} diff --git a/crates/workspace/src/pane/dragged_item_receiver.rs b/crates/workspace/src/pane/dragged_item_receiver.rs index e4d0a636b8..3e1f6393a6 100644 --- a/crates/workspace/src/pane/dragged_item_receiver.rs +++ b/crates/workspace/src/pane/dragged_item_receiver.rs @@ -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( 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 } diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index aef03dcda0..c6eaa71663 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -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) -> Self { + pub fn new(pane: View) -> Self { Self { root: Member::Pane(pane), } @@ -35,8 +34,8 @@ impl PaneGroup { pub fn split( &mut self, - old_pane: &ViewHandle, - new_pane: &ViewHandle, + old_pane: &View, + new_pane: &View, direction: SplitDirection, ) -> Result<()> { match &mut self.root { @@ -52,14 +51,14 @@ impl PaneGroup { } } - pub fn bounding_box_for_pane(&self, pane: &ViewHandle) -> Option { + pub fn bounding_box_for_pane(&self, pane: &View) -> Option> { 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> { + pub fn pane_at_pixel_position(&self, coordinate: Point) -> Option<&View> { 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) -> Result { + pub fn remove(&mut self, pane: &View) -> Result { match &mut self.root { Member::Pane(_) => Ok(false), Member::Axis(axis) => { @@ -82,7 +81,7 @@ impl PaneGroup { } } - pub fn swap(&mut self, from: &ViewHandle, to: &ViewHandle) { + pub fn swap(&mut self, from: &View, to: &View) { 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, - theme: &Theme, - follower_states: &HashMap, FollowerState>, - active_call: Option<&ModelHandle>, - active_pane: &ViewHandle, - zoomed: Option<&AnyViewHandle>, + project: &Model, + follower_states: &HashMap, FollowerState>, + active_call: Option<&Model>, + active_pane: &View, + zoomed: Option<&AnyWeakView>, app_state: &Arc, cx: &mut ViewContext, - ) -> AnyElement { + ) -> 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> { + pub(crate) fn panes(&self) -> Vec<&View> { let mut panes = Vec::new(); self.root.collect_panes(&mut panes); panes } + + pub(crate) fn first_pane(&self) -> View { + self.root.first_pane() + } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone)] pub(crate) enum Member { Axis(PaneAxis), - Pane(ViewHandle), + Pane(View), } impl Member { - fn new_axis( - old_pane: ViewHandle, - new_pane: ViewHandle, - direction: SplitDirection, - ) -> Self { + fn new_axis(old_pane: View, new_pane: View, direction: SplitDirection) -> Self { use Axis::*; use SplitDirection::*; @@ -148,52 +145,52 @@ impl Member { Member::Axis(PaneAxis::new(axis, members)) } - fn contains(&self, needle: &ViewHandle) -> bool { + fn contains(&self, needle: &View) -> bool { match self { Member::Axis(axis) => axis.members.iter().any(|member| member.contains(needle)), Member::Pane(pane) => pane == needle, } } + fn first_pane(&self) -> View { + match self { + Member::Axis(axis) => axis.members[0].first_pane(), + Member::Pane(pane) => pane.clone(), + } + } + pub fn render( &self, - project: &ModelHandle, + project: &Model, basis: usize, - theme: &Theme, - follower_states: &HashMap, FollowerState>, - active_call: Option<&ModelHandle>, - active_pane: &ViewHandle, - zoomed: Option<&AnyViewHandle>, + follower_states: &HashMap, FollowerState>, + active_call: Option<&Model>, + active_pane: &View, + zoomed: Option<&AnyWeakView>, app_state: &Arc, cx: &mut ViewContext, - ) -> AnyElement { - 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::( - 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, + ), ) - .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(), + .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( - 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>) { + fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a View>) { 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, - pub flexes: Rc>>, - pub bounding_boxes: Rc>>>, + pub flexes: Arc>>, + pub bounding_boxes: Arc>>>>, } impl PaneAxis { pub fn new(axis: Axis, members: Vec) -> 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, - new_pane: &ViewHandle, + old_pane: &View, + new_pane: &View, 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) -> Result> { + fn remove(&mut self, pane_to_remove: &View) -> Result> { 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, to: &ViewHandle) { + fn swap(&mut self, from: &View, to: &View) { 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) -> Option { - debug_assert!(self.members.len() == self.bounding_boxes.borrow().len()); + fn bounding_box_for_pane(&self, pane: &View) -> Option> { + 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> { - debug_assert!(self.members.len() == self.bounding_boxes.borrow().len()); + fn pane_at_pixel_position(&self, coordinate: Point) -> Option<&View> { + 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: &Model, basis: usize, - theme: &Theme, - follower_states: &HashMap, FollowerState>, - active_call: Option<&ModelHandle>, - active_pane: &ViewHandle, - zoomed: Option<&AnyViewHandle>, + follower_states: &HashMap, FollowerState>, + active_call: Option<&Model>, + active_pane: &View, + zoomed: Option<&AnyWeakView>, app_state: &Arc, cx: &mut ViewContext, - ) -> AnyElement { - 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 { 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, length: Pixels) -> Bounds { 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>>, + bounding_boxes: Arc>>>>, + ) -> PaneAxisElement { + PaneAxisElement { + axis, + basis, + flexes, + bounding_boxes, + children: SmallVec::new(), + active_pane_ix: None, + } + } pub struct PaneAxisElement { axis: Axis, basis: usize, + flexes: Arc>>, + bounding_boxes: Arc>>>>, + children: SmallVec<[AnyElement; 2]>, active_pane_ix: Option, - flexes: Rc>>, - children: Vec>, - bounding_boxes: Rc>>>, } impl PaneAxisElement { - pub fn new( - axis: Axis, - basis: usize, - flexes: Rc>>, - bounding_boxes: Rc>>>, - ) -> 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) { + pub fn with_active_pane(mut self, active_pane_ix: Option) -> 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, - ) { - 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 child_size = if *remaining_flex == 0.0 { - *remaining_space - } else { - let space_per_flex = *remaining_space / *remaining_flex; - space_per_flex * flex - }; - - 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)); - } - } - - fn handle_resize( - flexes: Rc>>, + fn compute_resize( + flexes: &Arc>>, + e: &MouseMoveEvent, + ix: usize, axis: Axis, - preceding_ix: usize, - child_start: Vector2F, - drag_bounds: RectF, - ) -> impl Fn(MouseDrag, &mut Workspace, &mut EventContext) { + child_start: Point, + container_size: Size, + 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]| { - drag_bounds.length_along(axis) * (flexes[ix] / flexes.len() as f32) + container_size.along(axis) * (flexes[ix] / flexes.len() as f32) }; - 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(); + // 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; + } - // 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; - } + let mut proposed_current_pixel_change = + (e.position - child_start).along(axis) - size(ix, flexes.as_slice()); - let mut proposed_current_pixel_change = (drag.position - child_start).along(axis) - - size(preceding_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 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) - }; - - 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; - - result - } - }); - - while proposed_current_pixel_change.abs() > 0. { - let Some(current_ix) = successors.next() else { - break; + 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) }; - let next_target_size = f32::max( - size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change, - min_size, - ); + ix_offset += 1; - let current_target_size = f32::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; + result } + }); - workspace.schedule_serialize(cx); - cx.notify(); + 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(); } - } - impl Extend> for PaneAxisElement { - fn extend>>(&mut self, children: T) { - self.children.extend(children); - } - } - - impl Element for PaneAxisElement { - type LayoutState = f32; - type PaintState = (); - - fn layout( - &mut self, - constraint: SizeConstraint, - view: &mut Workspace, - cx: &mut ViewContext, - ) -> (Vector2F, Self::LayoutState) { - debug_assert!(self.children.len() == self.flexes.borrow().len()); - - let active_pane_magnification = - settings::get::(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"); - } - - 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 push_handle( + flexes: Arc>>, + dragged_handle: Rc>>, + axis: Axis, + ix: usize, + pane_bounds: Bounds, + axis_bounds: Bounds, + 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)), }; - 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())); - } + 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, + }) + } - if size.x() > constraint.max.x() { - size.set_x(constraint.max.x()); - } - if size.y() > constraint.max.y() { - size.set_y(constraint.max.y()); - } + cx.add_opaque_layer(handle_bounds); + cx.paint_quad(gpui::fill(divider_bounds, cx.theme().colors().border)); - (size, remaining_space) + 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 { + Some(self.basis.into()) + } + + fn into_element(self) -> Self::Element { + self + } + } + + impl Element for PaneAxisElement { + type State = Rc>>; + + fn request_layout( + &mut self, + state: Option, + 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, - ) -> Self::PaintState { - let can_resize = settings::get::(cx).active_pane_magnification == 1.; - let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); + bounds: gpui::Bounds, + 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 overflowing = *remaining_space < 0.; - if overflowing { - cx.scene().push_layer(Some(visible_bounds)); - } + let mut origin = bounds.origin; + let space_per_flex = bounds.size.along(self.axis) / len as f32; - let mut child_origin = bounds.origin(); - - 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); + 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]); - bounding_boxes.push(Some(RectF::new(child_origin, child.size()))); + 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, + ); + } + }); - 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::( - 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); - - cx.scene().pop_stacking_context(); - } + origin = origin.apply_along(self.axis, |val| val + child_size.along(self.axis)); } - if overflowing { - cx.scene().pop_layer(); - } - } - - fn rect_for_text_range( - &self, - range_utf16: Range, - _: RectF, - _: RectF, - _: &Self::LayoutState, - _: &Self::PaintState, - view: &Workspace, - cx: &ViewContext, - ) -> Option { - self.children - .iter() - .find_map(|child| child.rect_for_text_range(range_utf16.clone(), view, cx)) - } - - fn debug( - &self, - bounds: RectF, - _: &Self::LayoutState, - _: &Self::PaintState, - view: &Workspace, - cx: &ViewContext, - ) -> 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::>() + 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::() - flexes.len() as f32).abs() < 0.001 + } } diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 2a4062c079..5358ee3f4c 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -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::>(&flexes)) + .map(|flexes: String| serde_json::from_str::>(&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 { + fn group(axis: Axis, children: Vec) -> 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![ diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 5f4c29cd5b..f204e5152c 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -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: &Model, workspace_id: WorkspaceId, - workspace: &WeakViewHandle, - cx: &mut AsyncAppContext, - ) -> Option<( - Member, - Option>, - Vec>>, - )> { + workspace: WeakView, + cx: &mut AsyncWindowContext, + ) -> Option<(Member, Option>, Vec>>)> { 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, - pane: &WeakViewHandle, + project: &Model, + pane: &WeakView, workspace_id: WorkspaceId, - workspace: &WeakViewHandle, - cx: &mut AsyncAppContext, + workspace: WeakView, + cx: &mut AsyncWindowContext, ) -> Result>>> { 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 { diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index ddde5c3554..59202cbbaf 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -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 { 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, - ) -> Option; + fn clear_matches(&mut self, cx: &mut ViewContext); fn update_matches(&mut self, matches: Vec, cx: &mut ViewContext); fn query_suggestion(&mut self, cx: &mut ViewContext) -> String; @@ -95,7 +94,7 @@ pub trait SearchableItemHandle: ItemHandle { fn subscribe_to_search_events( &self, cx: &mut WindowContext, - handler: Box, + handler: Box, ) -> Subscription; fn clear_matches(&self, cx: &mut WindowContext); fn update_matches(&self, matches: &Vec>, cx: &mut WindowContext); @@ -128,7 +127,8 @@ pub trait SearchableItemHandle: ItemHandle { ) -> Option; } -impl SearchableItemHandle for ViewHandle { +// todo!("here is where we need to use AnyWeakView"); +impl SearchableItemHandle for View { fn downgrade(&self) -> Box { Box::new(self.downgrade()) } @@ -144,14 +144,9 @@ impl SearchableItemHandle for ViewHandle { fn subscribe_to_search_events( &self, cx: &mut WindowContext, - handler: Box, + handler: Box, ) -> 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 SearchableItemHandle for ViewHandle { cx: &mut WindowContext, ) -> Task>> { 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(matches: &Vec>) -> Vec> for AnyViewHandle { +impl From> for AnyView { fn from(this: Box) -> Self { - this.as_any().clone() + this.to_any().clone() } } -impl From<&Box> for AnyViewHandle { +impl From<&Box> for AnyView { fn from(this: &Box) -> Self { - this.as_any().clone() + this.to_any().clone() } } impl PartialEq for Box { 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 {} pub trait WeakSearchableItemHandle: WeakItemHandle { fn upgrade(&self, cx: &AppContext) -> Option>; - fn into_any(self) -> AnyWeakViewHandle; + // fn into_any(self) -> AnyWeakView; } -impl WeakSearchableItemHandle for WeakViewHandle { - fn upgrade(&self, cx: &AppContext) -> Option> { - Some(Box::new(self.upgrade(cx)?)) +impl WeakSearchableItemHandle for WeakView { + fn upgrade(&self, _cx: &AppContext) -> Option> { + 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 { 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 {} impl std::hash::Hash for Box { fn hash(&self, state: &mut H) { - (self.id(), self.window().id()).hash(state) + self.id().hash(state) } } diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs index b99c5f3ab9..dbbe7de6a1 100644 --- a/crates/workspace/src/shared_screen.rs +++ b/crates/workspace/src/shared_screen.rs @@ -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, nav_history: Option, _maintain_frame: Task>, + focus: FocusHandle, } impl SharedScreen { @@ -38,6 +35,7 @@ impl SharedScreen { user: Arc, cx: &mut ViewContext, ) -> 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 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) -> AnyElement { - enum Focus {} - - let frame = self.frame.clone(); - MouseEventHandler::new::(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) -> 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> { + type Event = Event; + + fn tab_tooltip_text(&self, _: &AppContext) -> Option { Some(format!("{}'s screen", self.user.github_login).into()) } + fn deactivated(&mut self, cx: &mut ViewContext) { if let Some(nav_history) = self.nav_history.as_mut() { nav_history.push::<()>(None, cx); } } - fn tab_content( + fn tab_content( &self, _: Option, - style: &theme::Tab, - _: &AppContext, - ) -> gpui::AnyElement { - 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, - ) -> Option { + ) -> Option> { 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), } } } diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index b62dae2114..bfa1a8f8ba 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -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>, right_items: Vec>, - active_pane: ViewHandle, + active_pane: View, _observe_active_pane: Subscription, } -impl Entity for StatusBar { - type Event = (); -} - -impl View for StatusBar { - fn ui_name() -> &'static str { - "StatusBar" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - 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() +impl Render for StatusBar { + fn render(&mut self, cx: &mut ViewContext) -> 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 { - pub fn new(active_pane: &ViewHandle, cx: &mut ViewContext) -> Self { + fn render_left_tools(&self, _: &mut ViewContext) -> 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) -> 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, cx: &mut ViewContext) -> 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(&mut self, item: ViewHandle, cx: &mut ViewContext) + pub fn add_left_item(&mut self, item: View, cx: &mut ViewContext) 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(&self) -> Option> { + pub fn item_of_type(&self) -> Option> { 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(&self) -> Option @@ -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::() { 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::() { return Some(index + self.left_items.len()); } } @@ -123,11 +115,14 @@ impl StatusBar { pub fn insert_item_after( &mut self, position: usize, - item: ViewHandle, + item: View, cx: &mut ViewContext, ) 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(&mut self, item: ViewHandle, cx: &mut ViewContext) + pub fn add_right_item(&mut self, item: View, cx: &mut ViewContext) 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, cx: &mut ViewContext) { + pub fn set_active_pane(&mut self, active_pane: &View, cx: &mut ViewContext) { 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 StatusItemViewHandle for ViewHandle { - fn as_any(&self) -> &AnyViewHandle { - self +impl StatusItemViewHandle for View { + fn to_any(&self) -> AnyView { + self.clone().into() } fn set_active_pane_item( @@ -184,88 +182,13 @@ impl StatusItemViewHandle for ViewHandle { }); } - fn ui_name(&self) -> &'static str { - T::ui_name() + fn item_type(&self) -> TypeId { + TypeId::of::() } } -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, - right: AnyElement, -} - -impl Element for StatusBarElement { - type LayoutState = (); - type PaintState = (); - - fn layout( - &mut self, - mut constraint: SizeConstraint, - view: &mut StatusBar, - cx: &mut ViewContext, - ) -> (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, - ) -> 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, - _: RectF, - _: RectF, - _: &Self::LayoutState, - _: &Self::PaintState, - _: &StatusBar, - _: &ViewContext, - ) -> Option { - None - } - - fn debug( - &self, - bounds: RectF, - _: &Self::LayoutState, - _: &Self::PaintState, - _: &StatusBar, - _: &ViewContext, - ) -> serde_json::Value { - json!({ - "type": "StatusBarElement", - "bounds": bounds.to_json() - }) + val.to_any().clone() } } diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index c3f4bb9723..dc17cd3c19 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -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 { fn set_active_pane_item( &mut self, active_pane_item: Option<&dyn crate::ItemHandle>, cx: &mut ViewContext, ) -> 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) {} /// 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) -> 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, 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) -> AnyElement { - 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 { + 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 { + 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); - - 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") - } + fn secondary_items(&self) -> impl Iterator { + self.items.iter().filter_map(|(item, location)| { + if *location == ToolbarItemLocation::Secondary { + Some(item.as_ref()) + } else { + None + } + }) } } -// <<<<<<< HEAD -// ======= -// #[allow(clippy::too_many_arguments)] -// fn nav_button)>( -// svg_path: &'static str, -// style: theme::Interactive, -// nav_button_height: f32, -// tooltip_style: TooltipStyle, -// enabled: bool, -// spacing: f32, -// on_click: F, -// tooltip_action: A, -// action_name: &'static str, -// cx: &mut ViewContext, -// ) -> AnyElement { -// MouseEventHandler::new::(0, cx, |state, _| { -// let style = if enabled { -// style.style_for(state) +impl Render for Toolbar { + fn render(&mut self, cx: &mut ViewContext) -> 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) -> AnyElement { +// 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::( -// 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(&mut self, item: ViewHandle, cx: &mut ViewContext) + pub fn add_item(&mut self, item: View, cx: &mut ViewContext) 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(&self) -> Option> { + pub fn item_of_type(&self) -> Option> { 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 ToolbarItemViewHandle for ViewHandle { - fn id(&self) -> usize { - self.id() +impl ToolbarItemViewHandle for View { + 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 ToolbarItemViewHandle for ViewHandle { } 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() +// } +// } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ee2a8ab104..76715f69be 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1,5 +1,6 @@ pub mod dock; pub mod item; +mod modal_layer; pub mod notifications; pub mod pane; pub mod pane_group; @@ -10,109 +11,78 @@ 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 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, - 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 pane::*; -pub use pane_group::*; -use persistence::{model::SerializedItem, DB}; -pub use persistence::{ - model::{ItemId, WorkspaceLocation}, - WorkspaceDb, DB as WORKSPACE_DB, -}; -use postage::prelude::Stream; -use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; -use serde::Deserialize; -use shared_screen::SharedScreen; -use status_bar::StatusBar; -pub use status_bar::StatusItemView; -use theme::{Theme, ThemeSettings}; -pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; +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 = env::var("ZED_WINDOW_SIZE") + static ref ZED_WINDOW_SIZE: Option> = env::var("ZED_WINDOW_SIZE") + .ok() + .as_deref() + .and_then(parse_pixel_size_env_var); + static ref ZED_WINDOW_POSITION: Option> = env::var("ZED_WINDOW_POSITION") .ok() .as_deref() .and_then(parse_pixel_position_env_var); - static ref ZED_WINDOW_POSITION: Option = 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 ModalHandle for ViewHandle { - fn as_any(&self) -> &AnyViewHandle { - self - } - - fn has_focus(&self, cx: &WindowContext) -> bool { - self.read(cx).has_focus() - } } #[derive(Clone, PartialEq)] @@ -183,6 +153,20 @@ pub struct CloseAllItemsAndPanes { pub save_intent: Option, } +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::(cx); - settings::register::(cx); + WorkspaceSettings::register(cx); + ItemSettings::register(cx); } pub fn init(app_state: Arc, 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| { - let pane = workspace.active_pane().clone(); - workspace.unfollow(&pane, cx); - }, - ); - cx.add_action( - |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext| { - 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 - .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, AnyModelHandle, &mut ViewContext) -> Box, ->; +type ProjectItemBuilders = + HashMap, AnyModel, &mut ViewContext) -> Box>; pub fn register_project_item(cx: &mut AppContext) { - cx.update_default_global(|builders: &mut ProjectItemBuilders, _| { - builders.insert(TypeId::of::(), |project, model, cx| { - let item = model.downcast::().unwrap(); - Box::new(cx.add_view(|cx| I::for_project_item(project, item, cx))) - }); + let builders = cx.default_global::(); + builders.insert(TypeId::of::(), |project, model, cx| { + let item = model.downcast::().unwrap(); + Box::new(cx.new_view(|cx| I::for_project_item(project, item, cx))) }); } type FollowableItemBuilder = fn( - ViewHandle, - ViewHandle, + View, + View, ViewId, &mut Option, - &mut AppContext, + &mut WindowContext, ) -> Option>>>; type FollowableItemBuilders = HashMap< TypeId, ( FollowableItemBuilder, - fn(&AnyViewHandle) -> Box, + fn(&AnyView) -> Box, ), >; pub fn register_followable_item(cx: &mut AppContext) { - cx.update_default_global(|builders: &mut FollowableItemBuilders, _| { - builders.insert( - TypeId::of::(), - ( - |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::().unwrap()), - ), - ); - }); + let builders = cx.default_global::(); + builders.insert( + TypeId::of::(), + ( + |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::().unwrap()), + ), + ); } type ItemDeserializers = HashMap< Arc, fn( - ModelHandle, - WeakViewHandle, + Model, + WeakView, WorkspaceId, ItemId, &mut ViewContext, ) -> Task>>, >; pub fn register_deserializable_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::(); + 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, pub client: Arc, - pub user_store: ModelHandle, - pub workspace_store: ModelHandle, + pub user_store: Model, + pub workspace_store: Model, pub fs: Arc, pub build_window_options: - fn(Option, Option, &dyn Platform) -> WindowOptions<'static>, - pub initialize_workspace: - fn(WeakViewHandle, bool, Arc, AsyncAppContext) -> Task>, - pub background_actions: BackgroundActions, + fn(Option, Option, &mut AppContext) -> WindowOptions, pub node_runtime: Arc, } pub struct WorkspaceStore { - workspaces: HashSet>, + workspaces: HashSet>, followers: Vec, client: Arc, _subscriptions: Vec, @@ -483,17 +350,18 @@ impl AppState { use settings::SettingsStore; if !cx.has_global::() { - 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(&mut self, delay: Duration, cx: &mut ViewContext, func: F) where - F: 'static + FnOnce(&mut Workspace, &mut ViewContext) -> Task>, + F: 'static + Send + FnOnce(&mut Workspace, &mut ViewContext) -> Task>, { 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), + PaneAdded(View), ContactRequestedJoin(u64), + WorkspaceCreated(WeakView), } pub struct Workspace { - weak_self: WeakViewHandle, - modal: Option, - zoomed: Option, + weak_self: WeakView, + workspace_actions: Vec) -> Div>>, + zoomed: Option, zoomed_position: Option, center: PaneGroup, - left_dock: ViewHandle, - bottom_dock: ViewHandle, - right_dock: ViewHandle, - panes: Vec>, - panes_by_item: HashMap>, - active_pane: ViewHandle, - last_active_center_pane: Option>, + left_dock: View, + bottom_dock: View, + right_dock: View, + panes: Vec>, + panes_by_item: HashMap>, + active_pane: View, + last_active_center_pane: Option>, last_active_view_id: Option, - status_bar: ViewHandle, - titlebar_item: Option, + status_bar: View, + modal_layer: View, + titlebar_item: Option, notifications: Vec<(TypeId, usize, Box)>, - project: ModelHandle, - follower_states: HashMap, FollowerState>, - last_leaders_by_pane: HashMap, PeerId>, + project: Model, + follower_states: HashMap, FollowerState>, + last_leaders_by_pane: HashMap, PeerId>, window_edited: bool, - active_call: Option<(ModelHandle, Vec)>, + active_call: Option<(Model, Vec)>, leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, database_id: WorkspaceId, app_state: Arc, - subscriptions: Vec, + _subscriptions: Vec, _apply_leader_updates: Task>, _observe_current_user: Task>, _schedule_serialize: Option>, pane_history_timestamp: Arc, + bounds: Bounds, } -struct ActiveModal { - view: Box, - previously_focused_view_id: Option, -} +impl EventEmitter for Workspace {} #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub struct ViewId { @@ -613,12 +478,10 @@ struct FollowerState { items_by_leader_view_id: HashMap>, } -enum WorkspaceBounds {} - impl Workspace { pub fn new( workspace_id: WorkspaceId, - project: ModelHandle, + project: Model, app_state: Arc, cx: &mut ViewContext, ) -> 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::().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())); + cx.emit(Event::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| { + 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::, _, _>(|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::>() { - let call = cx.global::>().clone(); + if cx.has_global::>() { + let call = cx.global::>().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; + } + + 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.background() - .spawn(DB.set_window_bounds(workspace_id, bounds, display)) - .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::().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, requesting_window: Option>, cx: &mut AppContext, - ) -> Task<( - WeakViewHandle, - Vec, anyhow::Error>>>, - )> { + ) -> Task< + anyhow::Result<( + WindowHandle, + Vec, 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 = + 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?; + 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 { - 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; - } - } + // 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(); + window + .update(&mut cx, |_, cx| cx.activate_window()) + .log_err(); - (app_state.initialize_workspace)( - workspace.downgrade(), - serialized_workspace.is_some(), - app_state.clone(), - cx.clone(), - ) - .await - .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(); - window.update(&mut cx, |cx| cx.activate_window()); - - 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(); - - (workspace, opened_items) + Ok((window, opened_items)) }) } - pub fn weak_handle(&self) -> WeakViewHandle { + pub fn weak_handle(&self) -> WeakView { self.weak_self.clone() } - pub fn left_dock(&self) -> &ViewHandle { + pub fn left_dock(&self) -> &View { &self.left_dock } - pub fn bottom_dock(&self) -> &ViewHandle { + pub fn bottom_dock(&self) -> &View { &self.bottom_dock } - pub fn right_dock(&self) -> &ViewHandle { + pub fn right_dock(&self) -> &View { &self.right_dock } - pub fn add_panel(&mut self, panel: ViewHandle, cx: &mut ViewContext) - where - T::Event: std::fmt::Debug, - { - self.add_panel_with_extra_event_handler(panel, cx, |_, _, _, _| {}) - } - - pub fn add_panel_with_extra_event_handler( - &mut self, - panel: ViewHandle, - cx: &mut ViewContext, - handler: F, - ) where - T::Event: std::fmt::Debug, - F: Fn(&mut Self, &ViewHandle, &T::Event, &mut ViewContext) + 'static, - { + pub fn add_panel(&mut self, panel: View, cx: &mut ViewContext) { 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 { + pub fn status_bar(&self) -> &View { &self.status_bar } @@ -1063,11 +859,11 @@ impl Workspace { &self.app_state } - pub fn user_store(&self) -> &ModelHandle { + pub fn user_store(&self) -> &Model { &self.app_state.user_store } - pub fn project(&self) -> &ModelHandle { + pub fn project(&self) -> &Model { &self.project } @@ -1129,12 +925,13 @@ impl Workspace { fn navigate_history( &mut self, - pane: WeakViewHandle, + pane: WeakView, mode: NavigationMode, cx: &mut ViewContext, ) -> Task> { - let to_load = if let Some(pane) = pane.upgrade(cx) { - cx.focus(&pane); + let to_load = if let Some(pane) = pane.upgrade() { + // todo!("focus") + // cx.focus(&pane); pane.update(cx, |pane, cx| { loop { @@ -1144,7 +941,7 @@ impl Workspace { // If the item is still present in this pane, then activate it. if let Some(index) = entry .item - .upgrade(cx) + .upgrade() .and_then(|v| pane.index_for_item(v.as_ref())) { let prev_active_item_index = pane.active_item_index(); @@ -1184,12 +981,12 @@ impl Workspace { 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.id()) + 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.id()) != prev_active_item_id; + 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); @@ -1214,7 +1011,7 @@ impl Workspace { pub fn go_back( &mut self, - pane: WeakViewHandle, + pane: WeakView, cx: &mut ViewContext, ) -> Task> { self.navigate_history(pane, NavigationMode::GoingBack, cx) @@ -1222,7 +1019,7 @@ impl Workspace { pub fn go_forward( &mut self, - pane: WeakViewHandle, + pane: WeakView, cx: &mut ViewContext, ) -> Task> { self.navigate_history(pane, NavigationMode::GoingForward, cx) @@ -1240,12 +1037,12 @@ impl Workspace { &self.app_state.client } - pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext) { + pub fn set_titlebar_item(&mut self, item: AnyView, cx: &mut ViewContext) { self.titlebar_item = Some(item); cx.notify(); } - pub fn titlebar_item(&self) -> Option { + pub fn titlebar_item(&self) -> Option { self.titlebar_item.clone() } @@ -1267,23 +1064,20 @@ impl Workspace { } 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; + let (workspace, _) = task.await?; workspace.update(&mut cx, callback) }) } } - pub fn worktrees<'a>( - &self, - cx: &'a AppContext, - ) -> impl 'a + Iterator> { - self.project.read(cx).worktrees(cx) + pub fn worktrees<'a>(&self, cx: &'a AppContext) -> impl 'a + Iterator> { + self.project.read(cx).worktrees() } pub fn visible_worktrees<'a>( &self, cx: &'a AppContext, - ) -> impl 'a + Iterator> { + ) -> impl 'a + Iterator> { self.project.read(cx).visible_worktrees(cx) } @@ -1301,33 +1095,34 @@ impl Workspace { } pub fn close_global(_: &CloseWindow, cx: &mut AppContext) { - cx.spawn(|mut cx| async move { - let window = cx - .windows() - .into_iter() - .find(|window| window.is_active(&cx).unwrap_or(false)); - if let Some(window) = window { - //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(&mut cx); - } - }) - .detach(); + 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( - &mut self, - _: &CloseWindow, - cx: &mut ViewContext, - ) -> Option>> { - let window = cx.window(); + pub fn close_window(&mut self, _: &CloseWindow, cx: &mut ViewContext) { + let window = cx.window_handle(); let prepare = self.prepare_to_close(false, cx); - Some(cx.spawn(|_, mut cx| async move { + cx.spawn(|_, mut cx| async move { if prepare.await? { - window.remove(&mut cx); + window.update(&mut cx, |_, cx| { + cx.remove_window(); + })?; } - Ok(()) - })) + anyhow::Ok(()) + }) + .detach_and_log_err(cx) } pub fn prepare_to_close( @@ -1335,37 +1130,38 @@ impl Workspace { quitting: bool, cx: &mut ViewContext, ) -> Task> { + //todo!(saveing) let active_call = self.active_call().cloned(); - let window = cx.window(); + let window = cx.window_handle(); cx.spawn(|this, mut cx| async move { - let workspace_count = cx - .windows() - .into_iter() - .filter(|window| window.root_is::()) - .count(); + let workspace_count = (*cx).update(|cx| { + cx.windows() + .iter() + .filter(|window| window.downcast::().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()) + && active_call.read_with(&cx, |call, _| call.room().is_some())? { - let answer = window.prompt( - PromptLevel::Warning, - "Do you want to leave the current call?", - &["Close window and hang up", "Cancel"], - &mut cx, - ); + 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 let Some(mut answer) = answer { - if answer.next().await == Some(1) { - return anyhow::Ok(false); - } else { - active_call - .update(&mut cx, |call, cx| call.hang_up(cx)) - .await - .log_err(); - } + 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(); } } } @@ -1378,17 +1174,9 @@ impl Workspace { }) } - fn save_all( - &mut self, - action: &SaveAll, - cx: &mut ViewContext, - ) -> Option>> { - let save_all = - self.save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx); - Some(cx.foreground().spawn(async move { - save_all.await?; - Ok(()) - })) + fn save_all(&mut self, action: &SaveAll, cx: &mut ViewContext) { + self.save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx) + .detach_and_log_err(cx); } fn save_all_internal( @@ -1417,7 +1205,7 @@ impl Workspace { 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 mut answer = workspace.update(&mut cx, |_, cx| { + let answer = workspace.update(&mut cx, |_, cx| { let prompt = Pane::file_names_for_prompt( &mut dirty_items.iter().map(|(_, handle)| handle), dirty_items.len(), @@ -1429,7 +1217,7 @@ impl Workspace { &["Save all", "Discard all", "Cancel"], ) })?; - match answer.next().await { + match answer.await.log_err() { Some(0) => save_intent = SaveIntent::SaveAll, Some(1) => save_intent = SaveIntent::Skip, _ => {} @@ -1437,10 +1225,10 @@ impl Workspace { } for (pane, item) in dirty_items { let (singleton, project_entry_ids) = - cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx))); + cx.update(|_, cx| (item.is_singleton(cx), item.project_entry_ids(cx)))?; if singleton || !project_entry_ids.is_empty() { if let Some(ix) = - pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))? + pane.update(&mut cx, |pane, _| pane.index_for_item(item.as_ref()))? { if !Pane::save_item( project.clone(), @@ -1461,28 +1249,30 @@ impl Workspace { }) } - pub fn open(&mut self, _: &Open, cx: &mut ViewContext) -> Option>> { - let telemetry_settings = *settings::get::(cx); + pub fn open(&mut self, _: &Open, cx: &mut ViewContext) { + let telemetry_settings = TelemetrySettings::get_global(cx).clone(); self.client() .telemetry() .report_app_event(telemetry_settings, "open project", false); - let mut paths = cx.prompt_for_paths(PathPromptOptions { + let paths = cx.prompt_for_paths(PathPromptOptions { files: true, directories: true, multiple: true, }); - Some(cx.spawn(|this, mut cx| async move { - if let Some(paths) = paths.recv().await.flatten() { - if let Some(task) = this - .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx)) - .log_err() - { - task.await? - } + 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(); } - Ok(()) - })) + }) + .detach() } pub fn open_workspace_for_paths( @@ -1490,9 +1280,9 @@ impl Workspace { paths: Vec, cx: &mut ViewContext, ) -> Task> { - let window = cx.window().downcast::(); + let window = cx.window_handle().downcast::(); let is_remote = self.project.read(cx).is_remote(); - let has_worktree = self.project.read(cx).worktrees(cx).next().is_some(); + 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 @@ -1510,7 +1300,7 @@ impl Workspace { } else { None }; - cx.update(|cx| open_paths(&paths, &app_state, window_to_replace, cx)) + cx.update(|_, cx| open_paths(&paths, &app_state, window_to_replace, cx))? .await?; Ok(()) }) @@ -1523,13 +1313,13 @@ impl Workspace { visible: bool, cx: &mut ViewContext, ) -> Task, anyhow::Error>>>> { - log::info!("open paths {:?}", abs_paths); + 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(|this, mut cx| async move { + 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 @@ -1548,43 +1338,41 @@ impl Workspace { }; let this = this.clone(); - let task = cx.spawn(|mut cx| { - let fs = fs.clone(); - let abs_path = abs_path.clone(); - 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))); - }) - } + 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()?; - None - } + .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); @@ -1595,13 +1383,13 @@ impl Workspace { } fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext) { - let mut paths = cx.prompt_for_paths(PathPromptOptions { + 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.recv().await.flatten() { + if let Some(paths) = paths.await.log_err().flatten() { let results = this .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))? .await; @@ -1615,17 +1403,17 @@ impl Workspace { } fn project_path_for_path( - project: ModelHandle, + project: Model, abs_path: &Path, visible: bool, cx: &mut AppContext, - ) -> Task, ProjectPath)>> { + ) -> Task, ProjectPath)>> { let entry = project.update(cx, |project, cx| { project.find_or_create_local_worktree(abs_path, visible, cx) }); - cx.spawn(|cx| async move { + cx.spawn(|mut cx| async move { let (worktree, path) = entry.await?; - let worktree_id = worktree.read_with(&cx, |t, _| t.id()); + let worktree_id = worktree.update(&mut cx, |t, _| t.id())?; Ok(( worktree, ProjectPath { @@ -1636,63 +1424,6 @@ impl Workspace { }) } - /// Returns the modal that was toggled closed if it was open. - pub fn toggle_modal( - &mut self, - cx: &mut ViewContext, - add_view: F, - ) -> Option> - where - V: 'static + Modal, - F: FnOnce(&mut Self, &mut ViewContext) -> ViewHandle, - { - cx.notify(); - // Whatever modal was visible is getting clobbered. If its the same type as V, then return - // it. Otherwise, create a new modal and set it as active. - if let Some(already_open_modal) = self - .dismiss_modal(cx) - .and_then(|modal| modal.downcast::()) - { - cx.focus_self(); - Some(already_open_modal) - } else { - let modal = add_view(self, cx); - cx.subscribe(&modal, |this, _, event, cx| { - if V::dismiss_on_event(event) { - this.dismiss_modal(cx); - } - }) - .detach(); - let previously_focused_view_id = cx.focused_view_id(); - cx.focus(&modal); - self.modal = Some(ActiveModal { - view: Box::new(modal), - previously_focused_view_id, - }); - None - } - } - - pub fn modal(&self) -> Option> { - self.modal - .as_ref() - .and_then(|modal| modal.view.as_any().clone().downcast::()) - } - - pub fn dismiss_modal(&mut self, cx: &mut ViewContext) -> Option { - if let Some(modal) = self.modal.take() { - if let Some(previously_focused_view_id) = modal.previously_focused_view_id { - if modal.view.has_focus(cx) { - cx.window_context().focus(Some(previously_focused_view_id)); - } - } - cx.notify(); - Some(modal.view.as_any().clone()) - } else { - None - } - } - pub fn items<'a>( &'a self, cx: &'a AppContext, @@ -1700,14 +1431,14 @@ impl Workspace { self.panes.iter().flat_map(|pane| pane.read(cx).items()) } - pub fn item_of_type(&self, cx: &AppContext) -> Option> { - self.items_of_type(cx).max_by_key(|item| item.id()) + pub fn item_of_type(&self, cx: &AppContext) -> Option> { + 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> { + ) -> impl 'a + Iterator> { self.panes .iter() .flat_map(|pane| pane.read(cx).items_of_type()) @@ -1717,6 +1448,11 @@ impl Workspace { self.active_pane().read(cx).active_item() } + pub fn active_item_as(&self, cx: &AppContext) -> Option> { + let item = self.active_item(cx)?; + item.to_any().downcast::().ok() + } + fn active_project_path(&self, cx: &ViewContext) -> Option { self.active_item(cx).and_then(|item| item.project_path(cx)) } @@ -1747,16 +1483,18 @@ impl Workspace { &mut self, _: &CloseInactiveTabsAndPanes, cx: &mut ViewContext, - ) -> Option>> { + ) { 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, - ) -> Option>> { + ) { 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( @@ -1778,7 +1516,7 @@ impl Workspace { } for pane in self.panes() { - if retain_active_pane && pane.id() == current_pane.id() { + if retain_active_pane && pane.entity_id() == current_pane.entity_id() { continue; } @@ -1821,11 +1559,12 @@ impl Workspace { if let Some(active_panel) = dock.active_panel() { if was_visible { - if active_panel.has_focus(cx) { + if active_panel.focus_handle(cx).contains_focused(cx) { focus_center = true; } } else { - cx.focus(active_panel.as_any()); + let focus_handle = &active_panel.focus_handle(cx); + cx.focus(focus_handle); reveal_dock = true; } } @@ -1836,7 +1575,7 @@ impl Workspace { } if focus_center { - cx.focus_self(); + self.active_pane.update(cx, |pane, cx| pane.focus(cx)) } cx.notify(); @@ -1852,23 +1591,24 @@ impl Workspace { }); } - cx.focus_self(); + // todo!("focus") + // cx.focus_self(); cx.notify(); self.serialize_workspace(cx); } /// Transfer focus to the panel of the given type. - pub fn focus_panel(&mut self, cx: &mut ViewContext) -> Option> { - self.focus_or_unfocus_panel::(cx, |_, _| true)? - .as_any() - .clone() - .downcast() + pub fn focus_panel(&mut self, cx: &mut ViewContext) -> Option> { + let panel = self.focus_or_unfocus_panel::(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(&mut self, cx: &mut ViewContext) { - self.focus_or_unfocus_panel::(cx, |panel, cx| !panel.has_focus(cx)); + self.focus_or_unfocus_panel::(cx, |panel, cx| { + !panel.focus_handle(cx).contains_focused(cx) + }); } /// Focus or unfocus the given panel type, depending on the given callback. @@ -1876,11 +1616,10 @@ impl Workspace { &mut self, cx: &mut ViewContext, should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext) -> bool, - ) -> Option> { + ) -> Option> { for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { if let Some(panel_index) = dock.read(cx).panel_index_for_type::() { let mut focus_center = false; - let mut reveal_dock = false; let panel = dock.update(cx, |dock, cx| { dock.activate_panel(panel_index, cx); @@ -1888,12 +1627,8 @@ impl Workspace { if let Some(panel) = panel.as_ref() { if should_focus(&**panel, cx) { dock.set_open(true, cx); - cx.focus(panel.as_any()); - reveal_dock = true; + panel.focus_handle(cx).focus(cx); } else { - // if panel.is_zoomed(cx) { - // dock.set_open(false, cx); - // } focus_center = true; } } @@ -1901,7 +1636,7 @@ impl Workspace { }); if focus_center { - cx.focus_self(); + self.active_pane.update(cx, |pane, cx| pane.focus(cx)) } self.serialize_workspace(cx); @@ -1912,7 +1647,7 @@ impl Workspace { None } - pub fn panel(&self, cx: &WindowContext) -> Option> { + pub fn panel(&self, cx: &WindowContext) -> Option> { for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { let dock = dock.read(cx); if let Some(panel) = dock.panel::() { @@ -1922,6 +1657,8 @@ impl Workspace { None } + // todo!("implement zoom") + #[allow(unused)] fn zoom_out(&mut self, cx: &mut ViewContext) { for pane in &self.panes { pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); @@ -1936,10 +1673,10 @@ impl Workspace { cx.notify(); } - #[cfg(any(test, feature = "test-support"))] - pub fn zoomed_view(&self, cx: &AppContext) -> Option { - self.zoomed.and_then(|view| view.upgrade(cx)) - } + // #[cfg(any(test, feature = "test-support"))] + // pub fn zoomed_view(&self, cx: &AppContext) -> Option { + // self.zoomed.and_then(|view| view.upgrade(cx)) + // } fn dismiss_zoomed_items_to_reveal( &mut self, @@ -1960,7 +1697,7 @@ impl Workspace { if Some(dock.position()) != dock_to_reveal { if let Some(panel) = dock.active_panel() { if panel.is_zoomed(cx) { - focus_center |= panel.has_focus(cx); + focus_center |= panel.focus_handle(cx).contains_focused(cx); dock.set_open(false, cx); } } @@ -1969,7 +1706,7 @@ impl Workspace { } if focus_center { - cx.focus_self(); + self.active_pane.update(cx, |pane, cx| pane.focus(cx)) } if self.zoomed_position != dock_to_reveal { @@ -1980,19 +1717,19 @@ impl Workspace { cx.notify(); } - fn add_pane(&mut self, cx: &mut ViewContext) -> ViewHandle { - let pane = cx.add_view(|cx| { + fn add_pane(&mut self, cx: &mut ViewContext) -> View { + let pane = cx.new_view(|cx| { Pane::new( self.weak_handle(), self.project.clone(), - self.app_state.background_actions, self.pane_history_timestamp.clone(), + None, cx, ) }); cx.subscribe(&pane, Self::handle_pane_event).detach(); self.panes.push(pane.clone()); - cx.focus(&pane); + cx.focus_view(&pane); cx.emit(Event::PaneAdded(pane.clone())); pane } @@ -2003,7 +1740,7 @@ impl Workspace { cx: &mut ViewContext, ) -> bool { if let Some(center_pane) = self.last_active_center_pane.clone() { - if let Some(center_pane) = center_pane.upgrade(cx) { + if let Some(center_pane) = center_pane.upgrade() { center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx)); true } else { @@ -2079,7 +1816,7 @@ impl Workspace { pub fn open_path( &mut self, path: impl Into, - pane: Option>, + pane: Option>, focus_item: bool, cx: &mut ViewContext, ) -> Task, anyhow::Error>> { @@ -2093,7 +1830,7 @@ impl Workspace { }); let task = self.load_path(path.into(), cx); - cx.spawn(|_, mut cx| async move { + 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) @@ -2123,7 +1860,7 @@ impl Workspace { 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(cx)?; + 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)) @@ -2140,19 +1877,19 @@ impl Workspace { ) -> Task< Result<( Option, - impl 'static + FnOnce(&mut ViewContext) -> Box, + impl 'static + Send + FnOnce(&mut ViewContext) -> Box, )>, > { 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| { + let build_item = cx.update(|_, cx| { cx.default_global::() - .get(&project_item.model_type()) + .get(&project_item.entity_type()) .ok_or_else(|| anyhow!("no item builder for project item")) .cloned() - })?; + })??; let build_item = move |cx: &mut ViewContext| build_item(project, project_item, cx); Ok((project_entry_id, build_item)) @@ -2161,9 +1898,9 @@ impl Workspace { pub fn open_project_item( &mut self, - project_item: ModelHandle, + project_item: Model, cx: &mut ViewContext, - ) -> ViewHandle + ) -> View where T: ProjectItem, { @@ -2178,16 +1915,16 @@ impl Workspace { return item; } - let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx)); + 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( &mut self, - project_item: ModelHandle, + project_item: Model, cx: &mut ViewContext, - ) -> ViewHandle + ) -> View where T: ProjectItem, { @@ -2202,7 +1939,7 @@ impl Workspace { return item; } - let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx)); + 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 } @@ -2232,7 +1969,7 @@ impl Workspace { fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext) { let panes = self.center.panes(); if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) { - cx.focus(&pane); + cx.focus_view(&pane); } else { self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx); } @@ -2243,7 +1980,7 @@ impl Workspace { 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(&next_pane); + cx.focus_view(&next_pane); } } @@ -2252,7 +1989,7 @@ impl Workspace { 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(&prev_pane); + cx.focus_view(&prev_pane); } } @@ -2262,7 +1999,7 @@ impl Workspace { cx: &mut ViewContext, ) { if let Some(pane) = self.find_pane_in_direction(direction, cx) { - cx.focus(pane); + cx.focus_view(pane); } } @@ -2284,28 +2021,36 @@ impl Workspace { &mut self, direction: SplitDirection, cx: &mut ViewContext, - ) -> Option<&ViewHandle> { + ) -> Option<&View> { 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_point(cursor) => cursor, + Some(cursor) if bounding_box.contains(&cursor) => cursor, _ => bounding_box.center(), }; - let distance_to_next = theme::current(cx).workspace.pane_divider.width + 1.; + let distance_to_next = 8.; //todo(pane dividers styling) let target = match direction { - SplitDirection::Left => vec2f(bounding_box.origin_x() - distance_to_next, center.y()), - SplitDirection::Right => vec2f(bounding_box.max_x() + distance_to_next, center.y()), - SplitDirection::Up => vec2f(center.x(), bounding_box.origin_y() - distance_to_next), - SplitDirection::Down => vec2f(center.x(), bounding_box.max_y() + distance_to_next), + 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: ViewHandle, cx: &mut ViewContext) { + fn handle_pane_focused(&mut self, pane: View, cx: &mut ViewContext) { if self.active_pane != pane { self.active_pane = pane.clone(); self.status_bar.update(cx, |status_bar, cx| { @@ -2317,7 +2062,7 @@ impl Workspace { self.dismiss_zoomed_items_to_reveal(None, cx); if pane.read(cx).is_zoomed() { - self.zoomed = Some(pane.downgrade().into_any()); + self.zoomed = Some(pane.downgrade().into()); } else { self.zoomed = None; } @@ -2329,7 +2074,7 @@ impl Workspace { fn handle_pane_event( &mut self, - pane: ViewHandle, + pane: View, event: &pane::Event, cx: &mut ViewContext, ) { @@ -2345,6 +2090,7 @@ impl Workspace { } if &pane == self.active_pane() { self.active_item_path_changed(cx); + self.update_active_view_for_followers(cx); } } pane::Event::ChangeItemTitle => { @@ -2356,7 +2102,7 @@ impl Workspace { 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().id() == pane.id() { + if entry.get().entity_id() == pane.entity_id() { entry.remove(); } } @@ -2367,8 +2113,8 @@ impl Workspace { pane::Event::ZoomIn => { if pane == self.active_pane { pane.update(cx, |pane, cx| pane.set_zoomed(true, cx)); - if pane.read(cx).has_focus() { - self.zoomed = Some(pane.downgrade().into_any()); + if pane.read(cx).has_focus(cx) { + self.zoomed = Some(pane.downgrade().into()); self.zoomed_position = None; } cx.notify(); @@ -2388,10 +2134,10 @@ impl Workspace { pub fn split_pane( &mut self, - pane_to_split: ViewHandle, + pane_to_split: View, split_direction: SplitDirection, cx: &mut ViewContext, - ) -> ViewHandle { + ) -> View { let new_pane = self.add_pane(cx); self.center .split(&pane_to_split, &new_pane, split_direction) @@ -2402,10 +2148,10 @@ impl Workspace { pub fn split_and_clone( &mut self, - pane: ViewHandle, + pane: View, direction: SplitDirection, cx: &mut ViewContext, - ) -> Option> { + ) -> Option> { 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); @@ -2421,16 +2167,16 @@ impl Workspace { pub fn split_pane_with_item( &mut self, - pane_to_split: WeakViewHandle, + pane_to_split: WeakView, split_direction: SplitDirection, - from: WeakViewHandle, - item_id_to_move: usize, + from: WeakView, + item_id_to_move: EntityId, cx: &mut ViewContext, ) { - let Some(pane_to_split) = pane_to_split.upgrade(cx) else { + let Some(pane_to_split) = pane_to_split.upgrade() else { return; }; - let Some(from) = from.upgrade(cx) else { + let Some(from) = from.upgrade() else { return; }; @@ -2444,12 +2190,12 @@ impl Workspace { pub fn split_pane_with_project_entry( &mut self, - pane_to_split: WeakViewHandle, + pane_to_split: WeakView, split_direction: SplitDirection, project_entry: ProjectEntryId, cx: &mut ViewContext, ) -> Option>> { - let pane_to_split = pane_to_split.upgrade(cx)?; + 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) @@ -2457,7 +2203,7 @@ impl Workspace { 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().spawn(async move { + Some(cx.foreground_executor().spawn(async move { task.await?; Ok(()) })) @@ -2465,9 +2211,9 @@ impl Workspace { pub fn move_item( &mut self, - source: ViewHandle, - destination: ViewHandle, - item_id_to_move: usize, + source: View, + destination: View, + item_id_to_move: EntityId, destination_index: usize, cx: &mut ViewContext, ) { @@ -2475,7 +2221,7 @@ impl Workspace { .read(cx) .items() .enumerate() - .find(|(_, item_handle)| item_handle.id() == item_id_to_move); + .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"); @@ -2494,17 +2240,17 @@ impl Workspace { // This automatically removes duplicate items in the pane destination.update(cx, |destination, cx| { destination.add_item(item_handle, true, true, Some(destination_index), cx); - cx.focus_self(); + destination.focus(cx) }); } - fn remove_pane(&mut self, pane: ViewHandle, cx: &mut ViewContext) { + fn remove_pane(&mut self, pane: View, cx: &mut ViewContext) { 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.id()); + self.panes_by_item.remove(&removed_item.item_id()); } cx.notify(); @@ -2513,14 +2259,19 @@ impl Workspace { } } - pub fn panes(&self) -> &[ViewHandle] { + pub fn panes(&self) -> &[View] { &self.panes } - pub fn active_pane(&self) -> &ViewHandle { + pub fn active_pane(&self) -> &View { &self.active_pane } + pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option> { + 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.follower_states.retain(|_, state| { if state.leader_id == peer_id { @@ -2535,7 +2286,7 @@ impl Workspace { cx.notify(); } - fn start_following( + pub fn start_following( &mut self, leader_id: PeerId, cx: &mut ViewContext, @@ -2594,7 +2345,7 @@ impl Workspace { &mut self, _: &FollowNextCollaborator, cx: &mut ViewContext, - ) -> Option>> { + ) { 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(); @@ -2619,25 +2370,25 @@ impl Workspace { let pane = self.active_pane.clone(); let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next()) else { - return None; + return; }; if Some(leader_id) == self.unfollow(&pane, cx) { - return None; + return; } - self.follow(leader_id, cx) + 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, - ) -> Option>> { - let room = ActiveCall::global(cx).read(cx).room()?.read(cx); - let project = self.project.read(cx); - - let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else { - return None; + pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext) { + 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, @@ -2654,31 +2405,24 @@ impl Workspace { // if they are active in another project, follow there. if let Some(project_id) = other_project_id { let app_state = self.app_state.clone(); - return Some(crate::join_remote_project( - project_id, - remote_participant.user.id, - app_state, - cx, - )); + 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(pane); - return None; + 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: &ViewHandle, - cx: &mut ViewContext, - ) -> Option { + pub fn unfollow(&mut self, pane: &View, cx: &mut ViewContext) -> Option { let state = self.follower_states.remove(pane)?; let leader_id = state.leader_id; for (_, item) in state.items_by_leader_view_id { @@ -2706,44 +2450,11 @@ impl Workspace { 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 render_titlebar(&self, theme: &Theme, cx: &mut ViewContext) -> AnyElement { - // TODO: There should be a better system in place for this - // (https://github.com/zed-industries/zed/issues/1290) - let is_fullscreen = cx.window_is_fullscreen(); - let container_theme = if is_fullscreen { - let mut container_theme = theme.titlebar.container; - container_theme.padding.left = container_theme.padding.right; - container_theme - } else { - theme.titlebar.container - }; - - enum TitleBar {} - MouseEventHandler::new::(0, cx, |_, cx| { - Stack::new() - .with_children( - self.titlebar_item - .as_ref() - .map(|item| ChildView::new(item, cx)), - ) - .contained() - .with_style(container_theme) - }) - .on_click(MouseButton::Left, |event, _, cx| { - if event.click_count == 2 { - cx.zoom_window(); - } - }) - .constrained() - .with_height(theme.titlebar.height) - .into_any_named("titlebar") - } + // 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) { let active_entry = self.active_project_path(cx); @@ -2807,55 +2518,27 @@ impl Workspace { } } - fn render_disconnected_overlay( - &self, - cx: &mut ViewContext, - ) -> Option> { - if self.project.read(cx).is_read_only() { - enum DisconnectedOverlay {} - Some( - MouseEventHandler::new::(0, cx, |_, cx| { - let theme = &theme::current(cx); - Label::new( - "Your connection to the remote project has been lost.", - theme.workspace.disconnected_overlay.text.clone(), - ) - .aligned() - .contained() - .with_style(theme.workspace.disconnected_overlay.container) - }) - .with_cursor_style(CursorStyle::Arrow) - .capture_all() - .into_any_named("disconnected overlay"), - ) - } else { - None - } - } - - fn render_notifications( - &self, - theme: &theme::Workspace, - cx: &AppContext, - ) -> Option> { + fn render_notifications(&self, _cx: &ViewContext) -> Option
{ if self.notifications.is_empty() { None } else { Some( - Flex::column() - .with_children(self.notifications.iter().map(|(_, _, notification)| { - ChildView::new(notification.as_any(), cx) - .contained() - .with_style(theme.notification) - })) - .constrained() - .with_width(theme.notifications.width) - .contained() - .with_style(theme.notifications.container) - .aligned() - .bottom() - .right() - .into_any(), + 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()), + ), ) } } @@ -2923,10 +2606,10 @@ impl Workspace { } async fn process_leader_update( - this: &WeakViewHandle, + this: &WeakView, leader_id: PeerId, update: proto::UpdateFollowers, - cx: &mut AsyncAppContext, + cx: &mut AsyncWindowContext, ) -> Result<()> { match update.variant.ok_or_else(|| anyhow!("invalid update"))? { proto::update_followers::Variant::UpdateActiveView(update_active_view) => { @@ -2967,7 +2650,7 @@ impl Workspace { try_join_all(tasks).await.log_err(); } proto::update_followers::Variant::CreateView(view) => { - let panes = this.read_with(cx, |this, _| { + let panes = this.update(cx, |this, _| { this.follower_states .iter() .filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane)) @@ -2982,22 +2665,20 @@ impl Workspace { } async fn add_views_from_leader( - this: WeakViewHandle, + this: WeakView, leader_id: PeerId, - panes: Vec>, + panes: Vec>, views: Vec, - cx: &mut AsyncAppContext, + cx: &mut AsyncWindowContext, ) -> Result<()> { - let this = this - .upgrade(cx) - .ok_or_else(|| anyhow!("workspace dropped"))?; + let this = this.upgrade().context("workspace dropped")?; - let item_builders = cx.update(|cx| { + let item_builders = cx.update(|_, cx| { cx.default_global::() .values() .map(|b| b.0) .collect::>() - }); + })?; let mut item_tasks_by_pane = HashMap::default(); for pane in panes { @@ -3011,8 +2692,9 @@ impl Workspace { 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)); + 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); @@ -3036,26 +2718,26 @@ impl Workspace { } Some(()) - }); + })?; } Ok(()) } - fn update_active_view_for_followers(&mut self, cx: &AppContext) { + fn update_active_view_for_followers(&mut self, cx: &mut ViewContext) { let mut is_project_item = true; let mut update = proto::UpdateActiveView::default(); - if self.active_pane.read(cx).has_focus() { - let item = self - .active_item(cx) - .and_then(|item| item.to_followable_item_handle(cx)); - if let Some(item) = item { - 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 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), + }; + } } } @@ -3073,19 +2755,19 @@ impl Workspace { &self, project_only: bool, update: proto::update_followers::Variant, - cx: &AppContext, + cx: &mut WindowContext, ) -> Option<()> { let project_id = if project_only { self.project.read(cx).remote_id() } else { None }; - self.app_state().workspace_store.read_with(cx, |store, cx| { + self.app_state().workspace_store.update(cx, |store, cx| { store.update_followers(project_id, update, cx) }) } - pub fn leader_for_pane(&self, pane: &ViewHandle) -> Option { + pub fn leader_for_pane(&self, pane: &View) -> Option { self.follower_states.get(pane).map(|state| state.leader_id) } @@ -3123,22 +2805,17 @@ impl Workspace { if leader_in_this_project || !item.is_project_item(cx) { items_to_activate.push((pane.clone(), item.boxed_clone())); } - } else { - log::warn!( - "unknown view id {:?} for leader {:?}", - active_view_id, - leader_id - ); } 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(); + 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 { @@ -3158,9 +2835,9 @@ impl Workspace { fn shared_screen_for_peer( &self, peer_id: PeerId, - pane: &ViewHandle, + pane: &View, cx: &mut ViewContext, - ) -> Option> { + ) -> Option> { let call = self.active_call()?; let room = call.read(cx).room()?.read(cx); let participant = room.remote_participant_for_peer_id(peer_id)?; @@ -3173,13 +2850,13 @@ impl Workspace { } } - Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx))) + Some(cx.new_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx))) } - pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext) { - if active { + pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext) { + if cx.is_window_active() { self.update_active_view_for_followers(cx); - cx.background() + cx.background_executor() .spawn(persistence::DB.update_timestamp(self.database_id())) .detach(); } else { @@ -3189,7 +2866,7 @@ impl Workspace { item.workspace_deactivated(cx); } if matches!( - settings::get::(cx).autosave, + WorkspaceSettings::get_global(cx).autosave, AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange ) { for item in pane.items() { @@ -3202,13 +2879,13 @@ impl Workspace { } } - fn active_call(&self) -> Option<&ModelHandle> { + fn active_call(&self) -> Option<&Model> { self.active_call.as_ref().map(|(call, _)| call) } fn on_active_call_event( &mut self, - _: ModelHandle, + _: Model, event: &call::room::Event, cx: &mut ViewContext, ) { @@ -3254,42 +2931,45 @@ impl Workspace { } } - fn force_remove_pane(&mut self, pane: &ViewHandle, cx: &mut ViewContext) { + fn force_remove_pane(&mut self, pane: &View, cx: &mut ViewContext) { self.panes.retain(|p| p != pane); - cx.focus(self.panes.last().unwrap()); + 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._schedule_serialize = Some(cx.spawn(|this, cx| async move { - cx.background().timer(Duration::from_millis(100)).await; - this.read_with(&cx, |this, cx| this.serialize_workspace(cx)) - .ok(); + 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: &ViewContext) { - fn serialize_pane_handle( - pane_handle: &ViewHandle, - cx: &AppContext, - ) -> SerializedPane { + fn serialize_workspace(&self, cx: &mut ViewContext) { + fn serialize_pane_handle(pane_handle: &View, cx: &WindowContext) -> SerializedPane { let (items, active) = { let pane = pane_handle.read(cx); - let active_item_id = pane.active_item().map(|item| item.id()); + 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.id(), - active: Some(item_handle.id()) == active_item_id, + item_id: item_handle.item_id().as_u64(), + active: Some(item_handle.item_id()) == active_item_id, }) }) .collect::>(), - pane.has_focus(), + pane.has_focus(cx), ) }; @@ -3298,7 +2978,7 @@ impl Workspace { fn build_serialized_pane_group( pane_group: &Member, - cx: &AppContext, + cx: &WindowContext, ) -> SerializedPaneGroup { match pane_group { Member::Axis(PaneAxis { @@ -3312,7 +2992,7 @@ impl Workspace { .iter() .map(|member| build_serialized_pane_group(member, cx)) .collect::>(), - flexes: Some(flexes.borrow().clone()), + flexes: Some(flexes.lock().clone()), }, Member::Pane(pane_handle) => { SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx)) @@ -3320,15 +3000,15 @@ impl Workspace { } } - fn build_serialized_docks(this: &Workspace, cx: &ViewContext) -> DockStructure { + fn build_serialized_docks( + this: &Workspace, + cx: &mut ViewContext, + ) -> 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( - cx.view_ui_name(panel.as_any().window(), panel.id())? - .to_string(), - ) - }); + 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)) @@ -3336,12 +3016,9 @@ impl Workspace { 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( - cx.view_ui_name(panel.as_any().window(), panel.id())? - .to_string(), - ) - }); + 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)) @@ -3349,12 +3026,9 @@ impl Workspace { 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( - cx.view_ui_name(panel.as_any().window(), panel.id())? - .to_string(), - ) - }); + 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)) @@ -3396,40 +3070,39 @@ impl Workspace { docks, }; - cx.background() - .spawn(persistence::DB.save_workspace(serialized_workspace)) + cx.spawn(|_, _| persistence::DB.save_workspace(serialized_workspace)) .detach(); } } } pub(crate) fn load_workspace( - workspace: WeakViewHandle, serialized_workspace: SerializedWorkspace, paths_to_open: Vec>, - cx: &mut AppContext, + cx: &mut ViewContext, ) -> Task>>>> { - cx.spawn(|mut cx| async move { - let (project, old_center_pane) = workspace.read_with(&cx, |workspace, _| { - ( - workspace.project().clone(), - workspace.last_active_center_pane.clone(), - ) - })?; + 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, &mut cx) + .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.read(|cx| { + let mut items_by_project_path = cx.update(|_, cx| { center_items .unwrap_or_default() .into_iter() @@ -3439,7 +3112,7 @@ impl Workspace { Some((project_path, item)) }) .collect::>() - }); + })?; let opened_items = paths_to_open .into_iter() @@ -3456,21 +3129,12 @@ impl Workspace { // Swap workspace center group workspace.center = PaneGroup::with_root(center_group); - - // Change the focus to the workspace first so that we retrigger focus in on the pane. - cx.focus_self(); - + workspace.last_active_center_pane = active_pane.as_ref().map(|p| p.downgrade()); if let Some(active_pane) = active_pane { - cx.focus(&active_pane); + workspace.active_pane = active_pane; + cx.focus_self(); } else { - cx.focus(workspace.panes.last().unwrap()); - } - } else { - let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx)); - if let Some(old_center_handle) = old_center_handle { - cx.focus(&old_center_handle) - } else { - cx.focus_self() + workspace.active_pane = workspace.center.first_pane().clone(); } } @@ -3478,7 +3142,7 @@ impl Workspace { 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_ui_name(&active_panel, cx) { + if let Some(ix) = dock.panel_index_for_persistent_name(&active_panel, cx) { dock.activate_panel(ix, cx); } } @@ -3492,7 +3156,7 @@ impl Workspace { 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_ui_name(&active_panel, cx) { + if let Some(ix) = dock.panel_index_for_persistent_name(&active_panel, cx) { dock.activate_panel(ix, cx); } } @@ -3506,7 +3170,7 @@ impl Workspace { 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_ui_name(&active_panel, cx) { + if let Some(ix) = dock.panel_index_for_persistent_name(&active_panel, cx) { dock.activate_panel(ix, cx); } } @@ -3523,20 +3187,83 @@ impl Workspace { })?; // Serialize ourself to make sure our timestamps and any pane / item changes are replicated - workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?; + workspace.update(&mut cx, |workspace, cx| workspace.serialize_workspace(cx))?; Ok(opened_items) }) } + fn actions(&self, div: Div, cx: &mut ViewContext) -> 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: ModelHandle, cx: &mut ViewContext) -> Self { + pub fn test_new(project: Model, cx: &mut ViewContext) -> Self { use node_runtime::FakeNodeRuntime; let client = project.read(cx).client(); let user_store = project.read(cx).user_store(); - let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx)); + 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, @@ -3544,239 +3271,205 @@ impl Workspace { user_store, fs: project.read(cx).fs().clone(), build_window_options: |_, _, _| Default::default(), - initialize_workspace: |_, _, _, _| Task::ready(Ok(())), - background_actions: || &[], node_runtime: FakeNodeRuntime::new(), }); - Self::new(0, project, app_state, cx) + 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> { - 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() - }; + // fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option> { + // 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(), - ) + // 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( + &mut self, + callback: impl Fn(&mut Self, &A, &mut ViewContext) + '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) -> 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( + &mut self, + cx: &ViewContext, + ) -> Option> { + self.modal_layer.read(cx).active_modal() + } + + pub fn toggle_modal(&mut self, cx: &mut ViewContext, build: B) + where + B: FnOnce(&mut ViewContext) -> V, + { + self.modal_layer + .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build)) } } fn window_bounds_env_override(cx: &AsyncAppContext) -> Option { + 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(RectF::new( - cx.platform().screens()[0].bounds().origin() + position, + WindowBounds::Fixed(Bounds { + origin: display_origin + position, size, - )) + }) }) } -async fn open_items( +fn open_items( serialized_workspace: Option, - workspace: &WeakViewHandle, mut project_paths_to_open: Vec<(PathBuf, Option)>, app_state: Arc, - mut cx: AsyncAppContext, -) -> Result>>>> { - let mut opened_items = Vec::with_capacity(project_paths_to_open.len()); - - if let Some(serialized_workspace) = serialized_workspace { - let workspace = workspace.clone(); - let restored_items = cx - .update(|cx| { - Workspace::load_workspace( - workspace, - serialized_workspace, - project_paths_to_open - .iter() - .map(|(_, project_path)| project_path) - .cloned() - .collect(), - cx, - ) - }) - .await?; - - let restored_project_paths = cx.read(|cx| { - restored_items + cx: &mut ViewContext, +) -> impl 'static + Future>>>>> { + let restored_items = serialized_workspace.map(|serialized_workspace| { + Workspace::load_workspace( + serialized_workspace, + project_paths_to_open .iter() - .filter_map(|item| item.as_ref()?.project_path(cx)) - .collect::>() - }); + .map(|(_, project_path)| project_path) + .cloned() + .collect(), + cx, + ) + }); - for restored_item in restored_items { - opened_items.push(restored_item.map(Ok)); - } + cx.spawn(|workspace, mut cx| async move { + let mut opened_items = Vec::with_capacity(project_paths_to_open.len()); - 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()); + if let Some(restored_items) = restored_items { + let restored_items = restored_items.await?; - 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 restored_project_paths = restored_items + .iter() + .filter_map(|item| { + cx.update(|_, cx| item.as_ref()?.project_path(cx)) + .ok() + .flatten() + }) + .collect::>(); + + 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; } } - }) - }); - - for maybe_opened_path in futures::future::join_all(tasks.into_iter()) - .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_of_new_dock(workspace: &WeakViewHandle, cx: &mut AsyncAppContext) { - const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system"; - const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key"; - const MESSAGE_ID: usize = 2; - - if workspace - .read_with(cx, |workspace, cx| { - workspace.has_shown_notification_once::(MESSAGE_ID, cx) - }) - .unwrap_or(false) - { - return; - } - - if db::kvp::KEY_VALUE_STORE - .read_kvp(NEW_DOCK_HINT_KEY) - .ok() - .flatten() - .is_some() - { - if !workspace - .read_with(cx, |workspace, cx| { - workspace.has_shown_notification_once::(MESSAGE_ID, cx) - }) - .unwrap_or(false) - { - cx.update(|cx| { - cx.update_global::(|tracker, _| { - let entry = tracker - .entry(TypeId::of::()) - .or_default(); - if !entry.contains(&MESSAGE_ID) { - entry.push(MESSAGE_ID); - } }); - }); + } 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::>(); + + 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); + } } - return; - } - - cx.spawn(|_| async move { - db::kvp::KEY_VALUE_STORE - .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string()) - .await - .ok(); + Ok(opened_items) }) - .detach(); - - workspace - .update(cx, |workspace, cx| { - workspace.show_notification_once(2, cx, |cx| { - cx.add_view(|_| { - MessageNotification::new_element(|text, _| { - Text::new( - "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.", - text, - ) - .with_custom_runs(vec![26..32, 34..46], |_, bounds, cx| { - let code_span_background_color = settings::get::(cx) - .theme - .editor - .document_highlight_read_background; - - cx.scene().push_quad(gpui::Quad { - bounds, - background: Some(code_span_background_color), - border: Default::default(), - corner_radii: (2.0).into(), - }) - }) - .into_any() - }) - .with_click_message("Read more about the new panel system") - .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST)) - }) - }) - }) - .ok(); } -fn notify_if_database_failed(workspace: &WeakViewHandle, cx: &mut AsyncAppContext) { +fn notify_if_database_failed(workspace: WindowHandle, 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.add_view(|_| { + 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.platform().open_url(REPORT_ISSUE_URL)) + .on_click(|cx| cx.open_url(REPORT_ISSUE_URL)) }) }); } @@ -3784,153 +3477,166 @@ fn notify_if_database_failed(workspace: &WeakViewHandle, cx: &mut Asy .log_err(); } -impl Entity for Workspace { - type Event = Event; - - fn release(&mut self, cx: &mut AppContext) { - self.app_state.workspace_store.update(cx, |store, _| { - store.workspaces.remove(&self.weak_self); - }) +impl FocusableView for Workspace { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.active_pane.focus_handle(cx) } } -impl View for Workspace { - fn ui_name() -> &'static str { - "Workspace" - } +#[derive(Clone, Render)] +struct DraggedDock(DockPosition); - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = theme::current(cx).clone(); - Stack::new() - .with_child( - Flex::column() - .with_child(self.render_titlebar(&theme, cx)) - .with_child( - Stack::new() - .with_child({ - let project = self.project.clone(); - Flex::row() - .with_children(self.render_dock(DockPosition::Left, cx)) - .with_child( - Flex::column() - .with_child( - FlexItem::new( - self.center.render( - &project, - &theme, - &self.follower_states, - self.active_call(), - self.active_pane(), - self.zoomed - .as_ref() - .and_then(|zoomed| zoomed.upgrade(cx)) - .as_ref(), - &self.app_state, - cx, - ), - ) - .flex(1., true), - ) - .with_children( - self.render_dock(DockPosition::Bottom, cx), - ) - .flex(1., true), - ) - .with_children(self.render_dock(DockPosition::Right, cx)) - }) - .with_child(Overlay::new( - Stack::new() - .with_children(self.zoomed.as_ref().and_then(|zoomed| { - enum ZoomBackground {} - let zoomed = zoomed.upgrade(cx)?; +impl Render for Workspace { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let mut context = KeyContext::default(); + context.add("Workspace"); - let mut foreground_style = - theme.workspace.zoomed_pane_foreground; - if let Some(zoomed_dock_position) = self.zoomed_position { - foreground_style = - theme.workspace.zoomed_panel_foreground; - let margin = foreground_style.margin.top; - let border = foreground_style.border.top; - - // Only include a margin and border on the opposite side. - foreground_style.margin.top = 0.; - foreground_style.margin.left = 0.; - foreground_style.margin.bottom = 0.; - foreground_style.margin.right = 0.; - foreground_style.border.top = false; - foreground_style.border.left = false; - foreground_style.border.bottom = false; - foreground_style.border.right = false; - match zoomed_dock_position { - DockPosition::Left => { - foreground_style.margin.right = margin; - foreground_style.border.right = border; - } - DockPosition::Right => { - foreground_style.margin.left = margin; - foreground_style.border.left = border; - } - DockPosition::Bottom => { - foreground_style.margin.top = margin; - foreground_style.border.top = border; - } - } - } - - Some( - ChildView::new(&zoomed, cx) - .contained() - .with_style(foreground_style) - .aligned() - .contained() - .with_style(theme.workspace.zoomed_background) - .mouse::(0) - .capture_all() - .on_down( - MouseButton::Left, - |_, this: &mut Self, cx| { - this.zoom_out(cx); - }, - ), - ) - })) - .with_children(self.modal.as_ref().map(|modal| { - // Prevent clicks within the modal from falling - // through to the rest of the workspace. - enum ModalBackground {} - MouseEventHandler::new::( - 0, - cx, - |_, cx| ChildView::new(modal.view.as_any(), cx), - ) - .on_click(MouseButton::Left, |_, _, _| {}) - .contained() - .with_style(theme.workspace.modal) - .aligned() - .top() - })) - .with_children(self.render_notifications(&theme.workspace, cx)), - )) - .provide_resize_bounds::() - .flex(1.0, true), - ) - .with_child(ChildView::new(&self.status_bar, cx)) - .contained() - .with_background_color(theme.workspace.background), + 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(), ) - .with_children(DragAndDrop::render(cx)) - .with_children(self.render_disconnected_overlay(cx)) - .into_any_named("workspace") - } + }; - fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - if cx.is_self_focused() { - cx.focus(&self.active_pane); - } - } + let theme = cx.theme().clone(); + let colors = theme.colors(); + cx.set_rem_size(ui_font_size); - fn modifiers_changed(&mut self, e: &ModifiersChangedEvent, cx: &mut ViewContext) -> bool { - DragAndDrop::::update_modifiers(e.modifiers, cx) + 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, 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 + }) } } @@ -3940,9 +3646,9 @@ impl WorkspaceStore { workspaces: Default::default(), followers: Default::default(), _subscriptions: vec![ - client.add_request_handler(cx.handle(), Self::handle_follow), - client.add_message_handler(cx.handle(), Self::handle_unfollow), - client.add_message_handler(cx.handle(), Self::handle_update_followers), + 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, } @@ -3954,7 +3660,7 @@ impl WorkspaceStore { update: proto::update_followers::Variant, cx: &AppContext, ) -> Option<()> { - if !cx.has_global::>() { + if !cx.has_global::>() { return None; } @@ -3983,8 +3689,8 @@ impl WorkspaceStore { .log_err() } - async fn handle_follow( - this: ModelHandle, + pub async fn handle_follow( + this: Model, envelope: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, @@ -3994,50 +3700,45 @@ impl WorkspaceStore { project_id: envelope.payload.project_id, peer_id: envelope.original_sender_id()?, }; - let active_project = ActiveCall::global(cx) - .read(cx) - .location() - .map(|project| project.id()); + let active_project = ActiveCall::global(cx).read(cx).location().cloned(); let mut response = proto::FollowResponse::default(); - for workspace in &this.workspaces { - let Some(workspace) = workspace.upgrade(cx) else { - continue; - }; - - workspace.update(cx.as_mut(), |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.id()) == active_project - { - response.active_view_id = Some(active_view_id); + 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( - this: ModelHandle, + model: Model, envelope: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { - this.update(&mut cx, |this, _| { + model.update(&mut cx, |this, _| { let follower = Follower { project_id: envelope.payload.project_id, peer_id: envelope.original_sender_id()?, @@ -4046,37 +3747,33 @@ impl WorkspaceStore { this.followers.remove(ix); } Ok(()) - }) + })? } async fn handle_update_followers( - this: ModelHandle, + this: Model, envelope: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { let leader_id = envelope.original_sender_id()?; let update = envelope.payload; - this.update(&mut cx, |this, cx| { - for workspace in &this.workspaces { - let Some(workspace) = workspace.upgrade(cx) else { - continue; - }; - workspace.update(cx.as_mut(), |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); - }); - } - Ok(()) - }) - } -} -impl Entity for WorkspaceStore { - type Event = (); + 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 { @@ -4101,7 +3798,7 @@ pub trait WorkspaceHandle { fn file_project_paths(&self, cx: &AppContext) -> Vec; } -impl WorkspaceHandle for ViewHandle { +impl WorkspaceHandle for View { fn file_project_paths(&self, cx: &AppContext) -> Vec { self.read(cx) .worktrees(cx) @@ -4124,30 +3821,33 @@ impl std::fmt::Debug for OpenPaths { } } -pub struct WorkspaceCreated(pub WeakViewHandle); - pub fn activate_workspace_for_project( - cx: &mut AsyncAppContext, - predicate: impl Fn(&mut Project, &mut ModelContext) -> bool, -) -> Option> { + cx: &mut AppContext, + predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static, +) -> Option> { for window in cx.windows() { - let handle = window - .update(cx, |cx| { - if let Some(workspace_handle) = cx.root_view().clone().downcast::() { - let project = workspace_handle.read(cx).project.clone(); - if project.update(cx, &predicate) { - cx.activate_window(); - return Some(workspace_handle.clone()); - } - } - None - }) - .flatten(); + let Some(workspace) = window.downcast::() else { + continue; + }; - if let Some(handle) = handle { - return Some(handle.downgrade()); + 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 } @@ -4159,10 +3859,10 @@ async fn join_channel_internal( channel_id: u64, app_state: &Arc, requesting_window: Option>, - active_call: &ModelHandle, + active_call: &Model, cx: &mut AsyncAppContext, ) -> Result { - let (should_prompt, open_room) = active_call.read_with(cx, |active_call, cx| { + 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); }; @@ -4177,7 +3877,7 @@ async fn join_channel_internal( None }; (should_prompt, open_room) - }); + })?; if let Some(room) = open_room { let task = room.update(cx, |room, cx| { @@ -4186,7 +3886,7 @@ async fn join_channel_internal( } None - }); + })?; if let Some(task) = task { task.await?; } @@ -4195,28 +3895,23 @@ async fn join_channel_internal( if should_prompt { if let Some(workspace) = requesting_window { - if let Some(window) = workspace.update(cx, |cx| cx.window()) { - let answer = window.prompt( + 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"], - cx, - ); + ) + })?.await; - if let Some(mut answer) = answer { - if answer.next().await == Some(1) { - return Ok(false); - } - } - } else { - return Ok(false); // unreachable!() hopefully + if answer == Ok(1) { + return Ok(false); } } else { return Ok(false); // unreachable!() hopefully } } - let client = cx.read(|cx| active_call.read(cx).client()); + let client = cx.update(|cx| active_call.read(cx).client())?; let mut client_status = client.status(); @@ -4243,14 +3938,14 @@ async fn join_channel_internal( 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()) + room.update(cx, |room, _| room.room_update_completed())? .await; let task = room.update(cx, |room, cx| { @@ -4259,7 +3954,7 @@ async fn join_channel_internal( } None - }); + })?; if let Some(task) = task { task.await?; return anyhow::Ok(true); @@ -4297,29 +3992,26 @@ pub fn join_channel( 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; + cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), requesting_window, cx))? + .await?; } active_window = activate_any_workspace_window(&mut cx); - if active_window.is_none() { - return result.map(|_| ()); // unreachable!() assuming new_local always opens a window - } + let Some(active_window) = active_window else { + return anyhow::Ok(()); + }; if let Err(err) = result { - let prompt = active_window.unwrap().update(&mut cx, |_, cx| { - cx.prompt( - PromptLevel::Critical, - &format!("Failed to join channel: {}", err), - &["Ok"], - ) - }); - - if let Some(mut prompt) = prompt { - prompt.next().await; - } else { - return Err(err); - } + 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. @@ -4330,39 +4022,32 @@ pub fn join_channel( pub async fn get_any_active_workspace( app_state: Arc, mut cx: AsyncAppContext, -) -> Result> { +) -> anyhow::Result> { // 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; + cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx))? + .await?; } - - let Some(active_window) = activate_any_workspace_window(&mut cx) else { - return Err(anyhow!("could not open zed"))?; - }; - - Ok(active_window) + activate_any_workspace_window(&mut cx) + .context("could not open zed")? + .downcast::() + .context("could not open zed workspace window") } -pub fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option> { - for window in cx.windows() { - if let Some(workspace) = window - .update(cx, |cx| { - cx.root_view() - .clone() - .downcast::() - .map(|workspace| { - cx.activate_window(); - workspace - }) - }) - .flatten() - { - return Some(workspace); +fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option { + cx.update(|cx| { + for window in cx.windows() { + let is_workspace = window.downcast::().is_some(); + if is_workspace { + window.update(cx, |_, cx| cx.activate_window()).ok(); + return Some(window); + } } - } - None + None + }) + .ok() + .flatten() } #[allow(clippy::type_complexity)] @@ -4372,19 +4057,19 @@ pub fn open_paths( requesting_window: Option>, cx: &mut AppContext, ) -> Task< - Result<( - WeakViewHandle, + anyhow::Result<( + WindowHandle, Vec, anyhow::Error>>>, )>, > { let app_state = app_state.clone(); let abs_paths = abs_paths.to_vec(); - cx.spawn(|mut cx| async move { - // Open paths in existing workspace if possible - let existing = activate_workspace_for_project(&mut cx, |project, cx| { - project.contains_paths(&abs_paths, cx) - }); - + // 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(), @@ -4395,11 +4080,10 @@ pub fn open_paths( .await, )) } else { - Ok(cx - .update(|cx| { - Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx) - }) - .await) + cx.update(move |cx| { + Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx) + })? + .await } }) } @@ -4407,19 +4091,19 @@ pub fn open_paths( pub fn open_new( app_state: &Arc, cx: &mut AppContext, - init: impl FnOnce(&mut Workspace, &mut ViewContext) + 'static, + init: impl FnOnce(&mut Workspace, &mut ViewContext) + 'static + Send, ) -> Task<()> { let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx); cx.spawn(|mut cx| async move { - let (workspace, opened_paths) = task.await; - - workspace - .update(&mut cx, |workspace, cx| { - if opened_paths.is_empty() { - init(workspace, cx) - } - }) - .log_err(); + 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(); + } }) } @@ -4429,7 +4113,7 @@ pub fn create_and_open_local_file( default_content: impl 'static + Send + FnOnce() -> Rope, ) -> Task>> { cx.spawn(|workspace, mut cx| async move { - let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?; + 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()) @@ -4456,14 +4140,14 @@ pub fn join_remote_project( app_state: Arc, cx: &mut AppContext, ) -> Task> { + let windows = cx.windows(); cx.spawn(|mut cx| async move { - let windows = cx.windows(); let existing_workspace = windows.into_iter().find_map(|window| { window.downcast::().and_then(|window| { window - .read_root_with(&cx, |workspace, cx| { + .update(&mut cx, |workspace, cx| { if workspace.project().read(cx).remote_id() == Some(project_id) { - Some(cx.handle().downgrade()) + Some(window) } else { None } @@ -4475,9 +4159,9 @@ pub fn join_remote_project( let workspace = if let Some(existing_workspace) = existing_workspace { existing_workspace } else { - let active_call = cx.read(ActiveCall::global); + let active_call = cx.update(|cx| ActiveCall::global(cx))?; let room = active_call - .read_with(&cx, |call, _| call.room().cloned()) + .read_with(&cx, |call, _| call.room().cloned())? .ok_or_else(|| anyhow!("not in a call"))?; let project = room .update(&mut cx, |room, cx| { @@ -4487,35 +4171,22 @@ pub fn join_remote_project( app_state.fs.clone(), cx, ) - }) + })? .await?; let window_bounds_override = window_bounds_env_override(&cx); - let window = cx.add_window( - (app_state.build_window_options)( - window_bounds_override, - None, - cx.platform().as_ref(), - ), - |cx| Workspace::new(0, project, app_state.clone(), cx), - ); - let workspace = window.root(&cx).unwrap(); - (app_state.initialize_workspace)( - workspace.downgrade(), - false, - app_state.clone(), - cx.clone(), - ) - .await - .log_err(); - - workspace.downgrade() + 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.window().activate(&mut cx); - cx.platform().activate(true); - 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) @@ -4535,9 +4206,7 @@ pub fn join_remote_project( }); if let Some(follow_peer_id) = follow_peer_id { - workspace - .follow(follow_peer_id, cx) - .map(|follow| follow.detach_and_log_err(cx)); + workspace.follow(follow_peer_id, cx); } } })?; @@ -4547,37 +4216,41 @@ pub fn join_remote_project( } pub fn restart(_: &Restart, cx: &mut AppContext) { - let should_confirm = settings::get::(cx).confirm_quit; + let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit; + let mut workspace_windows = cx + .windows() + .into_iter() + .filter_map(|window| window.downcast::()) + .collect::>(); + + // 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 { - let mut workspace_windows = cx - .windows() - .into_iter() - .filter_map(|window| window.downcast::()) - .collect::>(); - - // 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)); - - if let (true, Some(window)) = (should_confirm, workspace_windows.first()) { - let answer = window.prompt( - PromptLevel::Info, - "Are you sure you want to restart?", - &["Restart", "Cancel"], - &mut cx, - ); - - if let Some(mut answer) = answer { - let answer = answer.next().await; - if answer != Some(0) { - return Ok(()); - } + 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 Some(should_close) = window.update_root(&mut cx, |workspace, cx| { + if let Ok(should_close) = window.update(&mut cx, |workspace, cx| { workspace.prepare_to_close(true, cx) }) { if !should_close.await? { @@ -4585,87 +4258,144 @@ pub fn restart(_: &Restart, cx: &mut AppContext) { } } } - cx.platform().restart(); - anyhow::Ok(()) + + cx.update(|cx| cx.restart()) }) .detach_and_log_err(cx); } -fn parse_pixel_position_env_var(value: &str) -> Option { +fn parse_pixel_position_env_var(value: &str) -> Option> { + 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> { let mut parts = value.split(','); let width: usize = parts.next()?.parse().ok()?; let height: usize = parts.next()?.parse().ok()?; - Some(vec2f(width as f32, height as f32)) + 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, + 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, 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 { + None + } + + fn into_element(self) -> Self::Element { + self + } } #[cfg(test)] mod tests { + use std::{cell::RefCell, rc::Rc}; + use super::*; - use crate::{ - dock::test::{TestPanel, TestPanelEvent}, - item::test::{TestItem, TestItemEvent, TestProjectItem}, + use crate::item::{ + test::{TestItem, TestProjectItem}, + ItemEvent, }; use fs::FakeFs; - use gpui::{executor::Deterministic, test::EmptyView, TestAppContext}; + use gpui::TestAppContext; use project::{Project, ProjectEntryId}; use serde_json::json; use settings::SettingsStore; - use std::{cell::RefCell, rc::Rc}; #[gpui::test] async fn test_tab_disambiguation(cx: &mut TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let workspace = window.root(cx); + 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 = window.add_view(cx, |_| { - let mut item = TestItem::new(); + 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.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None)); + 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 = window.add_view(cx, |_| { - let mut item = TestItem::new(); + 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.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); - item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); + 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 = window.add_view(cx, |_| { - let mut item = TestItem::new(); + 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.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); - item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3))); - item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3))); + 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.background()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root1", json!({ @@ -4683,23 +4413,22 @@ mod tests { .await; let project = Project::test(fs, ["root1".as_ref()], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let workspace = window.root(cx); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - let worktree_id = project.read_with(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() + 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 = window.add_view(cx, |cx| { - TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)]) + let item1 = cx.new_view(|cx| { + TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)]) }); - let item2 = window.add_view(cx, |cx| { - TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.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.read_with(cx, |project, cx| { + project.update(cx, |project, cx| { assert_eq!( project.active_entry(), project @@ -4707,12 +4436,12 @@ mod tests { .map(|e| e.id) ); }); - assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root1")); + 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!(window.current_title(cx).as_deref(), Some("two.txt — root1")); - project.read_with(cx, |project, cx| { + assert_eq!(cx.window_title().as_deref(), Some("two.txt — root1")); + project.update(cx, |project, cx| { assert_eq!( project.active_entry(), project @@ -4727,8 +4456,8 @@ mod tests { }) .await .unwrap(); - assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root1")); - project.read_with(cx, |project, cx| { + assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1")); + project.update(cx, |project, cx| { assert_eq!( project.active_entry(), project @@ -4744,38 +4473,34 @@ mod tests { }) .await .unwrap(); - assert_eq!( - window.current_title(cx).as_deref(), - Some("one.txt — root1, root2") - ); + 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!(window.current_title(cx).as_deref(), Some("one.txt — root2")); + 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.background()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree("/root", json!({ "one": "" })).await; let project = Project::test(fs, ["root".as_ref()], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let workspace = window.root(cx); + 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 = window.add_view(cx, |_| TestItem::new()); + 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 = window.add_view(cx, |_| TestItem::new().with_dirty(true)); - let item3 = window.add_view(cx, |cx| { - TestItem::new() + 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)]) }); @@ -4784,12 +4509,12 @@ mod tests { w.add_item(Box::new(item3.clone()), cx); }); let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); - cx.foreground().run_until_parked(); - window.simulate_prompt_answer(2, cx); // cancel save all - cx.foreground().run_until_parked(); - window.simulate_prompt_answer(2, cx); // cancel save all - cx.foreground().run_until_parked(); - assert!(!window.has_pending_prompt(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()); } @@ -4797,31 +4522,30 @@ mod tests { async fn test_close_pane_items(cx: &mut TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, None, cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - let item1 = window.add_view(cx, |cx| { - TestItem::new() + let item1 = cx.new_view(|cx| { + TestItem::new(cx) .with_dirty(true) .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) }); - let item2 = window.add_view(cx, |cx| { - TestItem::new() + 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 = window.add_view(cx, |cx| { - TestItem::new() + 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 = window.add_view(cx, |cx| { - TestItem::new() + let item4 = cx.new_view(|cx| { + TestItem::new(cx) .with_dirty(true) .with_project_items(&[TestProjectItem::new_untitled(cx)]) }); @@ -4835,68 +4559,68 @@ mod tests { let close_items = pane.update(cx, |pane, cx| { pane.activate_item(1, true, true, cx); - assert_eq!(pane.active_item().unwrap().id(), item2.id()); - let item1_id = item1.id(); - let item3_id = item3.id(); - let item4_id = item4.id(); + 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.foreground().run_until_parked(); + cx.executor().run_until_parked(); - assert!(window.has_pending_prompt(cx)); + assert!(cx.has_pending_prompt()); // Ignore "Save all" prompt - window.simulate_prompt_answer(2, cx); - cx.foreground().run_until_parked(); + cx.simulate_prompt_answer(2); + cx.executor().run_until_parked(); // There's a prompt to save item 1. - pane.read_with(cx, |pane, _| { + pane.update(cx, |pane, _| { assert_eq!(pane.items_len(), 4); - assert_eq!(pane.active_item().unwrap().id(), item1.id()); + assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id()); }); // Confirm saving item 1. - window.simulate_prompt_answer(0, cx); - cx.foreground().run_until_parked(); + cx.simulate_prompt_answer(0); + cx.executor().run_until_parked(); // Item 1 is saved. There's a prompt to save item 3. - pane.read_with(cx, |pane, cx| { + 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().id(), item3.id()); + assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id()); }); - assert!(window.has_pending_prompt(cx)); + assert!(cx.has_pending_prompt()); // Cancel saving item 3. - window.simulate_prompt_answer(1, cx); - cx.foreground().run_until_parked(); + cx.simulate_prompt_answer(1); + cx.executor().run_until_parked(); // Item 3 is reloaded. There's a prompt to save item 4. - pane.read_with(cx, |pane, cx| { + 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().id(), item4.id()); + assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id()); }); - assert!(window.has_pending_prompt(cx)); + assert!(cx.has_pending_prompt()); // Confirm saving item 4. - window.simulate_prompt_answer(0, cx); - cx.foreground().run_until_parked(); + 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.read_with(cx, |pane, cx| { + 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().id(), item2.id()); + assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id()); }); } @@ -4904,18 +4628,16 @@ mod tests { async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.background()); - + let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); + let (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| { - window.add_view(cx, |cx| { - TestItem::new() + cx.new_view(|cx| { + TestItem::new(cx) .with_dirty(true) .with_project_items(&[TestProjectItem::new( project_entry_id, @@ -4925,8 +4647,8 @@ mod tests { }) }) .collect::>(); - let item_2_3 = window.add_view(cx, |cx| { - TestItem::new() + let item_2_3 = cx.new_view(|cx| { + TestItem::new(cx) .with_dirty(true) .with_singleton(false) .with_project_items(&[ @@ -4934,8 +4656,8 @@ mod tests { single_entry_items[3].read(cx).project_items[0].clone(), ]) }); - let item_3_4 = window.add_view(cx, |cx| { - TestItem::new() + let item_3_4 = cx.new_view(|cx| { + TestItem::new(cx) .with_dirty(true) .with_singleton(false) .with_project_items(&[ @@ -4961,70 +4683,70 @@ mod tests { pane.activate_item(2, true, true, cx); }); - workspace + 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 }); - //Need to cause an effect flush in order to respect new focus - workspace.update(cx, |workspace, cx| { - workspace.add_item(Box::new(item_3_4.clone()), cx); - cx.focus(&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. After those two + // 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_items(cx, SaveIntent::Close, move |_| true) + pane.close_all_items(&CloseAllItems::default(), cx).unwrap() }); - cx.foreground().run_until_parked(); - // Discard "Save all" prompt - window.simulate_prompt_answer(2, cx); + cx.executor().run_until_parked(); - cx.foreground().run_until_parked(); - left_pane.read_with(cx, |pane, cx| { + // 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)] ); }); - window.simulate_prompt_answer(0, cx); + cx.simulate_prompt_answer(0); - cx.foreground().run_until_parked(); - left_pane.read_with(cx, |pane, cx| { + 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)] ); }); - window.simulate_prompt_answer(0, cx); + cx.simulate_prompt_answer(0); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); close.await.unwrap(); - left_pane.read_with(cx, |pane, _| { + left_pane.update(cx, |pane, _| { assert_eq!(pane.items_len(), 0); }); } #[gpui::test] - async fn test_autosave(deterministic: Arc, cx: &mut gpui::TestAppContext) { + async fn test_autosave(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.background()); - + let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + 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 = window.add_view(cx, |cx| { - TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) + let item = cx.new_view(|cx| { + TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) }); - let item_id = item.id(); + let item_id = item.entity_id(); workspace.update(cx, |workspace, cx| { workspace.add_item(Box::new(item.clone()), cx); }); @@ -5040,9 +4762,9 @@ mod tests { }); // Deactivating the window saves the file. - window.simulate_deactivation(cx); - deterministic.run_until_parked(); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 1)); + 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| { @@ -5057,19 +4779,19 @@ mod tests { // Blurring the item saves the file. item.update(cx, |_, cx| cx.blur()); - deterministic.run_until_parked(); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 2)); + cx.executor().run_until_parked(); + item.update(cx, |item, _| assert_eq!(item.save_count, 2)); // Deactivating the window still saves the file. - window.simulate_activation(cx); + cx.simulate_activation(); item.update(cx, |item, cx| { cx.focus_self(); item.is_dirty = true; }); - window.simulate_deactivation(cx); + cx.simulate_deactivation(); - deterministic.run_until_parked(); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 3)); + cx.executor().run_until_parked(); + item.update(cx, |item, _| assert_eq!(item.save_count, 3)); // Autosave after delay. item.update(cx, |item, cx| { @@ -5079,16 +4801,16 @@ mod tests { }) }); item.is_dirty = true; - cx.emit(TestItemEvent::Edit); + cx.emit(ItemEvent::Edit); }); // Delay hasn't fully expired, so the file is still dirty and unsaved. - deterministic.advance_clock(Duration::from_millis(250)); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 3)); + 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. - deterministic.advance_clock(Duration::from_millis(250)); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 4)); + 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| { @@ -5105,8 +4827,8 @@ mod tests { }) .await .unwrap(); - assert!(!window.has_pending_prompt(cx)); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); + 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| { @@ -5119,33 +4841,32 @@ mod tests { item.is_dirty = true; cx.blur(); }); - deterministic.run_until_parked(); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); + 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) }); - deterministic.run_until_parked(); - assert!(window.has_pending_prompt(cx)); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); + 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.background()); + let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - let item = window.add_view(cx, |cx| { - TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) + let item = cx.new_view(|cx| { + TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) }); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone()); + 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| { @@ -5157,7 +4878,7 @@ mod tests { .detach(); }); - pane.read_with(cx, |pane, _| { + pane.update(cx, |pane, _| { assert!(!pane.can_navigate_backward()); assert!(!pane.can_navigate_forward()); }); @@ -5169,7 +4890,7 @@ mod tests { // Toolbar must be notified to re-render the navigation buttons assert_eq!(*toolbar_notify_count.borrow(), 1); - pane.read_with(cx, |pane, _| { + pane.update(cx, |pane, _| { assert!(pane.can_navigate_backward()); assert!(!pane.can_navigate_forward()); }); @@ -5179,376 +4900,375 @@ mod tests { .await .unwrap(); - assert_eq!(*toolbar_notify_count.borrow(), 3); - pane.read_with(cx, |pane, _| { + 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.background()); + // #[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 window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); + // 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.add_view(|_| TestPanel::new(DockPosition::Right)); - workspace.add_panel(panel.clone(), 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)); + // workspace + // .right_dock() + // .update(cx, |right_dock, cx| right_dock.set_open(true, cx)); - panel - }); + // panel + // }); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - pane.update(cx, |pane, cx| { - let item = cx.add_view(|_| TestItem::new()); - pane.add_item(Box::new(item), true, true, None, cx); - }); + // 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::(cx); - }); + // // Transfer focus from center to panel + // workspace.update(cx, |workspace, cx| { + // workspace.toggle_panel_focus::(cx); + // }); - workspace.read_with(cx, |workspace, cx| { - assert!(workspace.right_dock().read(cx).is_open()); - assert!(!panel.is_zoomed(cx)); - assert!(panel.has_focus(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::(cx); - }); + // // Transfer focus from panel to center + // workspace.update(cx, |workspace, cx| { + // workspace.toggle_panel_focus::(cx); + // }); - workspace.read_with(cx, |workspace, cx| { - assert!(workspace.right_dock().read(cx).is_open()); - assert!(!panel.is_zoomed(cx)); - assert!(!panel.has_focus(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); - }); + // // Close the dock + // workspace.update(cx, |workspace, cx| { + // workspace.toggle_dock(DockPosition::Right, cx); + // }); - workspace.read_with(cx, |workspace, cx| { - assert!(!workspace.right_dock().read(cx).is_open()); - assert!(!panel.is_zoomed(cx)); - assert!(!panel.has_focus(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); - }); + // // Open the dock + // workspace.update(cx, |workspace, cx| { + // workspace.toggle_dock(DockPosition::Right, cx); + // }); - workspace.read_with(cx, |workspace, cx| { - assert!(workspace.right_dock().read(cx).is_open()); - assert!(!panel.is_zoomed(cx)); - assert!(panel.has_focus(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) - }); + // // Focus and zoom panel + // panel.update(cx, |panel, cx| { + // cx.focus_self(); + // panel.set_zoomed(true, cx) + // }); - workspace.read_with(cx, |workspace, cx| { - assert!(workspace.right_dock().read(cx).is_open()); - assert!(panel.is_zoomed(cx)); - assert!(panel.has_focus(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::(cx); - }); + // // Transfer focus to the center closes the dock + // workspace.update(cx, |workspace, cx| { + // workspace.toggle_panel_focus::(cx); + // }); - workspace.read_with(cx, |workspace, cx| { - assert!(!workspace.right_dock().read(cx).is_open()); - assert!(panel.is_zoomed(cx)); - assert!(!panel.has_focus(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::(cx); - }); + // // Transferring focus back to the panel keeps it zoomed + // workspace.update(cx, |workspace, cx| { + // workspace.toggle_panel_focus::(cx); + // }); - workspace.read_with(cx, |workspace, cx| { - assert!(workspace.right_dock().read(cx).is_open()); - assert!(panel.is_zoomed(cx)); - assert!(panel.has_focus(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) - }); + // // Close the dock while it is zoomed + // workspace.update(cx, |workspace, cx| { + // workspace.toggle_dock(DockPosition::Right, cx) + // }); - workspace.read_with(cx, |workspace, cx| { - assert!(!workspace.right_dock().read(cx).is_open()); - assert!(panel.is_zoomed(cx)); - assert!(workspace.zoomed.is_none()); - assert!(!panel.has_focus(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) - }); + // // Opening the dock, when it's zoomed, retains focus + // workspace.update(cx, |workspace, cx| { + // workspace.toggle_dock(DockPosition::Right, cx) + // }); - workspace.read_with(cx, |workspace, cx| { - assert!(workspace.right_dock().read(cx).is_open()); - assert!(panel.is_zoomed(cx)); - assert!(workspace.zoomed.is_some()); - assert!(panel.has_focus(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)); + // // 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.read_with(cx, |workspace, cx| { - let pane = pane.read(cx); - assert!(!pane.is_zoomed()); - assert!(!pane.has_focus()); - assert!(workspace.right_dock().read(cx).is_open()); - assert!(workspace.zoomed.is_none()); - }); - } + // // 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.background()); + // #[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 window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); + // 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.add_view(|_| TestPanel::new(DockPosition::Left)); - 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.add_view(|_| TestPanel::new(DockPosition::Right)); - workspace.add_panel(panel_2.clone(), cx); - workspace - .right_dock() - .update(cx, |right_dock, cx| right_dock.set_open(true, 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().id(), - panel_1.id() - ); - assert_eq!( - left_dock.read(cx).active_panel_size(cx).unwrap(), - panel_1.size(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() - .id(), - panel_2.id() - ); + // 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) - }); + // (panel_1, panel_2) + // }); - // Move panel_1 to the right - panel_1.update(cx, |panel_1, cx| { - panel_1.set_position(DockPosition::Right, cx) - }); + // // 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().id(), - panel_1.id() - ); - assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.); + // 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); - }); + // // 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() - .id(), - panel_1.id() - ); - }); + // 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) - }); + // // 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().id(), - panel_1.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()); + // 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); - }); + // // 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); - }); + // 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(TestPanelEvent::Activated)); + // // 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.read_with(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().id(), - panel_1.id() - ); - assert!(panel_1.is_focused(cx)); - }); + // // 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(TestPanelEvent::Closed)); + // // 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.read_with(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().id(), - panel_1.id() - ); - }); + // // 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(TestPanelEvent::ZoomIn)); - workspace.read_with(cx, |workspace, _| { - assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); - assert_eq!(workspace.zoomed_position, Some(DockPosition::Left)); - }); + // // 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.read_with(cx, |workspace, _| { - assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); - assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); - }); + // // 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())); - // If focus is transferred to another view that's not a panel or another pane, we still show - // the panel as zoomed. - let focus_receiver = window.add_view(cx, |_| EmptyView); - focus_receiver.update(cx, |_, cx| cx.focus_self()); - workspace.read_with(cx, |workspace, _| { - assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); - assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); - }); + // 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.read_with(cx, |workspace, _| { - assert_eq!(workspace.zoomed, None); - assert_eq!(workspace.zoomed_position, None); - }); + // // 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 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. - focus_receiver.update(cx, |_, cx| cx.focus_self()); - workspace.read_with(cx, |workspace, _| { - assert_eq!(workspace.zoomed, None); - assert_eq!(workspace.zoomed_position, None); - }); + // // 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); + // }); - // When focus is transferred back to the panel, it is zoomed again. - panel_1.update(cx, |_, cx| cx.focus_self()); - workspace.read_with(cx, |workspace, _| { - assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); - assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); - }); + // // 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); + // }); - // Emitting a ZoomOut event unzooms the panel. - panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut)); - workspace.read_with(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)); + // }); - // Emit closed event on panel 1, which is active - panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed)); + // // 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); + // }); - // Now the left dock is closed, because panel_1 was the active panel - workspace.read_with(cx, |workspace, cx| { - let right_dock = workspace.right_dock(); - assert!(!right_dock.read(cx).is_open()); - }); - } + // // 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.foreground().forbid_parking(); cx.update(|cx| { - cx.set_global(SettingsStore::test(cx)); - theme::init((), cx); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + theme::init(theme::LoadThemes::JustBase, cx); language::init(cx); crate::init_settings(cx); Project::init_settings(cx); diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 6483167018..f3882a9dbd 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -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::load_via_json_merge(default_value, user_values) } diff --git a/crates/workspace/test.db b/crates/workspace/test.db deleted file mode 100644 index 9c94aa3162f548eeb541b3b23e9ccab9d09271e2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI4-*4Mg6vu7nx7Ee)fTAH)+-j(4q?R@*FxZ43ZQT-)rd{d+dm%E7X{|PPXS-YB z1&vOdkoFINCjJ2=B>n;<-a)(}#DBnF!S!+MB+aT@1gl2%wVdSo-h1vn_j8VKa!%52 z-D=u_#XfVro*A%3=>XaI&XGk!-gCWu|IpmA!f}_H8%u*hsr}G&sA+6UYTdV;E;ICxxf-!5 zk(A3su^o@JIX52qBG)7F*GqItR5} z-&(G(=)*1Bn=2v0e#`9GwEcCB%ox`BGjYrCXruE)qiwbu>-F|+T14M4vje3$&Wc)L zTSmLFY*cdD(mN$t;G|pEC0~ME;fOS!|Or z(+{_ZO+T{UF&BtP_%kt(%2PZow(Nte>0IgBHTlu!iMjgXWEL92&y1Nao`TjB&-3go z$hf1~J7!>oy8{nw2KMw^z8yGjS76Wc4qzVhO=fgI2)8EPIqKbDto)(n1zeAVGMi`R z_g&Ba!gT_ENf`bc<B=;u>MoMf%45FO^(rYDzw7kZ&Znp+6|4Mm%FsA8q%^b#mj5 z-P_^)*tu)_Hh%+&{fF+tCD%Q$Oozx)C)3&4b3HN+b%<LlCHH(FEu+4^nY;%i``%(ITUwhsYu`(j z=Xa`uau`!nobAa(@BTc|>)E@W8NQ|XgOV~rZ&1%7d|kI^+D^1$;@}6?VFHP25*g~H zR<7{L<-+$8KXHHn2!H?xfB*=900=x^1Rl5L%q#hCUP?YWj(iwQYW+*{QTnE;jx2B} zyo^?eDDrYfkxw&x->Jz5hSGs!(P45IBmv~9G@Ut;2}xq3FlI;QhiCA{vk6ITd)h9P zzKd?fusD=ROX6aRyOE})`?R0H73X6p{XCYVdnRexn<~Zs+D{Tcaex2_fB*=900@8p z2!H?xfB*=900=yv1m2Z1t-{qsrLSH5^ZfascbMtdh2>iGqlel;tu|M!&Q;%Fwc3r> z`T3U0d&%0L5)DQZkg63T8Rh&kN;OY1h4lGl7IN7FS4|wpCYn{V z(Il#Hl2p8yYKBDe;;?!kl8$B(E2bo~q@r1JLd6o11%-ZP6?Kto@<=sC})O+S}TTm&wVo77zdd5C8!X009sH0T2KI5CDOTCQwfE zY=b!0pi)0^exX49#5slhbeg9UgmH0>h7)HGveZwUGRROrah@Pe{lv+E6!jBl1e9rx JxggF1{0&nU)e!&y diff --git a/crates/workspace2/Cargo.toml b/crates/workspace2/Cargo.toml deleted file mode 100644 index a06ac6e3e0..0000000000 --- a/crates/workspace2/Cargo.toml +++ /dev/null @@ -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 diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs deleted file mode 100644 index bd965f63d4..0000000000 --- a/crates/workspace2/src/dock.rs +++ /dev/null @@ -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 { - 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); - fn size(&self, cx: &WindowContext) -> Pixels; - fn set_size(&mut self, size: Option, cx: &mut ViewContext); - fn icon(&self, cx: &WindowContext) -> Option; - fn icon_tooltip(&self, cx: &WindowContext) -> Option<&'static str>; - fn toggle_action(&self) -> Box; - fn icon_label(&self, _: &WindowContext) -> Option { - None - } - fn is_zoomed(&self, _cx: &WindowContext) -> bool { - false - } - fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext) {} - fn set_active(&mut self, _active: bool, _cx: &mut ViewContext) {} -} - -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, cx: &mut WindowContext); - fn icon(&self, cx: &WindowContext) -> Option; - fn icon_tooltip(&self, cx: &WindowContext) -> Option<&'static str>; - fn toggle_action(&self, cx: &WindowContext) -> Box; - fn icon_label(&self, cx: &WindowContext) -> Option; - fn focus_handle(&self, cx: &AppContext) -> FocusHandle; - fn to_any(&self) -> AnyView; -} - -impl PanelHandle for View -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, cx: &mut WindowContext) { - self.update(cx, |this, cx| this.set_size(size, cx)) - } - - fn icon(&self, cx: &WindowContext) -> Option { - 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 { - self.read(cx).toggle_action() - } - - fn icon_label(&self, cx: &WindowContext) -> Option { - 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, - 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, - // todo!() - // context_menu: View, - _subscriptions: [Subscription; 2], -} - -pub struct PanelButtons { - dock: View, -} - -impl Dock { - pub fn new(position: DockPosition, cx: &mut ViewContext) -> View { - let focus_handle = cx.focus_handle(); - - let dock = cx.new_view(|cx: &mut ViewContext| { - 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(&self) -> Option> { - self.panel_entries - .iter() - .find_map(|entry| entry.panel.to_any().clone().downcast().ok()) - } - - pub fn panel_index_for_type(&self) -> Option { - self.panel_entries - .iter() - .position(|entry| entry.panel.to_any().downcast::().is_ok()) - } - - pub fn panel_index_for_persistent_name( - &self, - ui_name: &str, - _cx: &AppContext, - ) -> Option { - 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) { - 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) { - 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) { - for entry in &mut self.panel_entries { - if entry.panel.is_zoomed(cx) { - entry.panel.set_zoomed(false, cx); - } - } - } - - pub(crate) fn add_panel( - &mut self, - panel: View, - workspace: WeakView, - cx: &mut ViewContext, - ) { - 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(&mut self, panel: &View, cx: &mut ViewContext) { - 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) { - 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> { - let entry = self.visible_entry()?; - Some(&entry.panel) - } - - pub fn active_panel(&self) -> Option<&Arc> { - 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> { - 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 { - 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 { - 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, cx: &mut ViewContext) { - 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 { - 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) -> 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, cx: &mut ViewContext) -> Self { - cx.observe(&dock, |_, _, cx| cx.notify()).detach(); - Self { dock } - } -} - -impl Render for PanelButtons { - fn render(&mut self, cx: &mut ViewContext) -> 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, - ) { - // 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 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) -> 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.position = position; - cx.emit(PanelEvent::ChangePosition); - } - - fn size(&self, _: &WindowContext) -> Pixels { - self.size - } - - fn set_size(&mut self, size: Option, _: &mut ViewContext) { - self.size = size.unwrap_or(px(300.)); - } - - fn icon(&self, _: &WindowContext) -> Option { - None - } - - fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { - None - } - - fn toggle_action(&self) -> Box { - ToggleTestPanel.boxed_clone() - } - - fn is_zoomed(&self, _: &WindowContext) -> bool { - self.zoomed - } - - fn set_zoomed(&mut self, zoomed: bool, _cx: &mut ViewContext) { - self.zoomed = zoomed; - } - - fn set_active(&mut self, active: bool, _cx: &mut ViewContext) { - self.active = active; - } - } - - impl FocusableView for TestPanel { - fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { - self.focus_handle.clone() - } - } -} diff --git a/crates/workspace2/src/item.rs b/crates/workspace2/src/item.rs deleted file mode 100644 index 38b7663030..0000000000 --- a/crates/workspace2/src/item.rs +++ /dev/null @@ -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, - close_position: Option, -} - -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::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, HighlightStyle)>>, -} - -pub trait Item: FocusableView + EventEmitter { - type Event; - - fn deactivated(&mut self, _: &mut ViewContext) {} - fn workspace_deactivated(&mut self, _: &mut ViewContext) {} - fn navigate(&mut self, _: Box, _: &mut ViewContext) -> bool { - false - } - fn tab_tooltip_text(&self, _: &AppContext) -> Option { - None - } - fn tab_description(&self, _: usize, _: &AppContext) -> Option { - None - } - fn tab_content(&self, detail: Option, 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) {} - fn clone_on_split( - &self, - _workspace_id: WorkspaceId, - _: &mut ViewContext, - ) -> Option> - 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, _cx: &mut ViewContext) -> Task> { - unimplemented!("save() must be implemented if can_save() returns true") - } - fn save_as( - &mut self, - _project: Model, - _abs_path: PathBuf, - _cx: &mut ViewContext, - ) -> Task> { - unimplemented!("save_as() must be implemented if can_save() returns true") - } - fn reload( - &mut self, - _project: Model, - _cx: &mut ViewContext, - ) -> Task> { - 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, - _: &'a AppContext, - ) -> Option { - if TypeId::of::() == type_id { - Some(self_handle.clone().into()) - } else { - None - } - } - - fn as_searchable(&self, _: &View) -> Option> { - None - } - - fn breadcrumb_location(&self) -> ToolbarItemLocation { - ToolbarItemLocation::Hidden - } - - fn breadcrumbs(&self, _theme: &Theme, _cx: &AppContext) -> Option> { - None - } - - fn added_to_workspace(&mut self, _workspace: &mut Workspace, _cx: &mut ViewContext) {} - - fn serialized_item_kind() -> Option<&'static str> { - None - } - - fn deserialize( - _project: Model, - _workspace: WeakView, - _workspace_id: WorkspaceId, - _item_id: ItemId, - _cx: &mut ViewContext, - ) -> Task>> { - 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> { - None - } -} - -pub trait ItemHandle: 'static + Send { - fn subscribe_to_item_events( - &self, - cx: &mut WindowContext, - handler: Box, - ) -> gpui::Subscription; - fn focus_handle(&self, cx: &WindowContext) -> FocusHandle; - fn tab_tooltip_text(&self, cx: &AppContext) -> Option; - fn tab_description(&self, detail: usize, cx: &AppContext) -> Option; - fn tab_content(&self, detail: Option, selected: bool, cx: &WindowContext) -> AnyElement; - fn dragged_tab_content(&self, detail: Option, cx: &WindowContext) -> AnyElement; - fn project_path(&self, cx: &AppContext) -> Option; - 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; - fn clone_on_split( - &self, - workspace_id: WorkspaceId, - cx: &mut WindowContext, - ) -> Option>; - fn added_to_pane( - &self, - workspace: &mut Workspace, - pane: View, - cx: &mut ViewContext, - ); - fn deactivated(&self, cx: &mut WindowContext); - fn workspace_deactivated(&self, cx: &mut WindowContext); - fn navigate(&self, data: Box, 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, cx: &mut WindowContext) -> Task>; - fn save_as( - &self, - project: Model, - abs_path: PathBuf, - cx: &mut WindowContext, - ) -> Task>; - fn reload(&self, project: Model, cx: &mut WindowContext) -> Task>; - fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option; - fn to_followable_item_handle(&self, cx: &AppContext) -> Option>; - fn on_release( - &self, - cx: &mut AppContext, - callback: Box, - ) -> gpui::Subscription; - fn to_searchable_item_handle(&self, cx: &AppContext) -> Option>; - fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation; - fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option>; - fn serialized_item_kind(&self) -> Option<&'static str>; - fn show_toolbar(&self, cx: &AppContext) -> bool; - fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option>; -} - -pub trait WeakItemHandle: Send + Sync { - fn id(&self) -> EntityId; - fn upgrade(&self) -> Option>; -} - -impl dyn ItemHandle { - pub fn downcast(&self) -> Option> { - self.to_any().downcast().ok() - } - - pub fn act_as(&self, cx: &AppContext) -> Option> { - self.act_as_type(TypeId::of::(), cx) - .and_then(|t| t.downcast().ok()) - } -} - -impl ItemHandle for View { - fn subscribe_to_item_events( - &self, - cx: &mut WindowContext, - handler: Box, - ) -> 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 { - self.read(cx).tab_tooltip_text(cx) - } - - fn tab_description(&self, detail: usize, cx: &AppContext) -> Option { - self.read(cx).tab_description(detail, cx) - } - - fn tab_content(&self, detail: Option, selected: bool, cx: &WindowContext) -> AnyElement { - self.read(cx).tab_content(detail, selected, cx) - } - - fn dragged_tab_content(&self, detail: Option, cx: &WindowContext) -> AnyElement { - self.read(cx).tab_content(detail, true, cx) - } - - fn project_path(&self, cx: &AppContext) -> Option { - 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 { - Box::new(self.clone()) - } - - fn clone_on_split( - &self, - workspace_id: WorkspaceId, - cx: &mut WindowContext, - ) -> Option> { - self.update(cx, |item, cx| item.clone_on_split(workspace_id, cx)) - .map(|handle| Box::new(handle) as Box) - } - - fn added_to_pane( - &self, - workspace: &mut Workspace, - pane: View, - cx: &mut ViewContext, - ) { - 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, 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, cx: &mut WindowContext) -> Task> { - self.update(cx, |item, cx| item.save(project, cx)) - } - - fn save_as( - &self, - project: Model, - abs_path: PathBuf, - cx: &mut WindowContext, - ) -> Task> { - self.update(cx, |item, cx| item.save_as(project, abs_path, cx)) - } - - fn reload(&self, project: Model, cx: &mut WindowContext) -> Task> { - self.update(cx, |item, cx| item.reload(project, cx)) - } - - fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a AppContext) -> Option { - self.read(cx).act_as_type(type_id, self, cx) - } - - fn to_followable_item_handle(&self, cx: &AppContext) -> Option> { - if cx.has_global::() { - let builders = cx.global::(); - let item = self.to_any(); - Some(builders.get(&item.entity_type())?.1(&item)) - } else { - None - } - } - - fn on_release( - &self, - cx: &mut AppContext, - callback: Box, - ) -> gpui::Subscription { - cx.observe_release(self, move |_, cx| callback(cx)) - } - - fn to_searchable_item_handle(&self, cx: &AppContext) -> Option> { - 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> { - 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> { - self.read(cx).pixel_position_of_cursor(cx) - } -} - -impl From> for AnyView { - fn from(val: Box) -> Self { - val.to_any() - } -} - -impl From<&Box> for AnyView { - fn from(val: &Box) -> Self { - val.to_any() - } -} - -impl Clone for Box { - fn clone(&self) -> Box { - self.boxed_clone() - } -} - -impl WeakItemHandle for WeakView { - fn id(&self) -> EntityId { - self.entity_id() - } - - fn upgrade(&self) -> Option> { - self.upgrade().map(|v| Box::new(v) as Box) - } -} - -pub trait ProjectItem: Item { - type Item: project::Item; - - fn for_project_item( - project: Model, - item: Model, - cx: &mut ViewContext, - ) -> Self - where - Self: Sized; -} - -pub enum FollowEvent { - Unfollow, -} - -pub trait FollowableItem: Item { - fn remote_id(&self) -> Option; - fn to_state_proto(&self, cx: &WindowContext) -> Option; - fn from_state_proto( - pane: View, - project: View, - id: ViewId, - state: &mut Option, - cx: &mut WindowContext, - ) -> Option>>>; - fn to_follow_event(event: &Self::Event) -> Option; - fn add_event_to_update_proto( - &self, - event: &Self::Event, - update: &mut Option, - cx: &WindowContext, - ) -> bool; - fn apply_update_proto( - &mut self, - project: &Model, - message: proto::update_view::Variant, - cx: &mut ViewContext, - ) -> Task>; - fn is_project_item(&self, cx: &WindowContext) -> bool; - fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext); -} - -pub trait FollowableItemHandle: ItemHandle { - fn remote_id(&self, client: &Arc, cx: &WindowContext) -> Option; - fn set_leader_peer_id(&self, leader_peer_id: Option, cx: &mut WindowContext); - fn to_state_proto(&self, cx: &WindowContext) -> Option; - fn add_event_to_update_proto( - &self, - event: &dyn Any, - update: &mut Option, - cx: &WindowContext, - ) -> bool; - fn to_follow_event(&self, event: &dyn Any) -> Option; - fn apply_update_proto( - &self, - project: &Model, - message: proto::update_view::Variant, - cx: &mut WindowContext, - ) -> Task>; - fn is_project_item(&self, cx: &WindowContext) -> bool; -} - -impl FollowableItemHandle for View { - fn remote_id(&self, client: &Arc, cx: &WindowContext) -> Option { - 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, 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 { - self.read(cx).to_state_proto(cx) - } - - fn add_event_to_update_proto( - &self, - event: &dyn Any, - update: &mut Option, - 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 { - T::to_follow_event(event.downcast_ref()?) - } - - fn apply_update_proto( - &self, - project: &Model, - message: proto::update_view::Variant, - cx: &mut WindowContext, - ) -> Task> { - 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, - pub project_path: Option, - } - - 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>, - pub nav_history: Option, - pub tab_descriptions: Option>, - pub tab_detail: Cell>, - focus_handle: gpui::FocusHandle, - } - - impl project::Item for TestProjectItem { - fn entry_id(&self, _: &AppContext) -> Option { - self.entry_id - } - - fn project_path(&self, _: &AppContext) -> Option { - 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 { - 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 { - cx.new_model(|_| Self { - project_path: None, - entry_id: None, - }) - } - } - - impl TestItem { - pub fn new(cx: &mut ViewContext) -> 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 { - 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]) -> 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.push_to_nav_history(cx); - self.state = state; - } - - fn push_to_nav_history(&mut self, cx: &mut ViewContext) { - 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) -> impl IntoElement { - gpui::div().track_focus(&self.focus_handle) - } - } - - impl EventEmitter 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 { - 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, - _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.nav_history = Some(history); - } - - fn navigate(&mut self, state: Box, _: &mut ViewContext) -> bool { - let state = *state.downcast::().unwrap_or_default(); - if state != self.state { - self.state = state; - true - } else { - false - } - } - - fn deactivated(&mut self, cx: &mut ViewContext) { - self.push_to_nav_history(cx); - } - - fn clone_on_split( - &self, - _workspace_id: WorkspaceId, - cx: &mut ViewContext, - ) -> Option> - 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, - _: &mut ViewContext, - ) -> Task> { - self.save_count += 1; - self.is_dirty = false; - Task::ready(Ok(())) - } - - fn save_as( - &mut self, - _: Model, - _: std::path::PathBuf, - _: &mut ViewContext, - ) -> Task> { - self.save_as_count += 1; - self.is_dirty = false; - Task::ready(Ok(())) - } - - fn reload( - &mut self, - _: Model, - _: &mut ViewContext, - ) -> Task> { - self.reload_count += 1; - self.is_dirty = false; - Task::ready(Ok(())) - } - - fn serialized_item_kind() -> Option<&'static str> { - Some("TestItem") - } - - fn deserialize( - _project: Model, - _workspace: WeakView, - workspace_id: WorkspaceId, - _item_id: ItemId, - cx: &mut ViewContext, - ) -> Task>> { - let view = cx.new_view(|cx| Self::new_deserialized(workspace_id, cx)); - Task::Ready(Some(anyhow::Ok(view))) - } - } -} diff --git a/crates/workspace2/src/notifications.rs b/crates/workspace2/src/notifications.rs deleted file mode 100644 index 85ecf52a84..0000000000 --- a/crates/workspace2/src/notifications.rs +++ /dev/null @@ -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 + Render {} - -impl + Render> Notification for V {} - -pub trait NotificationHandle: Send { - fn id(&self) -> EntityId; - fn to_any(&self) -> AnyView; -} - -impl NotificationHandle for View { - 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>, -} - -impl std::ops::Deref for NotificationTracker { - type Target = HashMap>; - - 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( - &self, - id: usize, - cx: &ViewContext, - ) -> bool { - cx.global::() - .get(&TypeId::of::()) - .map(|ids| ids.contains(&id)) - .unwrap_or(false) - } - - pub fn show_notification_once( - &mut self, - id: usize, - cx: &mut ViewContext, - build_notification: impl FnOnce(&mut ViewContext) -> View, - ) { - if !self.has_shown_notification_once::(id, cx) { - let tracker = cx.global_mut::(); - let entry = tracker.entry(TypeId::of::()).or_default(); - entry.push(id); - self.show_notification::(id, cx, build_notification) - } - } - - pub fn show_notification( - &mut self, - id: usize, - cx: &mut ViewContext, - build_notification: impl FnOnce(&mut ViewContext) -> View, - ) { - let type_id = TypeId::of::(); - 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(&mut self, err: &E, cx: &mut ViewContext) - 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(&mut self, id: usize, cx: &mut ViewContext) { - let type_id = TypeId::of::(); - - self.dismiss_notification_internal(type_id, id, cx) - } - - pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext) { - self.dismiss_notification::(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.dismiss_notification::(id, cx); - } - - fn dismiss_notification_internal( - &mut self, - type_id: TypeId, - id: usize, - cx: &mut ViewContext, - ) { - 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)>>, - click_message: Option, - } - - impl EventEmitter for MessageNotification {} - - impl MessageNotification { - pub fn new(message: S) -> MessageNotification - where - S: Into, - { - Self { - message: message.into(), - on_click: None, - click_message: None, - } - } - - pub fn with_click_message(mut self, message: S) -> Self - where - S: Into, - { - self.click_message = Some(message.into()); - self - } - - pub fn on_click(mut self, on_click: F) -> Self - where - F: 'static + Fn(&mut ViewContext), - { - self.on_click = Some(Arc::new(on_click)); - self - } - - pub fn dismiss(&mut self, cx: &mut ViewContext) { - cx.emit(DismissEvent); - } - } - - impl Render for MessageNotification { - fn render(&mut self, cx: &mut ViewContext) -> 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) -> gpui::AnyElement { - // 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::(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::( - // 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, - ) -> Option; - - fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option; -} - -impl NotifyResultExt for Result -where - E: std::fmt::Debug, -{ - type Ok = T; - - fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext) -> Option { - 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 { - match self { - Ok(value) => Some(value), - Err(err) => { - log::error!("TODO {err:?}"); - cx.update(|view, cx| { - if let Ok(workspace) = view.downcast::() { - workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx)) - } - }) - .ok(); - None - } - } - } -} diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs deleted file mode 100644 index 7664231e26..0000000000 --- a/crates/workspace2/src/pane.rs +++ /dev/null @@ -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, -// } - -// #[derive(Clone, PartialEq)] -// pub struct CloseItemsToTheLeftById { -// pub item_id: usize, -// pub pane: WeakView, -// } - -// #[derive(Clone, PartialEq)] -// pub struct CloseItemsToTheRightById { -// pub item_id: usize, -// pub pane: WeakView, -// } - -#[derive(Clone, PartialEq, Debug, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -pub struct CloseActiveItem { - pub save_intent: Option, -} - -#[derive(Clone, PartialEq, Debug, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -pub struct CloseAllItems { - pub save_intent: Option, -} - -#[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 }, - 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>, - activation_history: Vec, - zoomed: bool, - was_focused: bool, - active_item_index: usize, - last_focused_view_by_item: HashMap, - nav_history: NavHistory, - toolbar: View, - new_item_menu: Option>, - split_item_menu: Option>, - // tab_context_menu: View, - workspace: WeakView, - project: Model, - drag_split_direction: Option, - can_drop_predicate: Option bool>>, - can_split: bool, - render_tab_bar_buttons: Rc) -> AnyElement>, - _subscriptions: Vec, - tab_bar_scroll_handle: ScrollHandle, - display_nav_history_buttons: bool, -} - -pub struct ItemNavHistory { - history: NavHistory, - item: Arc, -} - -#[derive(Clone)] -pub struct NavHistory(Arc>); - -struct NavHistoryState { - mode: NavigationMode, - backward_stack: VecDeque, - forward_stack: VecDeque, - closed_stack: VecDeque, - paths_by_item: HashMap)>, - pane: WeakView, - next_timestamp: Arc, -} - -#[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, - pub data: Option>, - pub timestamp: usize, -} - -#[derive(Clone)] -pub struct DraggedTab { - pub pane: View, - pub ix: usize, - pub item_id: EntityId, - pub detail: usize, - pub is_active: bool, -} - -// pub struct DraggedItem { -// pub handle: Box, -// pub pane: WeakView, -// } - -// pub enum ReorderBehavior { -// None, -// MoveAfterActive, -// MoveToIndex(usize), -// } - -// #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// enum TabBarContextMenuKind { -// New, -// Split, -// } - -// struct TabBarContextMenu { -// kind: TabBarContextMenuKind, -// handle: View, -// } - -// impl TabBarContextMenu { -// fn handle_if_kind(&self, kind: TabBarContextMenuKind) -> Option> { -// if self.kind == kind { -// return Some(self.handle.clone()); -// } -// None -// } -// } - -// #[allow(clippy::too_many_arguments)] -// fn nav_button)>( -// svg_path: &'static str, -// style: theme2::Interactive, -// nav_button_height: f32, -// tooltip_style: TooltipStyle, -// enabled: bool, -// on_click: F, -// tooltip_action: A, -// action_name: &str, -// cx: &mut ViewContext, -// ) -> AnyElement { -// MouseEventHandler::new::(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::( -// 0, -// action_name.to_string(), -// Some(Box::new(tooltip_action)), -// tooltip_style, -// cx, -// ) -// .contained() -// .into_any_named("nav button") -// } - -impl EventEmitter for Pane {} - -impl Pane { - pub fn new( - workspace: WeakView, - project: Model, - next_timestamp: Arc, - can_drop_predicate: Option bool + 'static>>, - cx: &mut ViewContext, - ) -> 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) { - 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) -> 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.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(&mut self, can_drop: F) - // where - // F: 'static + Fn(&DragAndDrop, &WindowContext) -> bool, - // { - // self.can_drop = Rc::new(can_drop); - // } - - pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext) { - self.can_split = can_split; - cx.notify(); - } - - pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext) { - self.toolbar.update(cx, |toolbar, cx| { - toolbar.set_can_navigate(can_navigate, cx); - }); - cx.notify(); - } - - pub fn set_render_tab_bar_buttons(&mut self, cx: &mut ViewContext, render: F) - where - F: 'static + Fn(&mut Pane, &mut ViewContext) -> AnyElement, - { - self.render_tab_bar_buttons = Rc::new(render); - cx.notify(); - } - - pub fn nav_history_for_item(&self, item: &View) -> 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) { - 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) { - 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.toolbar.update(cx, |_, cx| cx.notify()); - } - - pub(crate) fn open_item( - &mut self, - project_entry_id: Option, - focus_item: bool, - cx: &mut ViewContext, - build_item: impl FnOnce(&mut ViewContext) -> Box, - ) -> Box { - 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, - activate_pane: bool, - focus_item: bool, - destination_index: Option, - cx: &mut ViewContext, - ) { - 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> + DoubleEndedIterator { - self.items.iter() - } - - pub fn items_of_type(&self) -> impl '_ + Iterator> { - self.items - .iter() - .filter_map(|item| item.to_any().downcast().ok()) - } - - pub fn active_item(&self) -> Option> { - self.items.get(self.active_item_index).cloned() - } - - pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option> { - self.items - .get(self.active_item_index)? - .pixel_position_of_cursor(cx) - } - - pub fn item_for_entry( - &self, - entry_id: ProjectEntryId, - cx: &AppContext, - ) -> Option> { - 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 { - 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) { - 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, - ) { - 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) { - 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) { - 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, - ) -> Option>> { - 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, - ) -> Task> { - 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, - ) -> Option>> { - 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, - ) -> Option>> { - 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, - ) -> Option>> { - 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, - ) -> Task> { - 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, - ) -> Option>> { - 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, - ) -> Task> { - 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, - ) -> Option>> { - 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>, - 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, - mut save_intent: SaveIntent, - should_close: impl Fn(EntityId) -> bool, - ) -> Task> { - // 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.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, - pane: &WeakView, - item_ix: usize, - item: &dyn ItemHandle, - save_intent: SaveIntent, - cx: &mut AsyncWindowContext, - ) -> Result { - 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, - cx: &mut WindowContext, - ) -> Task> { - if Self::can_autosave_item(item, cx) { - item.save(project, cx) - } else { - Task::ready(Ok(())) - } - } - - pub fn focus(&mut self, cx: &mut ViewContext) { - cx.focus(&self.focus_handle); - } - - pub fn focus_active_item(&mut self, cx: &mut ViewContext) { - 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) { - cx.emit(Event::Split(direction)); - } - - // fn deploy_split_menu(&mut self, cx: &mut ViewContext) { - // 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.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, - // ) { - // 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 { - &self.toolbar - } - - pub fn handle_deleted_project_item( - &mut self, - entry_id: ProjectEntryId, - cx: &mut ViewContext, - ) -> 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) { - 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) { - 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, - 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::(|tab| tab.bg(cx.theme().colors().drop_target_background)) - .drag_over::(|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) - .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::(|bar| { - bar.bg(cx.theme().colors().drop_target_background) - }) - .drag_over::(|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) -> 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 { - let mut tab_details = self.items.iter().map(|_| 0).collect::>(); - - 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.zoomed = zoomed; - cx.notify(); - } - - pub fn is_zoomed(&self) -> bool { - self.zoomed - } - - fn handle_drag_move(&mut self, event: &DragMoveEvent, cx: &mut ViewContext) { - 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) -> 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::(cx.listener(Self::handle_drag_move)) - .on_drag_move::(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::("", |style| style.visible()) - .group_drag_over::("", |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(&mut self, data: Option, cx: &mut WindowContext) { - self.history.push(data, self.item.clone(), cx); - } - - pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option { - self.history.pop(NavigationMode::GoingBack, cx) - } - - pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option { - self.history.pop(NavigationMode::GoingForward, cx) - } -} - -impl NavHistory { - pub fn for_each_entry( - &self, - cx: &AppContext, - mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option)), - ) { - 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 { - 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( - &mut self, - data: Option, - item: Arc, - 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), - 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), - 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), - 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), - 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)> { - 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) -> 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, - label: &str, - is_dirty: bool, - cx: &mut VisualTestContext, - ) -> Box> { - 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( - pane: &View, - labels: [&str; COUNT], - cx: &mut VisualTestContext, - ) -> [Box>; 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( - pane: &View, - 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::() - .unwrap() - .read(cx) - .label - .clone(); - if ix == pane.active_item_index { - state.push('*'); - } - if item.is_dirty(cx) { - state.push('^'); - } - state - }) - .collect::>(); - - assert_eq!( - actual_states, expected_states, - "pane items do not match expectation" - ); - }) - } -} - -impl Render for DraggedTab { - fn render(&mut self, cx: &mut ViewContext) -> 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) - } -} diff --git a/crates/workspace2/src/pane/dragged_item_receiver.rs b/crates/workspace2/src/pane/dragged_item_receiver.rs deleted file mode 100644 index 3e1f6393a6..0000000000 --- a/crates/workspace2/src/pane/dragged_item_receiver.rs +++ /dev/null @@ -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( - pane: &Pane, - region_id: usize, - drop_index: usize, - allow_same_pane: bool, - split_margin: Option, - cx: &mut ViewContext, - render_child: F, -) -> MouseEventHandler -where - Tag: 'static, - D: Element, - F: FnOnce(&mut MouseState, &mut ViewContext) -> D, -{ - let drag_and_drop = cx.global::>(); - let drag_position = if (pane.can_drop)(drag_and_drop, cx) { - drag_and_drop - .currently_dragged::(cx.window()) - .map(|(drag_position, _)| drag_position) - .or_else(|| { - drag_and_drop - .currently_dragged::(cx.window()) - .map(|(drag_position, _)| drag_position) - }) - } else { - None - }; - - let mut handler = MouseEventHandler::above::(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::>(); - - if drag_and_drop - .currently_dragged::(cx.window()) - .is_some() - || drag_and_drop - .currently_dragged::(cx.window()) - .is_some() - { - cx.notify(); - } else { - cx.propagate_event(); - } - }) - } - - handler -} - -pub fn handle_dropped_item( - event: MouseUp, - workspace: WeakViewHandle, - pane: &WeakViewHandle, - index: usize, - allow_same_pane: bool, - split_margin: Option, - cx: &mut EventContext, -) { - enum Action { - Move(WeakViewHandle, usize), - Open(ProjectEntryId), - } - let drag_and_drop = cx.global::>(); - let action = if let Some((_, dragged_item)) = - drag_and_drop.currently_dragged::(cx.window()) - { - Action::Move(dragged_item.pane.clone(), dragged_item.handle.id()) - } else if let Some((_, project_entry)) = - drag_and_drop.currently_dragged::(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 { - 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 -} diff --git a/crates/workspace2/src/pane_group.rs b/crates/workspace2/src/pane_group.rs deleted file mode 100644 index c6eaa71663..0000000000 --- a/crates/workspace2/src/pane_group.rs +++ /dev/null @@ -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) -> Self { - Self { - root: Member::Pane(pane), - } - } - - pub fn split( - &mut self, - old_pane: &View, - new_pane: &View, - 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) -> Option> { - match &self.root { - Member::Pane(_) => None, - Member::Axis(axis) => axis.bounding_box_for_pane(pane), - } - } - - pub fn pane_at_pixel_position(&self, coordinate: Point) -> Option<&View> { - 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) -> Result { - 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, to: &View) { - match &mut self.root { - Member::Pane(_) => {} - Member::Axis(axis) => axis.swap(from, to), - }; - } - - pub(crate) fn render( - &self, - project: &Model, - follower_states: &HashMap, FollowerState>, - active_call: Option<&Model>, - active_pane: &View, - zoomed: Option<&AnyWeakView>, - app_state: &Arc, - cx: &mut ViewContext, - ) -> impl IntoElement { - self.root.render( - project, - 0, - follower_states, - active_call, - active_pane, - zoomed, - app_state, - cx, - ) - } - - pub(crate) fn panes(&self) -> Vec<&View> { - let mut panes = Vec::new(); - self.root.collect_panes(&mut panes); - panes - } - - pub(crate) fn first_pane(&self) -> View { - self.root.first_pane() - } -} - -#[derive(Clone)] -pub(crate) enum Member { - Axis(PaneAxis), - Pane(View), -} - -impl Member { - fn new_axis(old_pane: View, new_pane: View, 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) -> bool { - match self { - Member::Axis(axis) => axis.members.iter().any(|member| member.contains(needle)), - Member::Pane(pane) => pane == needle, - } - } - - fn first_pane(&self) -> View { - match self { - Member::Axis(axis) => axis.members[0].first_pane(), - Member::Pane(pane) => pane.clone(), - } - } - - pub fn render( - &self, - project: &Model, - basis: usize, - follower_states: &HashMap, FollowerState>, - active_call: Option<&Model>, - active_pane: &View, - zoomed: Option<&AnyWeakView>, - app_state: &Arc, - cx: &mut ViewContext, - ) -> 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>) { - 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, - pub flexes: Arc>>, - pub bounding_boxes: Arc>>>>, -} - -impl PaneAxis { - pub fn new(axis: Axis, members: Vec) -> 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, flexes: Option>) -> 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, - new_pane: &View, - 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) -> Result> { - 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, to: &View) { - 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) -> Option> { - 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) -> Option<&View> { - 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, - basis: usize, - follower_states: &HashMap, FollowerState>, - active_call: Option<&Model>, - active_pane: &View, - zoomed: Option<&AnyWeakView>, - app_state: &Arc, - cx: &mut ViewContext, - ) -> 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 { - 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, length: Pixels) -> Bounds { - 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>>, - bounding_boxes: Arc>>>>, - ) -> PaneAxisElement { - PaneAxisElement { - axis, - basis, - flexes, - bounding_boxes, - children: SmallVec::new(), - active_pane_ix: None, - } - } - - pub struct PaneAxisElement { - axis: Axis, - basis: usize, - flexes: Arc>>, - bounding_boxes: Arc>>>>, - children: SmallVec<[AnyElement; 2]>, - active_pane_ix: Option, - } - - impl PaneAxisElement { - pub fn with_active_pane(mut self, active_pane_ix: Option) -> Self { - self.active_pane_ix = active_pane_ix; - self - } - - fn compute_resize( - flexes: &Arc>>, - e: &MouseMoveEvent, - ix: usize, - axis: Axis, - child_start: Point, - container_size: Size, - 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>>, - dragged_handle: Rc>>, - axis: Axis, - ix: usize, - pane_bounds: Bounds, - axis_bounds: Bounds, - 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 { - Some(self.basis.into()) - } - - fn into_element(self) -> Self::Element { - self - } - } - - impl Element for PaneAxisElement { - type State = Rc>>; - - fn request_layout( - &mut self, - state: Option, - 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, - 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::() - flexes.len() as f32).abs() < 0.001 - } -} diff --git a/crates/workspace2/src/persistence.rs b/crates/workspace2/src/persistence.rs deleted file mode 100644 index 5358ee3f4c..0000000000 --- a/crates/workspace2/src/persistence.rs +++ /dev/null @@ -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>, - // dock_visible: bool, // Deprecated - // dock_anchor: DockAnchor, // Deprecated - // dock_pane: Option, // Deprecated - // left_sidebar_open: boolean, - // timestamp: String, // UTC YYYY-MM-DD HH:MM:SS - // window_state: String, // WindowBounds Discriminant - // window_x: Option, // WindowBounds::Fixed RectF x - // window_y: Option, // WindowBounds::Fixed RectF y - // window_width: Option, // WindowBounds::Fixed RectF width - // window_height: Option, // WindowBounds::Fixed RectF height - // display: Option, // Display id - // ) - // - // pane_groups( - // group_id: usize, // Primary key for pane_groups - // workspace_id: usize, // References workspaces table - // parent_group_id: Option, // None indicates that this is the root node - // position: Optiopn, // None indicates that this is the root node - // axis: Option, // 'Vertical', 'Horizontal' - // flexes: Option>, // 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, // References pane_groups. If none, this is the root - // position: Option, // 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>( - &self, - worktree_roots: &[P], - ) -> Option { - 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, - Option, - 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 { - INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id - } - } - - query! { - fn recent_workspaces() -> Result> { - 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> { - 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> { - Ok(self - .recent_workspaces_on_disk() - .await? - .into_iter() - .next() - .map(|(_, location)| location)) - } - - fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result { - 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, - ) -> Result> { - type GroupKey = (Option, WorkspaceId); - type GroupOrPane = ( - Option, - Option, - Option, - Option, - Option, - ); - self.select_bound::(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::>(&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::>() - } - - 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 { - 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> { - 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::(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::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>( - 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); - } -} diff --git a/crates/workspace2/src/persistence/model.rs b/crates/workspace2/src/persistence/model.rs deleted file mode 100644 index f204e5152c..0000000000 --- a/crates/workspace2/src/persistence/model.rs +++ /dev/null @@ -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>); - -impl WorkspaceLocation { - pub fn paths(&self) -> Arc> { - self.0.clone() - } -} - -impl, T: IntoIterator> From for WorkspaceLocation { - fn from(iterator: T) -> Self { - let mut roots = iterator - .into_iter() - .map(|p| p.as_ref().to_path_buf()) - .collect::>(); - roots.sort(); - Self(Arc::new(roots)) - } -} - -impl StaticColumnCount for WorkspaceLocation {} -impl Bind for &WorkspaceLocation { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - 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, - pub display: Option, - 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 { - 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, - pub(crate) zoom: bool, -} - -impl Column for DockData { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let (visible, next_index) = Option::::column(statement, start_index)?; - let (active_panel, next_index) = Option::::column(statement, next_index)?; - let (zoom, next_index) = Option::::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 { - 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>, - children: Vec, - }, - 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, - workspace_id: WorkspaceId, - workspace: WeakView, - cx: &mut AsyncWindowContext, - ) -> Option<(Member, Option>, Vec>>)> { - 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, -} - -impl SerializedPane { - pub fn new(children: Vec, active: bool) -> Self { - SerializedPane { children, active } - } - - pub async fn deserialize_to( - &self, - project: &Model, - pane: &WeakView, - workspace_id: WorkspaceId, - workspace: WeakView, - cx: &mut AsyncWindowContext, - ) -> Result>>> { - 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::().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, - pub item_id: ItemId, - pub active: bool, -} - -impl SerializedItem { - pub fn new(kind: impl AsRef, 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 { - 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::::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, - )) - } -} diff --git a/crates/workspace2/src/searchable.rs b/crates/workspace2/src/searchable.rs deleted file mode 100644 index 59202cbbaf..0000000000 --- a/crates/workspace2/src/searchable.rs +++ /dev/null @@ -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 { - 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); - fn update_matches(&mut self, matches: Vec, cx: &mut ViewContext); - fn query_suggestion(&mut self, cx: &mut ViewContext) -> String; - fn activate_match( - &mut self, - index: usize, - matches: Vec, - cx: &mut ViewContext, - ); - fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext); - fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext); - fn match_index_for_direction( - &mut self, - matches: &Vec, - current_index: usize, - direction: Direction, - count: usize, - _: &mut ViewContext, - ) -> 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, - cx: &mut ViewContext, - ) -> Task>; - fn active_match_index( - &mut self, - matches: Vec, - cx: &mut ViewContext, - ) -> Option; -} - -pub trait SearchableItemHandle: ItemHandle { - fn downgrade(&self) -> Box; - fn boxed_clone(&self) -> Box; - fn supported_options(&self) -> SearchOptions; - fn subscribe_to_search_events( - &self, - cx: &mut WindowContext, - handler: Box, - ) -> Subscription; - fn clear_matches(&self, cx: &mut WindowContext); - fn update_matches(&self, matches: &Vec>, cx: &mut WindowContext); - fn query_suggestion(&self, cx: &mut WindowContext) -> String; - fn activate_match( - &self, - index: usize, - matches: &Vec>, - cx: &mut WindowContext, - ); - fn select_matches(&self, matches: &Vec>, cx: &mut WindowContext); - fn replace(&self, _: &Box, _: &SearchQuery, _: &mut WindowContext); - fn match_index_for_direction( - &self, - matches: &Vec>, - current_index: usize, - direction: Direction, - count: usize, - cx: &mut WindowContext, - ) -> usize; - fn find_matches( - &self, - query: Arc, - cx: &mut WindowContext, - ) -> Task>>; - fn active_match_index( - &self, - matches: &Vec>, - cx: &mut WindowContext, - ) -> Option; -} - -// todo!("here is where we need to use AnyWeakView"); -impl SearchableItemHandle for View { - fn downgrade(&self) -> Box { - Box::new(self.downgrade()) - } - - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } - - fn supported_options(&self) -> SearchOptions { - T::supported_options() - } - - fn subscribe_to_search_events( - &self, - cx: &mut WindowContext, - handler: Box, - ) -> 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>, 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>, - 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>, 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>, - 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, - cx: &mut WindowContext, - ) -> Task>> { - let matches = self.update(cx, |this, cx| this.find_matches(query, cx)); - cx.spawn(|_| async { - let matches = matches.await; - matches - .into_iter() - .map::, _>(|range| Box::new(range)) - .collect() - }) - } - fn active_match_index( - &self, - matches: &Vec>, - cx: &mut WindowContext, - ) -> Option { - let matches = downcast_matches(matches); - self.update(cx, |this, cx| this.active_match_index(matches, cx)) - } - - fn replace(&self, matches: &Box, query: &SearchQuery, cx: &mut WindowContext) { - let matches = matches.downcast_ref().unwrap(); - self.update(cx, |this, cx| this.replace(matches, query, cx)) - } -} - -fn downcast_matches(matches: &Vec>) -> Vec { - matches - .iter() - .map(|range| range.downcast_ref::().cloned()) - .collect::>>() - .expect( - "SearchableItemHandle function called with vec of matches of a different type than expected", - ) -} - -impl From> for AnyView { - fn from(this: Box) -> Self { - this.to_any().clone() - } -} - -impl From<&Box> for AnyView { - fn from(this: &Box) -> Self { - this.to_any().clone() - } -} - -impl PartialEq for Box { - fn eq(&self, other: &Self) -> bool { - self.item_id() == other.item_id() - } -} - -impl Eq for Box {} - -pub trait WeakSearchableItemHandle: WeakItemHandle { - fn upgrade(&self, cx: &AppContext) -> Option>; - - // fn into_any(self) -> AnyWeakView; -} - -impl WeakSearchableItemHandle for WeakView { - fn upgrade(&self, _cx: &AppContext) -> Option> { - Some(Box::new(self.upgrade()?)) - } - - // fn into_any(self) -> AnyView { - // self.into_any() - // } -} - -impl PartialEq for Box { - fn eq(&self, other: &Self) -> bool { - self.id() == other.id() - } -} - -impl Eq for Box {} - -impl std::hash::Hash for Box { - fn hash(&self, state: &mut H) { - self.id().hash(state) - } -} diff --git a/crates/workspace2/src/shared_screen.rs b/crates/workspace2/src/shared_screen.rs deleted file mode 100644 index dbbe7de6a1..0000000000 --- a/crates/workspace2/src/shared_screen.rs +++ /dev/null @@ -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, - frame: Option, - pub peer_id: PeerId, - user: Arc, - nav_history: Option, - _maintain_frame: Task>, - focus: FocusHandle, -} - -impl SharedScreen { - pub fn new( - track: &Arc, - peer_id: PeerId, - user: Arc, - cx: &mut ViewContext, - ) -> 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 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) -> 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 { - Some(format!("{}'s screen", self.user.github_login).into()) - } - - fn deactivated(&mut self, cx: &mut ViewContext) { - if let Some(nav_history) = self.nav_history.as_mut() { - nav_history.push::<()>(None, cx); - } - } - - fn tab_content( - &self, - _: Option, - 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.nav_history = Some(history); - } - - fn clone_on_split( - &self, - _workspace_id: WorkspaceId, - cx: &mut ViewContext, - ) -> Option> { - 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), - } - } -} diff --git a/crates/workspace2/src/status_bar.rs b/crates/workspace2/src/status_bar.rs deleted file mode 100644 index bfa1a8f8ba..0000000000 --- a/crates/workspace2/src/status_bar.rs +++ /dev/null @@ -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, - ); -} - -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>, - right_items: Vec>, - active_pane: View, - _observe_active_pane: Subscription, -} - -impl Render for StatusBar { - fn render(&mut self, cx: &mut ViewContext) -> 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) -> 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) -> 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, cx: &mut ViewContext) -> 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(&mut self, item: View, cx: &mut ViewContext) - 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(&self) -> Option> { - self.left_items - .iter() - .chain(self.right_items.iter()) - .find_map(|item| item.to_any().clone().downcast().log_err()) - } - - pub fn position_of_item(&self) -> Option - where - T: StatusItemView, - { - for (index, item) in self.left_items.iter().enumerate() { - if item.item_type() == TypeId::of::() { - return Some(index); - } - } - for (index, item) in self.right_items.iter().enumerate() { - if item.item_type() == TypeId::of::() { - return Some(index + self.left_items.len()); - } - } - return None; - } - - pub fn insert_item_after( - &mut self, - position: usize, - item: View, - cx: &mut ViewContext, - ) 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) { - 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(&mut self, item: View, cx: &mut ViewContext) - 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, cx: &mut ViewContext) { - 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) { - 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 StatusItemViewHandle for View { - 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::() - } -} - -impl From<&dyn StatusItemViewHandle> for AnyView { - fn from(val: &dyn StatusItemViewHandle) -> Self { - val.to_any().clone() - } -} diff --git a/crates/workspace2/src/toolbar.rs b/crates/workspace2/src/toolbar.rs deleted file mode 100644 index dc17cd3c19..0000000000 --- a/crates/workspace2/src/toolbar.rs +++ /dev/null @@ -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 { - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn crate::ItemHandle>, - cx: &mut ViewContext, - ) -> ToolbarItemLocation; - - fn pane_focus_update(&mut self, _pane_focused: bool, _cx: &mut ViewContext) {} - - /// 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>, - hidden: bool, - can_navigate: bool, - items: Vec<(Box, 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 { - self.items.iter().filter_map(|(item, location)| { - if *location == ToolbarItemLocation::PrimaryLeft { - Some(item.as_ref()) - } else { - None - } - }) - } - - fn right_items(&self) -> impl Iterator { - self.items.iter().filter_map(|(item, location)| { - if *location == ToolbarItemLocation::PrimaryRight { - Some(item.as_ref()) - } else { - None - } - }) - } - - fn secondary_items(&self) -> impl Iterator { - 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) -> 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) -> AnyElement { -// 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.can_navigate = can_navigate; - cx.notify(); - } - - pub fn add_item(&mut self, item: View, cx: &mut ViewContext) - 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.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) { - for (toolbar_item, _) in self.items.iter_mut() { - toolbar_item.focus_changed(focused, cx); - } - } - - pub fn item_of_type(&self) -> Option> { - self.items - .iter() - .find_map(|(item, _)| item.to_any().downcast().ok()) - } - - pub fn hidden(&self) -> bool { - self.hidden - } -} - -impl ToolbarItemViewHandle for View { - 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() -// } -// } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs deleted file mode 100644 index 76715f69be..0000000000 --- a/crates/workspace2/src/workspace2.rs +++ /dev/null @@ -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> = env::var("ZED_WINDOW_SIZE") - .ok() - .as_deref() - .and_then(parse_pixel_size_env_var); - static ref ZED_WINDOW_POSITION: Option> = 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, -} - -#[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, -} - -#[derive(Clone, PartialEq, Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Save { - pub save_intent: Option, -} - -#[derive(Clone, PartialEq, Debug, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -pub struct CloseAllItemsAndPanes { - pub save_intent: Option, -} - -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)>, -} - -impl Toast { - pub fn new>>(id: usize, msg: I) -> Self { - Toast { - id, - msg: msg.into(), - on_click: None, - } - } - - pub fn on_click(mut self, message: M, on_click: F) -> Self - where - M: Into>, - 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, 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, AnyModel, &mut ViewContext) -> Box>; -pub fn register_project_item(cx: &mut AppContext) { - let builders = cx.default_global::(); - builders.insert(TypeId::of::(), |project, model, cx| { - let item = model.downcast::().unwrap(); - Box::new(cx.new_view(|cx| I::for_project_item(project, item, cx))) - }); -} - -type FollowableItemBuilder = fn( - View, - View, - ViewId, - &mut Option, - &mut WindowContext, -) -> Option>>>; -type FollowableItemBuilders = HashMap< - TypeId, - ( - FollowableItemBuilder, - fn(&AnyView) -> Box, - ), ->; -pub fn register_followable_item(cx: &mut AppContext) { - let builders = cx.default_global::(); - builders.insert( - TypeId::of::(), - ( - |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::().unwrap()), - ), - ); -} - -type ItemDeserializers = HashMap< - Arc, - fn( - Model, - WeakView, - WorkspaceId, - ItemId, - &mut ViewContext, - ) -> Task>>, ->; -pub fn register_deserializable_item(cx: &mut AppContext) { - if let Some(serialized_item_kind) = I::serialized_item_kind() { - let deserializers = cx.default_global::(); - 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, - pub client: Arc, - pub user_store: Model, - pub workspace_store: Model, - pub fs: Arc, - pub build_window_options: - fn(Option, Option, &mut AppContext) -> WindowOptions, - pub node_runtime: Arc, -} - -pub struct WorkspaceStore { - workspaces: HashSet>, - followers: Vec, - client: Arc, - _subscriptions: Vec, -} - -#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)] -struct Follower { - project_id: Option, - peer_id: PeerId, -} - -impl AppState { - #[cfg(any(test, feature = "test-support"))] - pub fn test(cx: &mut AppContext) -> Arc { - use node_runtime::FakeNodeRuntime; - use settings::SettingsStore; - - if !cx.has_global::() { - 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>, - cancel_channel: Option>, -} - -impl DelayedDebouncedEditAction { - fn new() -> DelayedDebouncedEditAction { - DelayedDebouncedEditAction { - task: None, - cancel_channel: None, - } - } - - fn fire_new(&mut self, delay: Duration, cx: &mut ViewContext, func: F) - where - F: 'static + Send + FnOnce(&mut Workspace, &mut ViewContext) -> Task>, - { - 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), - ContactRequestedJoin(u64), - WorkspaceCreated(WeakView), -} - -pub struct Workspace { - weak_self: WeakView, - workspace_actions: Vec) -> Div>>, - zoomed: Option, - zoomed_position: Option, - center: PaneGroup, - left_dock: View, - bottom_dock: View, - right_dock: View, - panes: Vec>, - panes_by_item: HashMap>, - active_pane: View, - last_active_center_pane: Option>, - last_active_view_id: Option, - status_bar: View, - modal_layer: View, - titlebar_item: Option, - notifications: Vec<(TypeId, usize, Box)>, - project: Model, - follower_states: HashMap, FollowerState>, - last_leaders_by_pane: HashMap, PeerId>, - window_edited: bool, - active_call: Option<(Model, Vec)>, - leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, - database_id: WorkspaceId, - app_state: Arc, - _subscriptions: Vec, - _apply_leader_updates: Task>, - _observe_current_user: Task>, - _schedule_serialize: Option>, - pane_history_timestamp: Arc, - bounds: Bounds, -} - -impl EventEmitter 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, - items_by_leader_view_id: HashMap>, -} - -impl Workspace { - pub fn new( - workspace_id: WorkspaceId, - project: Model, - app_state: Arc, - cx: &mut ViewContext, - ) -> 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::().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::>() { - let call = cx.global::>().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::().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, - app_state: Arc, - requesting_window: Option>, - cx: &mut AppContext, - ) -> Task< - anyhow::Result<( - WindowHandle, - Vec, 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 = - 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> = Default::default(); - let mut project_paths: Vec<(PathBuf, Option)> = - 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.weak_self.clone() - } - - pub fn left_dock(&self) -> &View { - &self.left_dock - } - - pub fn bottom_dock(&self) -> &View { - &self.bottom_dock - } - - pub fn right_dock(&self) -> &View { - &self.right_dock - } - - pub fn add_panel(&mut self, panel: View, cx: &mut ViewContext) { - 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 { - &self.status_bar - } - - pub fn app_state(&self) -> &Arc { - &self.app_state - } - - pub fn user_store(&self) -> &Model { - &self.app_state.user_store - } - - pub fn project(&self) -> &Model { - &self.project - } - - pub fn recent_navigation_history( - &self, - limit: Option, - cx: &AppContext, - ) -> Vec<(ProjectPath, Option)> { - let mut abs_paths_opened: HashMap> = HashMap::default(); - let mut history: HashMap, 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, - mode: NavigationMode, - cx: &mut ViewContext, - ) -> Task> { - 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, - cx: &mut ViewContext, - ) -> Task> { - self.navigate_history(pane, NavigationMode::GoingBack, cx) - } - - pub fn go_forward( - &mut self, - pane: WeakView, - cx: &mut ViewContext, - ) -> Task> { - self.navigate_history(pane, NavigationMode::GoingForward, cx) - } - - pub fn reopen_closed_item(&mut self, cx: &mut ViewContext) -> Task> { - 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.titlebar_item = Some(item); - cx.notify(); - } - - pub fn titlebar_item(&self) -> Option { - 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( - &mut self, - cx: &mut ViewContext, - callback: F, - ) -> Task> - where - T: 'static, - F: 'static + FnOnce(&mut Workspace, &mut ViewContext) -> 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> { - self.project.read(cx).worktrees() - } - - pub fn visible_worktrees<'a>( - &self, - cx: &'a AppContext, - ) -> impl 'a + Iterator> { - self.project.read(cx).visible_worktrees(cx) - } - - pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future + 'static { - let futures = self - .worktrees(cx) - .filter_map(|worktree| worktree.read(cx).as_local()) - .map(|worktree| worktree.scan_complete()) - .collect::>(); - 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) { - 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, - ) -> Task> { - //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::().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.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, - ) -> Task> { - 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::>(); - - 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) { - 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, - cx: &mut ViewContext, - ) -> Task> { - let window = cx.window_handle().downcast::(); - 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, - visible: bool, - cx: &mut ViewContext, - ) -> Task, 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) { - 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, - abs_path: &Path, - visible: bool, - cx: &mut AppContext, - ) -> Task, 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> { - self.panes.iter().flat_map(|pane| pane.read(cx).items()) - } - - pub fn item_of_type(&self, cx: &AppContext) -> Option> { - 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> { - self.panes - .iter() - .flat_map(|pane| pane.read(cx).items_of_type()) - } - - pub fn active_item(&self, cx: &AppContext) -> Option> { - self.active_pane().read(cx).active_item() - } - - pub fn active_item_as(&self, cx: &AppContext) -> Option> { - let item = self.active_item(cx)?; - item.to_any().downcast::().ok() - } - - fn active_project_path(&self, cx: &ViewContext) -> Option { - self.active_item(cx).and_then(|item| item.project_path(cx)) - } - - pub fn save_active_item( - &mut self, - save_intent: SaveIntent, - cx: &mut ViewContext, - ) -> Task> { - 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.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.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, - ) -> Option>> { - 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) { - 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) { - 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(&mut self, cx: &mut ViewContext) -> Option> { - let panel = self.focus_or_unfocus_panel::(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(&mut self, cx: &mut ViewContext) { - self.focus_or_unfocus_panel::(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( - &mut self, - cx: &mut ViewContext, - should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext) -> bool, - ) -> Option> { - for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { - if let Some(panel_index) = dock.read(cx).panel_index_for_type::() { - 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(&self, cx: &WindowContext) -> Option> { - for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { - let dock = dock.read(cx); - if let Some(panel) = dock.panel::() { - return Some(panel); - } - } - None - } - - // todo!("implement zoom") - #[allow(unused)] - fn zoom_out(&mut self, cx: &mut ViewContext) { - 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 { - // self.zoomed.and_then(|view| view.upgrade(cx)) - // } - - fn dismiss_zoomed_items_to_reveal( - &mut self, - dock_to_reveal: Option, - cx: &mut ViewContext, - ) { - // 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) -> View { - 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, - cx: &mut ViewContext, - ) -> 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, cx: &mut ViewContext) { - 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, - cx: &mut ViewContext, - ) { - 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, - ) -> Task>> { - 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, - ) -> Task>> { - 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, - pane: Option>, - focus_item: bool, - cx: &mut ViewContext, - ) -> Task, 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, - cx: &mut ViewContext, - ) -> Task, 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, - ) -> Task< - Result<( - Option, - impl 'static + Send + FnOnce(&mut ViewContext) -> Box, - )>, - > { - 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::() - .get(&project_item.entity_type()) - .ok_or_else(|| anyhow!("no item builder for project item")) - .cloned() - })??; - let build_item = - move |cx: &mut ViewContext| build_item(project, project_item, cx); - Ok((project_entry_id, build_item)) - }) - } - - pub fn open_project_item( - &mut self, - project_item: Model, - cx: &mut ViewContext, - ) -> View - 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( - &mut self, - project_item: Model, - cx: &mut ViewContext, - ) -> View - 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) { - 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) -> 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) { - 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) { - 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) { - 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, - ) { - 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, - ) { - 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, - ) -> Option<&View> { - 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, cx: &mut ViewContext) { - 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, - event: &pane::Event, - cx: &mut ViewContext, - ) { - 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, - split_direction: SplitDirection, - cx: &mut ViewContext, - ) -> View { - 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, - direction: SplitDirection, - cx: &mut ViewContext, - ) -> Option> { - 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, - split_direction: SplitDirection, - from: WeakView, - item_id_to_move: EntityId, - cx: &mut ViewContext, - ) { - 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, - split_direction: SplitDirection, - project_entry: ProjectEntryId, - cx: &mut ViewContext, - ) -> Option>> { - 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, - destination: View, - item_id_to_move: EntityId, - destination_index: usize, - cx: &mut ViewContext, - ) { - 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, cx: &mut ViewContext) { - 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] { - &self.panes - } - - pub fn active_pane(&self) -> &View { - &self.active_pane - } - - pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option> { - 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.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, - ) -> Option>> { - 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, - ) { - 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) { - 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, cx: &mut ViewContext) -> Option { - 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) { - 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) { - 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) { - 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) -> Option
{ - 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, - cx: &mut ViewContext, - ) -> 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.leader_updates_tx - .unbounded_send((leader_id, message)) - .ok(); - } - - async fn process_leader_update( - this: &WeakView, - 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, - leader_id: PeerId, - panes: Vec>, - views: Vec, - cx: &mut AsyncWindowContext, - ) -> Result<()> { - let this = this.upgrade().context("workspace dropped")?; - - let item_builders = cx.update(|_, cx| { - cx.default_global::() - .values() - .map(|b| b.0) - .collect::>() - })?; - - 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) { - 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) -> Option { - self.follower_states.get(pane).map(|state| state.leader_id) - } - - fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> 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, - cx: &mut ViewContext, - ) -> Option> { - 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::() { - 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) { - 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> { - self.active_call.as_ref().map(|(call, _)| call) - } - - fn on_active_call_event( - &mut self, - _: Model, - event: &call::room::Event, - cx: &mut ViewContext, - ) { - 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 { - let project = self.project().read(cx); - - if project.is_local() { - Some( - project - .visible_worktrees(cx) - .map(|worktree| worktree.read(cx).abs_path()) - .collect::>() - .into(), - ) - } else { - None - } - } - - fn remove_panes(&mut self, member: Member, cx: &mut ViewContext) { - 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, cx: &mut ViewContext) { - 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._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) { - fn serialize_pane_handle(pane_handle: &View, 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::>(), - 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::>(), - 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, - ) -> 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>, - cx: &mut ViewContext, - ) -> Task>>>> { - 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::>() - })?; - - 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::>(); - - // 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) -> 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, cx: &mut ViewContext) -> 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> { - // 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( - &mut self, - callback: impl Fn(&mut Self, &A, &mut ViewContext) + '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) -> 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( - &mut self, - cx: &ViewContext, - ) -> Option> { - self.modal_layer.read(cx).active_modal() - } - - pub fn toggle_modal(&mut self, cx: &mut ViewContext, build: B) - where - B: FnOnce(&mut ViewContext) -> V, - { - self.modal_layer - .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build)) - } -} - -fn window_bounds_env_override(cx: &AsyncAppContext) -> Option { - 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, - mut project_paths_to_open: Vec<(PathBuf, Option)>, - app_state: Arc, - cx: &mut ViewContext, -) -> impl 'static + Future>>>>> { - 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::>(); - - 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::>(); - - 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, 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) -> 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, 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, cx: &mut ModelContext) -> 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, - update: proto::update_followers::Variant, - cx: &AppContext, - ) -> Option<()> { - if !cx.has_global::>() { - 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, - envelope: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result { - 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, - envelope: TypedEnvelope, - _: Arc, - 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, - envelope: TypedEnvelope, - _: Arc, - 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 { - 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; -} - -impl WorkspaceHandle for View { - fn file_project_paths(&self, cx: &AppContext) -> Vec { - 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::>() - } -} - -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> { - for window in cx.windows() { - let Some(workspace) = window.downcast::() 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 { - DB.last_workspace().await.log_err().flatten() -} - -async fn join_channel_internal( - channel_id: u64, - app_state: &Arc, - requesting_window: Option>, - active_call: &Model, - cx: &mut AsyncAppContext, -) -> Result { - 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, - requesting_window: Option>, - cx: &mut AppContext, -) -> Task> { - 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, - mut cx: AsyncAppContext, -) -> anyhow::Result> { - // 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::() - .context("could not open zed workspace window") -} - -fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option { - cx.update(|cx| { - for window in cx.windows() { - let is_workspace = window.downcast::().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, - requesting_window: Option>, - cx: &mut AppContext, -) -> Task< - anyhow::Result<( - WindowHandle, - Vec, 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, - cx: &mut AppContext, - init: impl FnOnce(&mut Workspace, &mut ViewContext) + '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, - default_content: impl 'static + Send + FnOnce() -> Rope, -) -> Task>> { - 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, - cx: &mut AppContext, -) -> Task> { - let windows = cx.windows(); - cx.spawn(|mut cx| async move { - let existing_workspace = windows.into_iter().find_map(|window| { - window.downcast::().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::()) - .collect::>(); - - // 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> { - 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> { - 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, - 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, 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 { - 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::>(); - 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::(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::(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::(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::(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::(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::(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::(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::(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); - }); - } -} diff --git a/crates/workspace2/src/workspace_settings.rs b/crates/workspace2/src/workspace_settings.rs deleted file mode 100644 index f3882a9dbd..0000000000 --- a/crates/workspace2/src/workspace_settings.rs +++ /dev/null @@ -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, - pub confirm_quit: Option, - pub show_call_status_icon: Option, - pub autosave: Option, -} - -#[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, - pub gutter_debounce: Option, -} - -#[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::load_via_json_merge(default_value, user_values) - } -} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index ad5b14de28..596141bf65 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -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