Merge branch 'main' into unborked-git-zed2-diagnostics-view

Julia created

Change summary

Cargo.lock                                                         |  109 
Cargo.toml                                                         |    6 
assets/settings/default.json                                       |    9 
crates/collab2/src/tests/test_server.rs                            |    3 
crates/collab_ui2/Cargo.toml                                       |   81 
crates/collab_ui2/src/channel_view.rs                              |  454 
crates/collab_ui2/src/chat_panel.rs                                |  983 
crates/collab_ui2/src/chat_panel/message_editor.rs                 |  313 
crates/collab_ui2/src/collab_panel.rs                              | 3575 
crates/collab_ui2/src/collab_panel/channel_modal.rs                |  717 
crates/collab_ui2/src/collab_panel/contact_finder.rs               |  261 
crates/collab_ui2/src/collab_titlebar_item.rs                      | 1371 
crates/collab_ui2/src/collab_ui.rs                                 |  154 
crates/collab_ui2/src/face_pile.rs                                 |  113 
crates/collab_ui2/src/notification_panel.rs                        |  884 
crates/collab_ui2/src/notifications.rs                             |   11 
crates/collab_ui2/src/notifications/incoming_call_notification.rs  |  213 
crates/collab_ui2/src/notifications/project_shared_notification.rs |  217 
crates/collab_ui2/src/panel_settings.rs                            |   69 
crates/command_palette2/src/command_palette.rs                     |   20 
crates/copilot/src/copilot.rs                                      |   20 
crates/diagnostics2/src/diagnostics.rs                             |   12 
crates/diagnostics2/src/toolbar_controls.rs                        |    6 
crates/editor2/src/display_map.rs                                  |   29 
crates/editor2/src/display_map/inlay_map.rs                        |    2 
crates/editor2/src/editor.rs                                       |   96 
crates/editor2/src/editor_tests.rs                                 |    2 
crates/editor2/src/element.rs                                      |  157 
crates/editor2/src/inlay_hint_cache.rs                             |    2 
crates/editor2/src/items.rs                                        |    8 
crates/editor2/src/movement.rs                                     |   10 
crates/editor2/src/scroll.rs                                       |    2 
crates/editor2/src/selections_collection.rs                        |    8 
crates/file_finder2/src/file_finder.rs                             |   23 
crates/go_to_line2/src/go_to_line.rs                               |   19 
crates/gpui2/Cargo.toml                                            |    1 
crates/gpui2/src/action.rs                                         |  247 
crates/gpui2/src/app.rs                                            |   27 
crates/gpui2/src/app/async_context.rs                              |   19 
crates/gpui2/src/app/entity_map.rs                                 |    1 
crates/gpui2/src/app/test_context.rs                               |   25 
crates/gpui2/src/element.rs                                        |   88 
crates/gpui2/src/elements/div.rs                                   |  140 
crates/gpui2/src/elements/img.rs                                   |   13 
crates/gpui2/src/elements/mod.rs                                   |    2 
crates/gpui2/src/elements/overlay.rs                               |  232 
crates/gpui2/src/elements/svg.rs                                   |   13 
crates/gpui2/src/elements/text.rs                                  |  207 
crates/gpui2/src/elements/uniform_list.rs                          |   86 
crates/gpui2/src/executor.rs                                       |   66 
crates/gpui2/src/geometry.rs                                       |   69 
crates/gpui2/src/gpui2.rs                                          |    6 
crates/gpui2/src/key_dispatch.rs                                   |   13 
crates/gpui2/src/keymap/keymap.rs                                  |   30 
crates/gpui2/src/platform.rs                                       |    6 
crates/gpui2/src/platform/mac/dispatcher.rs                        |    6 
crates/gpui2/src/platform/mac/text_system.rs                       |   10 
crates/gpui2/src/platform/mac/window.rs                            |   36 
crates/gpui2/src/platform/test/dispatcher.rs                       |   83 
crates/gpui2/src/style.rs                                          |    1 
crates/gpui2/src/text_system.rs                                    |   82 
crates/gpui2/src/text_system/line.rs                               |  265 
crates/gpui2/src/text_system/line_layout.rs                        |  136 
crates/gpui2/src/view.rs                                           |   97 
crates/gpui2/src/window.rs                                         |   95 
crates/gpui2/tests/action_macros.rs                                |   45 
crates/gpui2_macros/Cargo.toml                                     |    2 
crates/gpui2_macros/src/action.rs                                  |  103 
crates/gpui2_macros/src/gpui2_macros.rs                            |    8 
crates/gpui2_macros/src/register_action.rs                         |   78 
crates/language/src/buffer.rs                                      |   90 
crates/language2/src/buffer.rs                                     |  215 
crates/live_kit_client2/Cargo.toml                                 |    2 
crates/live_kit_client2/examples/test_app2.rs                      |    4 
crates/picker2/src/picker2.rs                                      |   10 
crates/project/src/project.rs                                      |    4 
crates/project/src/worktree.rs                                     |    2 
crates/project2/src/project2.rs                                    |    4 
crates/project2/src/project_tests.rs                               |  121 
crates/project2/src/worktree.rs                                    |    3 
crates/project_panel2/src/project_panel.rs                         |   95 
crates/rope/src/rope.rs                                            |    6 
crates/rope2/src/rope2.rs                                          |    6 
crates/settings2/src/keymap_file.rs                                |   10 
crates/settings2/src/settings_file.rs                              |    3 
crates/sqlez/src/bindable.rs                                       |   17 
crates/storybook2/src/stories/focus.rs                             |    1 
crates/storybook2/src/stories/scroll.rs                            |   13 
crates/storybook2/src/storybook2.rs                                |    4 
crates/storybook3/Cargo.toml                                       |   17 
crates/storybook3/src/storybook3.rs                                |   73 
crates/terminal2/src/terminal_settings.rs                          |   12 
crates/terminal_view2/Cargo.toml                                   |   45 
crates/terminal_view2/README.md                                    |   23 
crates/terminal_view2/scripts/print256color.sh                     |   96 
crates/terminal_view2/scripts/truecolor.sh                         |   19 
crates/terminal_view2/src/persistence.rs                           |   71 
crates/terminal_view2/src/terminal_element.rs                      |  954 
crates/terminal_view2/src/terminal_panel.rs                        |  446 
crates/terminal_view2/src/terminal_view.rs                         | 1204 
crates/theme2/src/registry.rs                                      |    8 
crates/theme2/src/settings.rs                                      |   19 
crates/theme2/src/theme2.rs                                        |   19 
crates/ui2/Cargo.toml                                              |    1 
crates/ui2/src/components/button.rs                                |    3 
crates/ui2/src/components/context_menu.rs                          |  368 
crates/ui2/src/components/icon.rs                                  |    2 
crates/ui2/src/components/icon_button.rs                           |   44 
crates/ui2/src/components/keybinding.rs                            |    5 
crates/ui2/src/components/label.rs                                 |    2 
crates/ui2/src/components/list.rs                                  |   52 
crates/ui2/src/components/tooltip.rs                               |   91 
crates/ui2/src/story.rs                                            |    1 
crates/ui2/src/styled_ext.rs                                       |    1 
crates/workspace2/src/dock.rs                                      |  215 
crates/workspace2/src/item.rs                                      |   24 
crates/workspace2/src/modal_layer.rs                               |   22 
crates/workspace2/src/pane.rs                                      |  462 
crates/workspace2/src/pane_group.rs                                |   38 
crates/workspace2/src/persistence/model.rs                         |    2 
crates/workspace2/src/searchable.rs                                |    2 
crates/workspace2/src/status_bar.rs                                |  119 
crates/workspace2/src/workspace2.rs                                |  746 
crates/zed2/Cargo.toml                                             |    7 
crates/zed2/src/languages/json.rs                                  |    2 
crates/zed2/src/main.rs                                            |   14 
crates/zed2/src/zed2.rs                                            |  298 
crates/zed_actions2/src/lib.rs                                     |    7 
128 files changed, 16,047 insertions(+), 2,394 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1829,6 +1829,47 @@ dependencies = [
  "zed-actions",
 ]
 
+[[package]]
+name = "collab_ui2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "call2",
+ "channel2",
+ "client2",
+ "clock",
+ "collections",
+ "db2",
+ "editor2",
+ "feature_flags2",
+ "futures 0.3.28",
+ "fuzzy",
+ "gpui2",
+ "language2",
+ "lazy_static",
+ "log",
+ "menu2",
+ "notifications2",
+ "picker2",
+ "postage",
+ "pretty_assertions",
+ "project2",
+ "rich_text2",
+ "rpc2",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "settings2",
+ "smallvec",
+ "theme2",
+ "time",
+ "tree-sitter-markdown",
+ "ui2",
+ "util",
+ "workspace2",
+ "zed_actions2",
+]
+
 [[package]]
 name = "collections"
 version = "0.1.0"
@@ -3784,6 +3825,7 @@ dependencies = [
  "image",
  "itertools 0.10.5",
  "lazy_static",
+ "linkme",
  "log",
  "media",
  "metal",
@@ -4802,6 +4844,26 @@ version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
 
+[[package]]
+name = "linkme"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ed2ee9464ff9707af8e9ad834cffa4802f072caad90639c583dd3c62e6e608"
+dependencies = [
+ "linkme-impl",
+]
+
+[[package]]
+name = "linkme-impl"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba125974b109d512fccbc6c0244e7580143e460895dfd6ea7f8bbb692fd94396"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.37",
+]
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.0.42"
@@ -8789,6 +8851,17 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "storybook3"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "gpui2",
+ "settings2",
+ "theme2",
+ "ui2",
+]
+
 [[package]]
 name = "stringprep"
 version = "0.1.4"
@@ -9162,6 +9235,39 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "terminal_view2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "client2",
+ "db2",
+ "dirs 4.0.0",
+ "editor2",
+ "futures 0.3.28",
+ "gpui2",
+ "itertools 0.10.5",
+ "language2",
+ "lazy_static",
+ "libc",
+ "mio-extras",
+ "ordered-float 2.10.0",
+ "procinfo",
+ "project2",
+ "rand 0.8.5",
+ "serde",
+ "serde_derive",
+ "settings2",
+ "shellexpand",
+ "smallvec",
+ "smol",
+ "terminal2",
+ "theme2",
+ "thiserror",
+ "util",
+ "workspace2",
+]
+
 [[package]]
 name = "text"
 version = "0.1.0"
@@ -10086,6 +10192,7 @@ dependencies = [
  "chrono",
  "gpui2",
  "itertools 0.11.0",
+ "menu2",
  "rand 0.8.5",
  "serde",
  "settings2",
@@ -11469,6 +11576,7 @@ dependencies = [
  "chrono",
  "cli",
  "client2",
+ "collab_ui2",
  "collections",
  "command_palette2",
  "copilot2",
@@ -11521,6 +11629,7 @@ dependencies = [
  "smol",
  "sum_tree",
  "tempdir",
+ "terminal_view2",
  "text2",
  "theme2",
  "thiserror",

Cargo.toml 🔗

@@ -18,6 +18,7 @@ members = [
     "crates/collab",
     "crates/collab2",
     "crates/collab_ui",
+    "crates/collab_ui2",
     "crates/collections",
     "crates/command_palette",
     "crates/command_palette2",
@@ -95,9 +96,11 @@ members = [
     "crates/sqlez_macros",
     "crates/rich_text",
     "crates/storybook2",
+    "crates/storybook3",
     "crates/sum_tree",
     "crates/terminal",
     "crates/terminal2",
+    "crates/terminal_view2",
     "crates/text",
     "crates/theme",
     "crates/theme2",
@@ -204,6 +207,9 @@ core-graphics = { git = "https://github.com/servo/core-foundation-rs", rev = "07
 [profile.dev]
 split-debuginfo = "unpacked"
 
+[profile.dev.package.taffy]
+opt-level = 3
+
 [profile.release]
 debug = true
 lto = "thin"

assets/settings/default.json 🔗

@@ -35,6 +35,15 @@
   //           "custom": 2
   //         },
   "buffer_line_height": "comfortable",
+  // The name of a font to use for rendering text in the UI
+  "ui_font_family": "Zed Mono",
+  // The OpenType features to enable for text in the UI
+  "ui_font_features": {
+    // Disable ligatures:
+    "calt": false
+  },
+  // The default font size for text in the UI
+  "ui_font_size": 14,
   // The factor to grow the active pane by. Defaults to 1.0
   // which gives the same size as all other panes.
   "active_pane_magnification": 1.0,

crates/collab2/src/tests/test_server.rs 🔗

@@ -220,12 +220,11 @@ impl TestServer {
             languages: Arc::new(language_registry),
             fs: fs.clone(),
             build_window_options: |_, _, _| Default::default(),
-            initialize_workspace: |_, _, _, _| gpui::Task::ready(Ok(())),
             node_runtime: FakeNodeRuntime::new(),
         });
 
         cx.update(|cx| {
-            theme::init(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             Project::init(&client, cx);
             client::init(&client, cx);
             language::init(cx);

crates/collab_ui2/Cargo.toml 🔗

@@ -0,0 +1,81 @@
+[package]
+name = "collab_ui2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/collab_ui.rs"
+doctest = false
+
+[features]
+test-support = [
+    "call/test-support",
+    "client/test-support",
+    "collections/test-support",
+    "editor/test-support",
+    "gpui/test-support",
+    "project/test-support",
+    "settings/test-support",
+    "util/test-support",
+    "workspace/test-support",
+]
+
+[dependencies]
+# auto_update = { path = "../auto_update" }
+db = { package = "db2", path = "../db2" }
+call = { package = "call2", path = "../call2" }
+client = { package = "client2", path = "../client2" }
+channel = { package = "channel2", path = "../channel2" }
+clock = { path = "../clock" }
+collections = { path = "../collections" }
+# context_menu = { path = "../context_menu" }
+# drag_and_drop = { path = "../drag_and_drop" }
+editor = { package="editor2", path = "../editor2" }
+#feedback = { path = "../feedback" }
+fuzzy = { path = "../fuzzy" }
+gpui = { package = "gpui2", path = "../gpui2" }
+language = { package = "language2", path = "../language2" }
+menu = { package = "menu2",  path = "../menu2" }
+notifications = { package = "notifications2",  path = "../notifications2" }
+rich_text = { package = "rich_text2", path = "../rich_text2" }
+picker = { package = "picker2", path = "../picker2" }
+project = { package = "project2", path = "../project2" }
+# recent_projects = { path = "../recent_projects" }
+rpc = { package ="rpc2",  path = "../rpc2" }
+settings = { package = "settings2", path = "../settings2" }
+feature_flags = { package = "feature_flags2", path = "../feature_flags2"}
+theme = { package = "theme2", path = "../theme2" }
+# theme_selector = { path = "../theme_selector" }
+# vcs_menu = { path = "../vcs_menu" }
+ui = { package = "ui2", path = "../ui2" }
+util = { path = "../util" }
+workspace = { package = "workspace2", path = "../workspace2" }
+zed-actions = { package="zed_actions2", path = "../zed_actions2"}
+
+anyhow.workspace = true
+futures.workspace = true
+lazy_static.workspace = true
+log.workspace = true
+schemars.workspace = true
+postage.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+time.workspace = true
+smallvec.workspace = true
+
+[dev-dependencies]
+call = { package = "call2", path = "../call2", features = ["test-support"] }
+client = { package = "client2", path = "../client2", features = ["test-support"] }
+collections = { path = "../collections", features = ["test-support"] }
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
+gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
+notifications = { package = "notifications2", path = "../notifications2", features = ["test-support"] }
+project = { package = "project2", path = "../project2", features = ["test-support"] }
+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"] }
+
+pretty_assertions.workspace = true
+tree-sitter-markdown.workspace = true

crates/collab_ui2/src/channel_view.rs 🔗

@@ -0,0 +1,454 @@
+// use anyhow::{anyhow, Result};
+// use call::report_call_event_for_channel;
+// use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
+// use client::{
+//     proto::{self, PeerId},
+//     Collaborator, ParticipantIndex,
+// };
+// use collections::HashMap;
+// use editor::{CollaborationHub, Editor};
+// use gpui::{
+//     actions,
+//     elements::{ChildView, Label},
+//     geometry::vector::Vector2F,
+//     AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View,
+//     ViewContext, ViewHandle,
+// };
+// use project::Project;
+// use smallvec::SmallVec;
+// use std::{
+//     any::{Any, TypeId},
+//     sync::Arc,
+// };
+// use util::ResultExt;
+// use workspace::{
+//     item::{FollowableItem, Item, ItemEvent, ItemHandle},
+//     register_followable_item,
+//     searchable::SearchableItemHandle,
+//     ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
+// };
+
+// actions!(channel_view, [Deploy]);
+
+// pub fn init(cx: &mut AppContext) {
+//     register_followable_item::<ChannelView>(cx)
+// }
+
+// pub struct ChannelView {
+//     pub editor: ViewHandle<Editor>,
+//     project: ModelHandle<Project>,
+//     channel_store: ModelHandle<ChannelStore>,
+//     channel_buffer: ModelHandle<ChannelBuffer>,
+//     remote_id: Option<ViewId>,
+//     _editor_event_subscription: Subscription,
+// }
+
+// impl ChannelView {
+//     pub fn open(
+//         channel_id: ChannelId,
+//         workspace: ViewHandle<Workspace>,
+//         cx: &mut AppContext,
+//     ) -> Task<Result<ViewHandle<Self>>> {
+//         let pane = workspace.read(cx).active_pane().clone();
+//         let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
+//         cx.spawn(|mut cx| async move {
+//             let channel_view = channel_view.await?;
+//             pane.update(&mut cx, |pane, cx| {
+//                 report_call_event_for_channel(
+//                     "open channel notes",
+//                     channel_id,
+//                     &workspace.read(cx).app_state().client,
+//                     cx,
+//                 );
+//                 pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
+//             });
+//             anyhow::Ok(channel_view)
+//         })
+//     }
+
+//     pub fn open_in_pane(
+//         channel_id: ChannelId,
+//         pane: ViewHandle<Pane>,
+//         workspace: ViewHandle<Workspace>,
+//         cx: &mut AppContext,
+//     ) -> Task<Result<ViewHandle<Self>>> {
+//         let workspace = workspace.read(cx);
+//         let project = workspace.project().to_owned();
+//         let channel_store = ChannelStore::global(cx);
+//         let language_registry = workspace.app_state().languages.clone();
+//         let markdown = language_registry.language_for_name("Markdown");
+//         let channel_buffer =
+//             channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
+
+//         cx.spawn(|mut cx| async move {
+//             let channel_buffer = channel_buffer.await?;
+//             let markdown = markdown.await.log_err();
+
+//             channel_buffer.update(&mut cx, |buffer, cx| {
+//                 buffer.buffer().update(cx, |buffer, cx| {
+//                     buffer.set_language_registry(language_registry);
+//                     if let Some(markdown) = markdown {
+//                         buffer.set_language(Some(markdown), cx);
+//                     }
+//                 })
+//             });
+
+//             pane.update(&mut cx, |pane, cx| {
+//                 let buffer_id = channel_buffer.read(cx).remote_id(cx);
+
+//                 let existing_view = pane
+//                     .items_of_type::<Self>()
+//                     .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
+
+//                 // If this channel buffer is already open in this pane, just return it.
+//                 if let Some(existing_view) = existing_view.clone() {
+//                     if existing_view.read(cx).channel_buffer == channel_buffer {
+//                         return existing_view;
+//                     }
+//                 }
+
+//                 let view = cx.add_view(|cx| {
+//                     let mut this = Self::new(project, channel_store, channel_buffer, cx);
+//                     this.acknowledge_buffer_version(cx);
+//                     this
+//                 });
+
+//                 // If the pane contained a disconnected view for this channel buffer,
+//                 // replace that.
+//                 if let Some(existing_item) = existing_view {
+//                     if let Some(ix) = pane.index_for_item(&existing_item) {
+//                         pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx)
+//                             .detach();
+//                         pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
+//                     }
+//                 }
+
+//                 view
+//             })
+//             .ok_or_else(|| anyhow!("pane was dropped"))
+//         })
+//     }
+
+//     pub fn new(
+//         project: ModelHandle<Project>,
+//         channel_store: ModelHandle<ChannelStore>,
+//         channel_buffer: ModelHandle<ChannelBuffer>,
+//         cx: &mut ViewContext<Self>,
+//     ) -> Self {
+//         let buffer = channel_buffer.read(cx).buffer();
+//         let editor = cx.add_view(|cx| {
+//             let mut editor = Editor::for_buffer(buffer, None, cx);
+//             editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
+//                 channel_buffer.clone(),
+//             )));
+//             editor.set_read_only(
+//                 !channel_buffer
+//                     .read(cx)
+//                     .channel(cx)
+//                     .is_some_and(|c| c.can_edit_notes()),
+//             );
+//             editor
+//         });
+//         let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
+
+//         cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
+//             .detach();
+
+//         Self {
+//             editor,
+//             project,
+//             channel_store,
+//             channel_buffer,
+//             remote_id: None,
+//             _editor_event_subscription,
+//         }
+//     }
+
+//     pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
+//         self.channel_buffer.read(cx).channel(cx)
+//     }
+
+//     fn handle_channel_buffer_event(
+//         &mut self,
+//         _: ModelHandle<ChannelBuffer>,
+//         event: &ChannelBufferEvent,
+//         cx: &mut ViewContext<Self>,
+//     ) {
+//         match event {
+//             ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
+//                 editor.set_read_only(true);
+//                 cx.notify();
+//             }),
+//             ChannelBufferEvent::ChannelChanged => {
+//                 self.editor.update(cx, |editor, cx| {
+//                     editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
+//                     cx.emit(editor::Event::TitleChanged);
+//                     cx.notify()
+//                 });
+//             }
+//             ChannelBufferEvent::BufferEdited => {
+//                 if cx.is_self_focused() || self.editor.is_focused(cx) {
+//                     self.acknowledge_buffer_version(cx);
+//                 } else {
+//                     self.channel_store.update(cx, |store, cx| {
+//                         let channel_buffer = self.channel_buffer.read(cx);
+//                         store.notes_changed(
+//                             channel_buffer.channel_id,
+//                             channel_buffer.epoch(),
+//                             &channel_buffer.buffer().read(cx).version(),
+//                             cx,
+//                         )
+//                     });
+//                 }
+//             }
+//             ChannelBufferEvent::CollaboratorsChanged => {}
+//         }
+//     }
+
+//     fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) {
+//         self.channel_store.update(cx, |store, cx| {
+//             let channel_buffer = self.channel_buffer.read(cx);
+//             store.acknowledge_notes_version(
+//                 channel_buffer.channel_id,
+//                 channel_buffer.epoch(),
+//                 &channel_buffer.buffer().read(cx).version(),
+//                 cx,
+//             )
+//         });
+//         self.channel_buffer.update(cx, |buffer, cx| {
+//             buffer.acknowledge_buffer_version(cx);
+//         });
+//     }
+// }
+
+// impl Entity for ChannelView {
+//     type Event = editor::Event;
+// }
+
+// impl View for ChannelView {
+//     fn ui_name() -> &'static str {
+//         "ChannelView"
+//     }
+
+//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+//         ChildView::new(self.editor.as_any(), cx).into_any()
+//     }
+
+//     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+//         if cx.is_self_focused() {
+//             self.acknowledge_buffer_version(cx);
+//             cx.focus(self.editor.as_any())
+//         }
+//     }
+// }
+
+// impl Item for ChannelView {
+//     fn act_as_type<'a>(
+//         &'a self,
+//         type_id: TypeId,
+//         self_handle: &'a ViewHandle<Self>,
+//         _: &'a AppContext,
+//     ) -> Option<&'a AnyViewHandle> {
+//         if type_id == TypeId::of::<Self>() {
+//             Some(self_handle)
+//         } else if type_id == TypeId::of::<Editor>() {
+//             Some(&self.editor)
+//         } else {
+//             None
+//         }
+//     }
+
+//     fn tab_content<V: 'static>(
+//         &self,
+//         _: Option<usize>,
+//         style: &theme::Tab,
+//         cx: &gpui::AppContext,
+//     ) -> AnyElement<V> {
+//         let label = if let Some(channel) = self.channel(cx) {
+//             match (
+//                 channel.can_edit_notes(),
+//                 self.channel_buffer.read(cx).is_connected(),
+//             ) {
+//                 (true, true) => format!("#{}", channel.name),
+//                 (false, true) => format!("#{} (read-only)", channel.name),
+//                 (_, false) => format!("#{} (disconnected)", channel.name),
+//             }
+//         } else {
+//             format!("channel notes (disconnected)")
+//         };
+//         Label::new(label, style.label.to_owned()).into_any()
+//     }
+
+//     fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self> {
+//         Some(Self::new(
+//             self.project.clone(),
+//             self.channel_store.clone(),
+//             self.channel_buffer.clone(),
+//             cx,
+//         ))
+//     }
+
+//     fn is_singleton(&self, _cx: &AppContext) -> bool {
+//         false
+//     }
+
+//     fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
+//         self.editor
+//             .update(cx, |editor, cx| editor.navigate(data, cx))
+//     }
+
+//     fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
+//         self.editor
+//             .update(cx, |editor, cx| Item::deactivated(editor, cx))
+//     }
+
+//     fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
+//         self.editor
+//             .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
+//     }
+
+//     fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+//         Some(Box::new(self.editor.clone()))
+//     }
+
+//     fn show_toolbar(&self) -> bool {
+//         true
+//     }
+
+//     fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
+//         self.editor.read(cx).pixel_position_of_cursor(cx)
+//     }
+
+//     fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
+//         editor::Editor::to_item_events(event)
+//     }
+// }
+
+// impl FollowableItem for ChannelView {
+//     fn remote_id(&self) -> Option<workspace::ViewId> {
+//         self.remote_id
+//     }
+
+//     fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
+//         let channel_buffer = self.channel_buffer.read(cx);
+//         if !channel_buffer.is_connected() {
+//             return None;
+//         }
+
+//         Some(proto::view::Variant::ChannelView(
+//             proto::view::ChannelView {
+//                 channel_id: channel_buffer.channel_id,
+//                 editor: if let Some(proto::view::Variant::Editor(proto)) =
+//                     self.editor.read(cx).to_state_proto(cx)
+//                 {
+//                     Some(proto)
+//                 } else {
+//                     None
+//                 },
+//             },
+//         ))
+//     }
+
+//     fn from_state_proto(
+//         pane: ViewHandle<workspace::Pane>,
+//         workspace: ViewHandle<workspace::Workspace>,
+//         remote_id: workspace::ViewId,
+//         state: &mut Option<proto::view::Variant>,
+//         cx: &mut AppContext,
+//     ) -> Option<gpui::Task<anyhow::Result<ViewHandle<Self>>>> {
+//         let Some(proto::view::Variant::ChannelView(_)) = state else {
+//             return None;
+//         };
+//         let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
+//             unreachable!()
+//         };
+
+//         let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
+
+//         Some(cx.spawn(|mut cx| async move {
+//             let this = open.await?;
+
+//             let task = this
+//                 .update(&mut cx, |this, cx| {
+//                     this.remote_id = Some(remote_id);
+
+//                     if let Some(state) = state.editor {
+//                         Some(this.editor.update(cx, |editor, cx| {
+//                             editor.apply_update_proto(
+//                                 &this.project,
+//                                 proto::update_view::Variant::Editor(proto::update_view::Editor {
+//                                     selections: state.selections,
+//                                     pending_selection: state.pending_selection,
+//                                     scroll_top_anchor: state.scroll_top_anchor,
+//                                     scroll_x: state.scroll_x,
+//                                     scroll_y: state.scroll_y,
+//                                     ..Default::default()
+//                                 }),
+//                                 cx,
+//                             )
+//                         }))
+//                     } else {
+//                         None
+//                     }
+//                 })
+//                 .ok_or_else(|| anyhow!("window was closed"))?;
+
+//             if let Some(task) = task {
+//                 task.await?;
+//             }
+
+//             Ok(this)
+//         }))
+//     }
+
+//     fn add_event_to_update_proto(
+//         &self,
+//         event: &Self::Event,
+//         update: &mut Option<proto::update_view::Variant>,
+//         cx: &AppContext,
+//     ) -> bool {
+//         self.editor
+//             .read(cx)
+//             .add_event_to_update_proto(event, update, cx)
+//     }
+
+//     fn apply_update_proto(
+//         &mut self,
+//         project: &ModelHandle<Project>,
+//         message: proto::update_view::Variant,
+//         cx: &mut ViewContext<Self>,
+//     ) -> gpui::Task<anyhow::Result<()>> {
+//         self.editor.update(cx, |editor, cx| {
+//             editor.apply_update_proto(project, message, cx)
+//         })
+//     }
+
+//     fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
+//         self.editor.update(cx, |editor, cx| {
+//             editor.set_leader_peer_id(leader_peer_id, cx)
+//         })
+//     }
+
+//     fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
+//         Editor::should_unfollow_on_event(event, cx)
+//     }
+
+//     fn is_project_item(&self, _cx: &AppContext) -> bool {
+//         false
+//     }
+// }
+
+// struct ChannelBufferCollaborationHub(ModelHandle<ChannelBuffer>);
+
+// impl CollaborationHub for ChannelBufferCollaborationHub {
+//     fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
+//         self.0.read(cx).collaborators()
+//     }
+
+//     fn user_participant_indices<'a>(
+//         &self,
+//         cx: &'a AppContext,
+//     ) -> &'a HashMap<u64, ParticipantIndex> {
+//         self.0.read(cx).user_store().read(cx).participant_indices()
+//     }
+// }

crates/collab_ui2/src/chat_panel.rs 🔗

@@ -0,0 +1,983 @@
+// use crate::{
+//     channel_view::ChannelView, is_channels_feature_enabled, render_avatar, ChatPanelSettings,
+// };
+// use anyhow::Result;
+// use call::ActiveCall;
+// use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
+// use client::Client;
+// use collections::HashMap;
+// use db::kvp::KEY_VALUE_STORE;
+// use editor::Editor;
+// use gpui::{
+//     actions,
+//     elements::*,
+//     platform::{CursorStyle, MouseButton},
+//     serde_json,
+//     views::{ItemType, Select, SelectStyle},
+//     AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View,
+//     ViewContext, ViewHandle, WeakViewHandle,
+// };
+// use language::LanguageRegistry;
+// use menu::Confirm;
+// use message_editor::MessageEditor;
+// use project::Fs;
+// use rich_text::RichText;
+// use serde::{Deserialize, Serialize};
+// use settings::SettingsStore;
+// use std::sync::Arc;
+// use theme::{IconButton, Theme};
+// use time::{OffsetDateTime, UtcOffset};
+// use util::{ResultExt, TryFutureExt};
+// use workspace::{
+//     dock::{DockPosition, Panel},
+//     Workspace,
+// };
+
+// mod message_editor;
+
+// const MESSAGE_LOADING_THRESHOLD: usize = 50;
+// const CHAT_PANEL_KEY: &'static str = "ChatPanel";
+
+// pub struct ChatPanel {
+//     client: Arc<Client>,
+//     channel_store: ModelHandle<ChannelStore>,
+//     languages: Arc<LanguageRegistry>,
+//     active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
+//     message_list: ListState<ChatPanel>,
+//     input_editor: ViewHandle<MessageEditor>,
+//     channel_select: ViewHandle<Select>,
+//     local_timezone: UtcOffset,
+//     fs: Arc<dyn Fs>,
+//     width: Option<f32>,
+//     active: bool,
+//     pending_serialization: Task<Option<()>>,
+//     subscriptions: Vec<gpui::Subscription>,
+//     workspace: WeakViewHandle<Workspace>,
+//     is_scrolled_to_bottom: bool,
+//     has_focus: bool,
+//     markdown_data: HashMap<ChannelMessageId, RichText>,
+// }
+
+// #[derive(Serialize, Deserialize)]
+// struct SerializedChatPanel {
+//     width: Option<f32>,
+// }
+
+// #[derive(Debug)]
+// pub enum Event {
+//     DockPositionChanged,
+//     Focus,
+//     Dismissed,
+// }
+
+// actions!(
+//     chat_panel,
+//     [LoadMoreMessages, ToggleFocus, OpenChannelNotes, JoinCall]
+// );
+
+// pub fn init(cx: &mut AppContext) {
+//     cx.add_action(ChatPanel::send);
+//     cx.add_action(ChatPanel::load_more_messages);
+//     cx.add_action(ChatPanel::open_notes);
+//     cx.add_action(ChatPanel::join_call);
+// }
+
+// impl ChatPanel {
+//     pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
+//         let fs = workspace.app_state().fs.clone();
+//         let client = workspace.app_state().client.clone();
+//         let channel_store = ChannelStore::global(cx);
+//         let languages = workspace.app_state().languages.clone();
+
+//         let input_editor = cx.add_view(|cx| {
+//             MessageEditor::new(
+//                 languages.clone(),
+//                 channel_store.clone(),
+//                 cx.add_view(|cx| {
+//                     Editor::auto_height(
+//                         4,
+//                         Some(Arc::new(|theme| theme.chat_panel.input_editor.clone())),
+//                         cx,
+//                     )
+//                 }),
+//                 cx,
+//             )
+//         });
+
+//         let workspace_handle = workspace.weak_handle();
+
+//         let channel_select = cx.add_view(|cx| {
+//             let channel_store = channel_store.clone();
+//             let workspace = workspace_handle.clone();
+//             Select::new(0, cx, {
+//                 move |ix, item_type, is_hovered, cx| {
+//                     Self::render_channel_name(
+//                         &channel_store,
+//                         ix,
+//                         item_type,
+//                         is_hovered,
+//                         workspace,
+//                         cx,
+//                     )
+//                 }
+//             })
+//             .with_style(move |cx| {
+//                 let style = &theme::current(cx).chat_panel.channel_select;
+//                 SelectStyle {
+//                     header: Default::default(),
+//                     menu: style.menu,
+//                 }
+//             })
+//         });
+
+//         let mut message_list =
+//             ListState::<Self>::new(0, Orientation::Bottom, 10., move |this, ix, cx| {
+//                 this.render_message(ix, cx)
+//             });
+//         message_list.set_scroll_handler(|visible_range, count, this, cx| {
+//             if visible_range.start < MESSAGE_LOADING_THRESHOLD {
+//                 this.load_more_messages(&LoadMoreMessages, cx);
+//             }
+//             this.is_scrolled_to_bottom = visible_range.end == count;
+//         });
+
+//         cx.add_view(|cx| {
+//             let mut this = Self {
+//                 fs,
+//                 client,
+//                 channel_store,
+//                 languages,
+//                 active_chat: Default::default(),
+//                 pending_serialization: Task::ready(None),
+//                 message_list,
+//                 input_editor,
+//                 channel_select,
+//                 local_timezone: cx.platform().local_timezone(),
+//                 has_focus: false,
+//                 subscriptions: Vec::new(),
+//                 workspace: workspace_handle,
+//                 is_scrolled_to_bottom: true,
+//                 active: false,
+//                 width: None,
+//                 markdown_data: Default::default(),
+//             };
+
+//             let mut old_dock_position = this.position(cx);
+//             this.subscriptions
+//                 .push(
+//                     cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
+//                         let new_dock_position = this.position(cx);
+//                         if new_dock_position != old_dock_position {
+//                             old_dock_position = new_dock_position;
+//                             cx.emit(Event::DockPositionChanged);
+//                         }
+//                         cx.notify();
+//                     }),
+//                 );
+
+//             this.update_channel_count(cx);
+//             cx.observe(&this.channel_store, |this, _, cx| {
+//                 this.update_channel_count(cx)
+//             })
+//             .detach();
+
+//             cx.observe(&this.channel_select, |this, channel_select, cx| {
+//                 let selected_ix = channel_select.read(cx).selected_index();
+
+//                 let selected_channel_id = this
+//                     .channel_store
+//                     .read(cx)
+//                     .channel_at(selected_ix)
+//                     .map(|e| e.id);
+//                 if let Some(selected_channel_id) = selected_channel_id {
+//                     this.select_channel(selected_channel_id, None, cx)
+//                         .detach_and_log_err(cx);
+//                 }
+//             })
+//             .detach();
+
+//             this
+//         })
+//     }
+
+//     pub fn is_scrolled_to_bottom(&self) -> bool {
+//         self.is_scrolled_to_bottom
+//     }
+
+//     pub fn active_chat(&self) -> Option<ModelHandle<ChannelChat>> {
+//         self.active_chat.as_ref().map(|(chat, _)| chat.clone())
+//     }
+
+//     pub fn load(
+//         workspace: WeakViewHandle<Workspace>,
+//         cx: AsyncAppContext,
+//     ) -> Task<Result<ViewHandle<Self>>> {
+//         cx.spawn(|mut cx| async move {
+//             let serialized_panel = if let Some(panel) = cx
+//                 .background()
+//                 .spawn(async move { KEY_VALUE_STORE.read_kvp(CHAT_PANEL_KEY) })
+//                 .await
+//                 .log_err()
+//                 .flatten()
+//             {
+//                 Some(serde_json::from_str::<SerializedChatPanel>(&panel)?)
+//             } else {
+//                 None
+//             };
+
+//             workspace.update(&mut cx, |workspace, cx| {
+//                 let panel = Self::new(workspace, cx);
+//                 if let Some(serialized_panel) = serialized_panel {
+//                     panel.update(cx, |panel, cx| {
+//                         panel.width = serialized_panel.width;
+//                         cx.notify();
+//                     });
+//                 }
+//                 panel
+//             })
+//         })
+//     }
+
+//     fn serialize(&mut self, cx: &mut ViewContext<Self>) {
+//         let width = self.width;
+//         self.pending_serialization = cx.background().spawn(
+//             async move {
+//                 KEY_VALUE_STORE
+//                     .write_kvp(
+//                         CHAT_PANEL_KEY.into(),
+//                         serde_json::to_string(&SerializedChatPanel { width })?,
+//                     )
+//                     .await?;
+//                 anyhow::Ok(())
+//             }
+//             .log_err(),
+//         );
+//     }
+
+//     fn update_channel_count(&mut self, cx: &mut ViewContext<Self>) {
+//         let channel_count = self.channel_store.read(cx).channel_count();
+//         self.channel_select.update(cx, |select, cx| {
+//             select.set_item_count(channel_count, cx);
+//         });
+//     }
+
+//     fn set_active_chat(&mut self, chat: ModelHandle<ChannelChat>, cx: &mut ViewContext<Self>) {
+//         if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
+//             let channel_id = chat.read(cx).channel_id;
+//             {
+//                 self.markdown_data.clear();
+//                 let chat = chat.read(cx);
+//                 self.message_list.reset(chat.message_count());
+
+//                 let channel_name = chat.channel(cx).map(|channel| channel.name.clone());
+//                 self.input_editor.update(cx, |editor, cx| {
+//                     editor.set_channel(channel_id, channel_name, cx);
+//                 });
+//             };
+//             let subscription = cx.subscribe(&chat, Self::channel_did_change);
+//             self.active_chat = Some((chat, subscription));
+//             self.acknowledge_last_message(cx);
+//             self.channel_select.update(cx, |select, cx| {
+//                 if let Some(ix) = self.channel_store.read(cx).index_of_channel(channel_id) {
+//                     select.set_selected_index(ix, cx);
+//                 }
+//             });
+//             cx.notify();
+//         }
+//     }
+
+//     fn channel_did_change(
+//         &mut self,
+//         _: ModelHandle<ChannelChat>,
+//         event: &ChannelChatEvent,
+//         cx: &mut ViewContext<Self>,
+//     ) {
+//         match event {
+//             ChannelChatEvent::MessagesUpdated {
+//                 old_range,
+//                 new_count,
+//             } => {
+//                 self.message_list.splice(old_range.clone(), *new_count);
+//                 if self.active {
+//                     self.acknowledge_last_message(cx);
+//                 }
+//             }
+//             ChannelChatEvent::NewMessage {
+//                 channel_id,
+//                 message_id,
+//             } => {
+//                 if !self.active {
+//                     self.channel_store.update(cx, |store, cx| {
+//                         store.new_message(*channel_id, *message_id, cx)
+//                     })
+//                 }
+//             }
+//         }
+//         cx.notify();
+//     }
+
+//     fn acknowledge_last_message(&mut self, cx: &mut ViewContext<'_, '_, ChatPanel>) {
+//         if self.active && self.is_scrolled_to_bottom {
+//             if let Some((chat, _)) = &self.active_chat {
+//                 chat.update(cx, |chat, cx| {
+//                     chat.acknowledge_last_message(cx);
+//                 });
+//             }
+//         }
+//     }
+
+//     fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+//         let theme = theme::current(cx);
+//         Flex::column()
+//             .with_child(
+//                 ChildView::new(&self.channel_select, cx)
+//                     .contained()
+//                     .with_style(theme.chat_panel.channel_select.container),
+//             )
+//             .with_child(self.render_active_channel_messages(&theme))
+//             .with_child(self.render_input_box(&theme, cx))
+//             .into_any()
+//     }
+
+//     fn render_active_channel_messages(&self, theme: &Arc<Theme>) -> AnyElement<Self> {
+//         let messages = if self.active_chat.is_some() {
+//             List::new(self.message_list.clone())
+//                 .contained()
+//                 .with_style(theme.chat_panel.list)
+//                 .into_any()
+//         } else {
+//             Empty::new().into_any()
+//         };
+
+//         messages.flex(1., true).into_any()
+//     }
+
+//     fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+//         let (message, is_continuation, is_last, is_admin) = self
+//             .active_chat
+//             .as_ref()
+//             .unwrap()
+//             .0
+//             .update(cx, |active_chat, cx| {
+//                 let is_admin = self
+//                     .channel_store
+//                     .read(cx)
+//                     .is_channel_admin(active_chat.channel_id);
+
+//                 let last_message = active_chat.message(ix.saturating_sub(1));
+//                 let this_message = active_chat.message(ix).clone();
+//                 let is_continuation = last_message.id != this_message.id
+//                     && this_message.sender.id == last_message.sender.id;
+
+//                 if let ChannelMessageId::Saved(id) = this_message.id {
+//                     if this_message
+//                         .mentions
+//                         .iter()
+//                         .any(|(_, user_id)| Some(*user_id) == self.client.user_id())
+//                     {
+//                         active_chat.acknowledge_message(id);
+//                     }
+//                 }
+
+//                 (
+//                     this_message,
+//                     is_continuation,
+//                     active_chat.message_count() == ix + 1,
+//                     is_admin,
+//                 )
+//             });
+
+//         let is_pending = message.is_pending();
+//         let theme = theme::current(cx);
+//         let text = self.markdown_data.entry(message.id).or_insert_with(|| {
+//             Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message)
+//         });
+
+//         let now = OffsetDateTime::now_utc();
+
+//         let style = if is_pending {
+//             &theme.chat_panel.pending_message
+//         } else if is_continuation {
+//             &theme.chat_panel.continuation_message
+//         } else {
+//             &theme.chat_panel.message
+//         };
+
+//         let belongs_to_user = Some(message.sender.id) == self.client.user_id();
+//         let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
+//             (message.id, belongs_to_user || is_admin)
+//         {
+//             Some(id)
+//         } else {
+//             None
+//         };
+
+//         enum MessageBackgroundHighlight {}
+//         MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {
+//             let container = style.style_for(state);
+//             if is_continuation {
+//                 Flex::row()
+//                     .with_child(
+//                         text.element(
+//                             theme.editor.syntax.clone(),
+//                             theme.chat_panel.rich_text.clone(),
+//                             cx,
+//                         )
+//                         .flex(1., true),
+//                     )
+//                     .with_child(render_remove(message_id_to_remove, cx, &theme))
+//                     .contained()
+//                     .with_style(*container)
+//                     .with_margin_bottom(if is_last {
+//                         theme.chat_panel.last_message_bottom_spacing
+//                     } else {
+//                         0.
+//                     })
+//                     .into_any()
+//             } else {
+//                 Flex::column()
+//                     .with_child(
+//                         Flex::row()
+//                             .with_child(
+//                                 Flex::row()
+//                                     .with_child(render_avatar(
+//                                         message.sender.avatar.clone(),
+//                                         &theme.chat_panel.avatar,
+//                                         theme.chat_panel.avatar_container,
+//                                     ))
+//                                     .with_child(
+//                                         Label::new(
+//                                             message.sender.github_login.clone(),
+//                                             theme.chat_panel.message_sender.text.clone(),
+//                                         )
+//                                         .contained()
+//                                         .with_style(theme.chat_panel.message_sender.container),
+//                                     )
+//                                     .with_child(
+//                                         Label::new(
+//                                             format_timestamp(
+//                                                 message.timestamp,
+//                                                 now,
+//                                                 self.local_timezone,
+//                                             ),
+//                                             theme.chat_panel.message_timestamp.text.clone(),
+//                                         )
+//                                         .contained()
+//                                         .with_style(theme.chat_panel.message_timestamp.container),
+//                                     )
+//                                     .align_children_center()
+//                                     .flex(1., true),
+//                             )
+//                             .with_child(render_remove(message_id_to_remove, cx, &theme))
+//                             .align_children_center(),
+//                     )
+//                     .with_child(
+//                         Flex::row()
+//                             .with_child(
+//                                 text.element(
+//                                     theme.editor.syntax.clone(),
+//                                     theme.chat_panel.rich_text.clone(),
+//                                     cx,
+//                                 )
+//                                 .flex(1., true),
+//                             )
+//                             // Add a spacer to make everything line up
+//                             .with_child(render_remove(None, cx, &theme)),
+//                     )
+//                     .contained()
+//                     .with_style(*container)
+//                     .with_margin_bottom(if is_last {
+//                         theme.chat_panel.last_message_bottom_spacing
+//                     } else {
+//                         0.
+//                     })
+//                     .into_any()
+//             }
+//         })
+//         .into_any()
+//     }
+
+//     fn render_markdown_with_mentions(
+//         language_registry: &Arc<LanguageRegistry>,
+//         current_user_id: u64,
+//         message: &channel::ChannelMessage,
+//     ) -> RichText {
+//         let mentions = message
+//             .mentions
+//             .iter()
+//             .map(|(range, user_id)| rich_text::Mention {
+//                 range: range.clone(),
+//                 is_self_mention: *user_id == current_user_id,
+//             })
+//             .collect::<Vec<_>>();
+
+//         rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
+//     }
+
+//     fn render_input_box(&self, theme: &Arc<Theme>, cx: &AppContext) -> AnyElement<Self> {
+//         ChildView::new(&self.input_editor, cx)
+//             .contained()
+//             .with_style(theme.chat_panel.input_editor.container)
+//             .into_any()
+//     }
+
+//     fn render_channel_name(
+//         channel_store: &ModelHandle<ChannelStore>,
+//         ix: usize,
+//         item_type: ItemType,
+//         is_hovered: bool,
+//         workspace: WeakViewHandle<Workspace>,
+//         cx: &mut ViewContext<Select>,
+//     ) -> AnyElement<Select> {
+//         let theme = theme::current(cx);
+//         let tooltip_style = &theme.tooltip;
+//         let theme = &theme.chat_panel;
+//         let style = match (&item_type, is_hovered) {
+//             (ItemType::Header, _) => &theme.channel_select.header,
+//             (ItemType::Selected, _) => &theme.channel_select.active_item,
+//             (ItemType::Unselected, false) => &theme.channel_select.item,
+//             (ItemType::Unselected, true) => &theme.channel_select.hovered_item,
+//         };
+
+//         let channel = &channel_store.read(cx).channel_at(ix).unwrap();
+//         let channel_id = channel.id;
+
+//         let mut row = Flex::row()
+//             .with_child(
+//                 Label::new("#".to_string(), style.hash.text.clone())
+//                     .contained()
+//                     .with_style(style.hash.container),
+//             )
+//             .with_child(Label::new(channel.name.clone(), style.name.clone()));
+
+//         if matches!(item_type, ItemType::Header) {
+//             row.add_children([
+//                 MouseEventHandler::new::<OpenChannelNotes, _>(0, cx, |mouse_state, _| {
+//                     render_icon_button(theme.icon_button.style_for(mouse_state), "icons/file.svg")
+//                 })
+//                 .on_click(MouseButton::Left, move |_, _, cx| {
+//                     if let Some(workspace) = workspace.upgrade(cx) {
+//                         ChannelView::open(channel_id, workspace, cx).detach();
+//                     }
+//                 })
+//                 .with_tooltip::<OpenChannelNotes>(
+//                     channel_id as usize,
+//                     "Open Notes",
+//                     Some(Box::new(OpenChannelNotes)),
+//                     tooltip_style.clone(),
+//                     cx,
+//                 )
+//                 .flex_float(),
+//                 MouseEventHandler::new::<ActiveCall, _>(0, cx, |mouse_state, _| {
+//                     render_icon_button(
+//                         theme.icon_button.style_for(mouse_state),
+//                         "icons/speaker-loud.svg",
+//                     )
+//                 })
+//                 .on_click(MouseButton::Left, move |_, _, cx| {
+//                     ActiveCall::global(cx)
+//                         .update(cx, |call, cx| call.join_channel(channel_id, cx))
+//                         .detach_and_log_err(cx);
+//                 })
+//                 .with_tooltip::<ActiveCall>(
+//                     channel_id as usize,
+//                     "Join Call",
+//                     Some(Box::new(JoinCall)),
+//                     tooltip_style.clone(),
+//                     cx,
+//                 )
+//                 .flex_float(),
+//             ]);
+//         }
+
+//         row.align_children_center()
+//             .contained()
+//             .with_style(style.container)
+//             .into_any()
+//     }
+
+//     fn render_sign_in_prompt(
+//         &self,
+//         theme: &Arc<Theme>,
+//         cx: &mut ViewContext<Self>,
+//     ) -> AnyElement<Self> {
+//         enum SignInPromptLabel {}
+
+//         MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
+//             Label::new(
+//                 "Sign in to use chat".to_string(),
+//                 theme
+//                     .chat_panel
+//                     .sign_in_prompt
+//                     .style_for(mouse_state)
+//                     .clone(),
+//             )
+//         })
+//         .with_cursor_style(CursorStyle::PointingHand)
+//         .on_click(MouseButton::Left, move |_, this, cx| {
+//             let client = this.client.clone();
+//             cx.spawn(|this, mut cx| async move {
+//                 if client
+//                     .authenticate_and_connect(true, &cx)
+//                     .log_err()
+//                     .await
+//                     .is_some()
+//                 {
+//                     this.update(&mut cx, |this, cx| {
+//                         if cx.handle().is_focused(cx) {
+//                             cx.focus(&this.input_editor);
+//                         }
+//                     })
+//                     .ok();
+//                 }
+//             })
+//             .detach();
+//         })
+//         .aligned()
+//         .into_any()
+//     }
+
+//     fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+//         if let Some((chat, _)) = self.active_chat.as_ref() {
+//             let message = self
+//                 .input_editor
+//                 .update(cx, |editor, cx| editor.take_message(cx));
+
+//             if let Some(task) = chat
+//                 .update(cx, |chat, cx| chat.send_message(message, cx))
+//                 .log_err()
+//             {
+//                 task.detach();
+//             }
+//         }
+//     }
+
+//     fn remove_message(&mut self, id: u64, cx: &mut ViewContext<Self>) {
+//         if let Some((chat, _)) = self.active_chat.as_ref() {
+//             chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach())
+//         }
+//     }
+
+//     fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
+//         if let Some((chat, _)) = self.active_chat.as_ref() {
+//             chat.update(cx, |channel, cx| {
+//                 if let Some(task) = channel.load_more_messages(cx) {
+//                     task.detach();
+//                 }
+//             })
+//         }
+//     }
+
+//     pub fn select_channel(
+//         &mut self,
+//         selected_channel_id: u64,
+//         scroll_to_message_id: Option<u64>,
+//         cx: &mut ViewContext<ChatPanel>,
+//     ) -> Task<Result<()>> {
+//         let open_chat = self
+//             .active_chat
+//             .as_ref()
+//             .and_then(|(chat, _)| {
+//                 (chat.read(cx).channel_id == selected_channel_id)
+//                     .then(|| Task::ready(anyhow::Ok(chat.clone())))
+//             })
+//             .unwrap_or_else(|| {
+//                 self.channel_store.update(cx, |store, cx| {
+//                     store.open_channel_chat(selected_channel_id, cx)
+//                 })
+//             });
+
+//         cx.spawn(|this, mut cx| async move {
+//             let chat = open_chat.await?;
+//             this.update(&mut cx, |this, cx| {
+//                 this.set_active_chat(chat.clone(), cx);
+//             })?;
+
+//             if let Some(message_id) = scroll_to_message_id {
+//                 if let Some(item_ix) =
+//                     ChannelChat::load_history_since_message(chat.clone(), message_id, cx.clone())
+//                         .await
+//                 {
+//                     this.update(&mut cx, |this, cx| {
+//                         if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
+//                             this.message_list.scroll_to(ListOffset {
+//                                 item_ix,
+//                                 offset_in_item: 0.,
+//                             });
+//                             cx.notify();
+//                         }
+//                     })?;
+//                 }
+//             }
+
+//             Ok(())
+//         })
+//     }
+
+//     fn open_notes(&mut self, _: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
+//         if let Some((chat, _)) = &self.active_chat {
+//             let channel_id = chat.read(cx).channel_id;
+//             if let Some(workspace) = self.workspace.upgrade(cx) {
+//                 ChannelView::open(channel_id, workspace, cx).detach();
+//             }
+//         }
+//     }
+
+//     fn join_call(&mut self, _: &JoinCall, cx: &mut ViewContext<Self>) {
+//         if let Some((chat, _)) = &self.active_chat {
+//             let channel_id = chat.read(cx).channel_id;
+//             ActiveCall::global(cx)
+//                 .update(cx, |call, cx| call.join_channel(channel_id, cx))
+//                 .detach_and_log_err(cx);
+//         }
+//     }
+// }
+
+// fn render_remove(
+//     message_id_to_remove: Option<u64>,
+//     cx: &mut ViewContext<'_, '_, ChatPanel>,
+//     theme: &Arc<Theme>,
+// ) -> AnyElement<ChatPanel> {
+//     enum DeleteMessage {}
+
+//     message_id_to_remove
+//         .map(|id| {
+//             MouseEventHandler::new::<DeleteMessage, _>(id as usize, cx, |mouse_state, _| {
+//                 let button_style = theme.chat_panel.icon_button.style_for(mouse_state);
+//                 render_icon_button(button_style, "icons/x.svg")
+//                     .aligned()
+//                     .into_any()
+//             })
+//             .with_padding(Padding::uniform(2.))
+//             .with_cursor_style(CursorStyle::PointingHand)
+//             .on_click(MouseButton::Left, move |_, this, cx| {
+//                 this.remove_message(id, cx);
+//             })
+//             .flex_float()
+//             .into_any()
+//         })
+//         .unwrap_or_else(|| {
+//             let style = theme.chat_panel.icon_button.default;
+
+//             Empty::new()
+//                 .constrained()
+//                 .with_width(style.icon_width)
+//                 .aligned()
+//                 .constrained()
+//                 .with_width(style.button_width)
+//                 .with_height(style.button_width)
+//                 .contained()
+//                 .with_uniform_padding(2.)
+//                 .flex_float()
+//                 .into_any()
+//         })
+// }
+
+// impl Entity for ChatPanel {
+//     type Event = Event;
+// }
+
+// impl View for ChatPanel {
+//     fn ui_name() -> &'static str {
+//         "ChatPanel"
+//     }
+
+//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+//         let theme = theme::current(cx);
+//         let element = if self.client.user_id().is_some() {
+//             self.render_channel(cx)
+//         } else {
+//             self.render_sign_in_prompt(&theme, cx)
+//         };
+//         element
+//             .contained()
+//             .with_style(theme.chat_panel.container)
+//             .constrained()
+//             .with_min_width(150.)
+//             .into_any()
+//     }
+
+//     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+//         self.has_focus = true;
+//         if matches!(
+//             *self.client.status().borrow(),
+//             client::Status::Connected { .. }
+//         ) {
+//             let editor = self.input_editor.read(cx).editor.clone();
+//             cx.focus(&editor);
+//         }
+//     }
+
+//     fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
+//         self.has_focus = false;
+//     }
+// }
+
+// impl Panel for ChatPanel {
+//     fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
+//         settings::get::<ChatPanelSettings>(cx).dock
+//     }
+
+//     fn position_is_valid(&self, position: DockPosition) -> bool {
+//         matches!(position, DockPosition::Left | DockPosition::Right)
+//     }
+
+//     fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
+//         settings::update_settings_file::<ChatPanelSettings>(self.fs.clone(), cx, move |settings| {
+//             settings.dock = Some(position)
+//         });
+//     }
+
+//     fn size(&self, cx: &gpui::WindowContext) -> f32 {
+//         self.width
+//             .unwrap_or_else(|| settings::get::<ChatPanelSettings>(cx).default_width)
+//     }
+
+//     fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
+//         self.width = size;
+//         self.serialize(cx);
+//         cx.notify();
+//     }
+
+//     fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
+//         self.active = active;
+//         if active {
+//             self.acknowledge_last_message(cx);
+//             if !is_channels_feature_enabled(cx) {
+//                 cx.emit(Event::Dismissed);
+//             }
+//         }
+//     }
+
+//     fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
+//         (settings::get::<ChatPanelSettings>(cx).button && is_channels_feature_enabled(cx))
+//             .then(|| "icons/conversations.svg")
+//     }
+
+//     fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
+//         ("Chat Panel".to_string(), Some(Box::new(ToggleFocus)))
+//     }
+
+//     fn should_change_position_on_event(event: &Self::Event) -> bool {
+//         matches!(event, Event::DockPositionChanged)
+//     }
+
+//     fn should_close_on_event(event: &Self::Event) -> bool {
+//         matches!(event, Event::Dismissed)
+//     }
+
+//     fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
+//         self.has_focus
+//     }
+
+//     fn is_focus_event(event: &Self::Event) -> bool {
+//         matches!(event, Event::Focus)
+//     }
+// }
+
+// fn format_timestamp(
+//     mut timestamp: OffsetDateTime,
+//     mut now: OffsetDateTime,
+//     local_timezone: UtcOffset,
+// ) -> String {
+//     timestamp = timestamp.to_offset(local_timezone);
+//     now = now.to_offset(local_timezone);
+
+//     let today = now.date();
+//     let date = timestamp.date();
+//     let mut hour = timestamp.hour();
+//     let mut part = "am";
+//     if hour > 12 {
+//         hour -= 12;
+//         part = "pm";
+//     }
+//     if date == today {
+//         format!("{:02}:{:02}{}", hour, timestamp.minute(), part)
+//     } else if date.next_day() == Some(today) {
+//         format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part)
+//     } else {
+//         format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
+//     }
+// }
+
+// fn render_icon_button<V: View>(style: &IconButton, svg_path: &'static str) -> impl Element<V> {
+//     Svg::new(svg_path)
+//         .with_color(style.color)
+//         .constrained()
+//         .with_width(style.icon_width)
+//         .aligned()
+//         .constrained()
+//         .with_width(style.button_width)
+//         .with_height(style.button_width)
+//         .contained()
+//         .with_style(style.container)
+// }
+
+// #[cfg(test)]
+// mod tests {
+//     use super::*;
+//     use gpui::fonts::HighlightStyle;
+//     use pretty_assertions::assert_eq;
+//     use rich_text::{BackgroundKind, Highlight, RenderedRegion};
+//     use util::test::marked_text_ranges;
+
+//     #[gpui::test]
+//     fn test_render_markdown_with_mentions() {
+//         let language_registry = Arc::new(LanguageRegistry::test());
+//         let (body, ranges) = marked_text_ranges("*hi*, «@abc», let's **call** «@fgh»", false);
+//         let message = channel::ChannelMessage {
+//             id: ChannelMessageId::Saved(0),
+//             body,
+//             timestamp: OffsetDateTime::now_utc(),
+//             sender: Arc::new(client::User {
+//                 github_login: "fgh".into(),
+//                 avatar: None,
+//                 id: 103,
+//             }),
+//             nonce: 5,
+//             mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
+//         };
+
+//         let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
+
+//         // Note that the "'" was replaced with ’ due to smart punctuation.
+//         let (body, ranges) = marked_text_ranges("«hi», «@abc», let’s «call» «@fgh»", false);
+//         assert_eq!(message.text, body);
+//         assert_eq!(
+//             message.highlights,
+//             vec![
+//                 (
+//                     ranges[0].clone(),
+//                     HighlightStyle {
+//                         italic: Some(true),
+//                         ..Default::default()
+//                     }
+//                     .into()
+//                 ),
+//                 (ranges[1].clone(), Highlight::Mention),
+//                 (
+//                     ranges[2].clone(),
+//                     HighlightStyle {
+//                         weight: Some(gpui::fonts::Weight::BOLD),
+//                         ..Default::default()
+//                     }
+//                     .into()
+//                 ),
+//                 (ranges[3].clone(), Highlight::SelfMention)
+//             ]
+//         );
+//         assert_eq!(
+//             message.regions,
+//             vec![
+//                 RenderedRegion {
+//                     background_kind: Some(BackgroundKind::Mention),
+//                     link_url: None
+//                 },
+//                 RenderedRegion {
+//                     background_kind: Some(BackgroundKind::SelfMention),
+//                     link_url: None
+//                 },
+//             ]
+//         );
+//     }
+// }

crates/collab_ui2/src/chat_panel/message_editor.rs 🔗

@@ -0,0 +1,313 @@
+use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
+use client::UserId;
+use collections::HashMap;
+use editor::{AnchorRangeExt, Editor};
+use gpui::{
+    elements::ChildView, AnyElement, AsyncAppContext, Element, Entity, ModelHandle, Task, View,
+    ViewContext, ViewHandle, WeakViewHandle,
+};
+use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
+use lazy_static::lazy_static;
+use project::search::SearchQuery;
+use std::{sync::Arc, time::Duration};
+
+const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
+
+lazy_static! {
+    static ref MENTIONS_SEARCH: SearchQuery = SearchQuery::regex(
+        "@[-_\\w]+",
+        false,
+        false,
+        Default::default(),
+        Default::default()
+    )
+    .unwrap();
+}
+
+pub struct MessageEditor {
+    pub editor: ViewHandle<Editor>,
+    channel_store: ModelHandle<ChannelStore>,
+    users: HashMap<String, UserId>,
+    mentions: Vec<UserId>,
+    mentions_task: Option<Task<()>>,
+    channel_id: Option<ChannelId>,
+}
+
+impl MessageEditor {
+    pub fn new(
+        language_registry: Arc<LanguageRegistry>,
+        channel_store: ModelHandle<ChannelStore>,
+        editor: ViewHandle<Editor>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        editor.update(cx, |editor, cx| {
+            editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
+        });
+
+        let buffer = editor
+            .read(cx)
+            .buffer()
+            .read(cx)
+            .as_singleton()
+            .expect("message editor must be singleton");
+
+        cx.subscribe(&buffer, Self::on_buffer_event).detach();
+
+        let markdown = language_registry.language_for_name("Markdown");
+        cx.app_context()
+            .spawn(|mut cx| async move {
+                let markdown = markdown.await?;
+                buffer.update(&mut cx, |buffer, cx| {
+                    buffer.set_language(Some(markdown), cx)
+                });
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
+
+        Self {
+            editor,
+            channel_store,
+            users: HashMap::default(),
+            channel_id: None,
+            mentions: Vec::new(),
+            mentions_task: None,
+        }
+    }
+
+    pub fn set_channel(
+        &mut self,
+        channel_id: u64,
+        channel_name: Option<String>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.editor.update(cx, |editor, cx| {
+            if let Some(channel_name) = channel_name {
+                editor.set_placeholder_text(format!("Message #{}", channel_name), cx);
+            } else {
+                editor.set_placeholder_text(format!("Message Channel"), cx);
+            }
+        });
+        self.channel_id = Some(channel_id);
+        self.refresh_users(cx);
+    }
+
+    pub fn refresh_users(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(channel_id) = self.channel_id {
+            let members = self.channel_store.update(cx, |store, cx| {
+                store.get_channel_member_details(channel_id, cx)
+            });
+            cx.spawn(|this, mut cx| async move {
+                let members = members.await?;
+                this.update(&mut cx, |this, cx| this.set_members(members, cx))?;
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
+        }
+    }
+
+    pub fn set_members(&mut self, members: Vec<ChannelMembership>, _: &mut ViewContext<Self>) {
+        self.users.clear();
+        self.users.extend(
+            members
+                .into_iter()
+                .map(|member| (member.user.github_login.clone(), member.user.id)),
+        );
+    }
+
+    pub fn take_message(&mut self, cx: &mut ViewContext<Self>) -> MessageParams {
+        self.editor.update(cx, |editor, cx| {
+            let highlights = editor.text_highlights::<Self>(cx);
+            let text = editor.text(cx);
+            let snapshot = editor.buffer().read(cx).snapshot(cx);
+            let mentions = if let Some((_, ranges)) = highlights {
+                ranges
+                    .iter()
+                    .map(|range| range.to_offset(&snapshot))
+                    .zip(self.mentions.iter().copied())
+                    .collect()
+            } else {
+                Vec::new()
+            };
+
+            editor.clear(cx);
+            self.mentions.clear();
+
+            MessageParams { text, mentions }
+        })
+    }
+
+    fn on_buffer_event(
+        &mut self,
+        buffer: ModelHandle<Buffer>,
+        event: &language::Event,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if let language::Event::Reparsed | language::Event::Edited = event {
+            let buffer = buffer.read(cx).snapshot();
+            self.mentions_task = Some(cx.spawn(|this, cx| async move {
+                cx.background().timer(MENTIONS_DEBOUNCE_INTERVAL).await;
+                Self::find_mentions(this, buffer, cx).await;
+            }));
+        }
+    }
+
+    async fn find_mentions(
+        this: WeakViewHandle<MessageEditor>,
+        buffer: BufferSnapshot,
+        mut cx: AsyncAppContext,
+    ) {
+        let (buffer, ranges) = cx
+            .background()
+            .spawn(async move {
+                let ranges = MENTIONS_SEARCH.search(&buffer, None).await;
+                (buffer, ranges)
+            })
+            .await;
+
+        this.update(&mut cx, |this, cx| {
+            let mut anchor_ranges = Vec::new();
+            let mut mentioned_user_ids = Vec::new();
+            let mut text = String::new();
+
+            this.editor.update(cx, |editor, cx| {
+                let multi_buffer = editor.buffer().read(cx).snapshot(cx);
+                for range in ranges {
+                    text.clear();
+                    text.extend(buffer.text_for_range(range.clone()));
+                    if let Some(username) = text.strip_prefix("@") {
+                        if let Some(user_id) = this.users.get(username) {
+                            let start = multi_buffer.anchor_after(range.start);
+                            let end = multi_buffer.anchor_after(range.end);
+
+                            mentioned_user_ids.push(*user_id);
+                            anchor_ranges.push(start..end);
+                        }
+                    }
+                }
+
+                editor.clear_highlights::<Self>(cx);
+                editor.highlight_text::<Self>(
+                    anchor_ranges,
+                    theme::current(cx).chat_panel.rich_text.mention_highlight,
+                    cx,
+                )
+            });
+
+            this.mentions = mentioned_user_ids;
+            this.mentions_task.take();
+        })
+        .ok();
+    }
+}
+
+impl Entity for MessageEditor {
+    type Event = ();
+}
+
+impl View for MessageEditor {
+    fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
+        ChildView::new(&self.editor, cx).into_any()
+    }
+
+    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+        if cx.is_self_focused() {
+            cx.focus(&self.editor);
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use client::{Client, User, UserStore};
+    use gpui::{TestAppContext, WindowHandle};
+    use language::{Language, LanguageConfig};
+    use rpc::proto;
+    use settings::SettingsStore;
+    use util::{http::FakeHttpClient, test::marked_text_ranges};
+
+    #[gpui::test]
+    async fn test_message_editor(cx: &mut TestAppContext) {
+        let editor = init_test(cx);
+        let editor = editor.root(cx);
+
+        editor.update(cx, |editor, cx| {
+            editor.set_members(
+                vec![
+                    ChannelMembership {
+                        user: Arc::new(User {
+                            github_login: "a-b".into(),
+                            id: 101,
+                            avatar: None,
+                        }),
+                        kind: proto::channel_member::Kind::Member,
+                        role: proto::ChannelRole::Member,
+                    },
+                    ChannelMembership {
+                        user: Arc::new(User {
+                            github_login: "C_D".into(),
+                            id: 102,
+                            avatar: None,
+                        }),
+                        kind: proto::channel_member::Kind::Member,
+                        role: proto::ChannelRole::Member,
+                    },
+                ],
+                cx,
+            );
+
+            editor.editor.update(cx, |editor, cx| {
+                editor.set_text("Hello, @a-b! Have you met @C_D?", cx)
+            });
+        });
+
+        cx.foreground().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
+
+        editor.update(cx, |editor, cx| {
+            let (text, ranges) = marked_text_ranges("Hello, «@a-b»! Have you met «@C_D»?", false);
+            assert_eq!(
+                editor.take_message(cx),
+                MessageParams {
+                    text,
+                    mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
+                }
+            );
+        });
+    }
+
+    fn init_test(cx: &mut TestAppContext) -> WindowHandle<MessageEditor> {
+        cx.foreground().forbid_parking();
+
+        cx.update(|cx| {
+            let http = FakeHttpClient::with_404_response();
+            let client = Client::new(http.clone(), cx);
+            let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
+            cx.set_global(SettingsStore::test(cx));
+            theme::init((), cx);
+            language::init(cx);
+            editor::init(cx);
+            client::init(&client, cx);
+            channel::init(&client, user_store, cx);
+        });
+
+        let language_registry = Arc::new(LanguageRegistry::test());
+        language_registry.add(Arc::new(Language::new(
+            LanguageConfig {
+                name: "Markdown".into(),
+                ..Default::default()
+            },
+            Some(tree_sitter_markdown::language()),
+        )));
+
+        let editor = cx.add_window(|cx| {
+            MessageEditor::new(
+                language_registry,
+                ChannelStore::global(cx),
+                cx.add_view(|cx| Editor::auto_height(4, None, cx)),
+                cx,
+            )
+        });
+        cx.foreground().run_until_parked();
+        editor
+    }
+}

crates/collab_ui2/src/collab_panel.rs 🔗

@@ -0,0 +1,3575 @@
+// mod channel_modal;
+// mod contact_finder;
+
+// use crate::{
+//     channel_view::{self, ChannelView},
+//     chat_panel::ChatPanel,
+//     face_pile::FacePile,
+//     panel_settings, CollaborationPanelSettings,
+// };
+// use anyhow::Result;
+// use call::ActiveCall;
+// use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
+// use channel_modal::ChannelModal;
+// use client::{
+//     proto::{self, PeerId},
+//     Client, Contact, User, UserStore,
+// };
+// use contact_finder::ContactFinder;
+// use context_menu::{ContextMenu, ContextMenuItem};
+// use db::kvp::KEY_VALUE_STORE;
+// use drag_and_drop::{DragAndDrop, Draggable};
+// use editor::{Cancel, Editor};
+// use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
+// use futures::StreamExt;
+// use fuzzy::{match_strings, StringMatchCandidate};
+// use gpui::{
+//     actions,
+//     elements::{
+//         Canvas, ChildView, Component, ContainerStyle, Empty, Flex, Image, Label, List, ListOffset,
+//         ListState, MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement,
+//         SafeStylable, Stack, Svg,
+//     },
+//     fonts::TextStyle,
+//     geometry::{
+//         rect::RectF,
+//         vector::{vec2f, Vector2F},
+//     },
+//     impl_actions,
+//     platform::{CursorStyle, MouseButton, PromptLevel},
+//     serde_json, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, FontCache,
+//     ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+// };
+// use menu::{Confirm, SelectNext, SelectPrev};
+// use project::{Fs, Project};
+// use serde_derive::{Deserialize, Serialize};
+// use settings::SettingsStore;
+// use std::{borrow::Cow, hash::Hash, mem, sync::Arc};
+// use theme::{components::ComponentExt, IconButton, Interactive};
+// use util::{maybe, ResultExt, TryFutureExt};
+// use workspace::{
+//     dock::{DockPosition, Panel},
+//     item::ItemHandle,
+//     FollowNextCollaborator, Workspace,
+// };
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct ToggleCollapse {
+//     location: ChannelId,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct NewChannel {
+//     location: ChannelId,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct RenameChannel {
+//     channel_id: ChannelId,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct ToggleSelectedIx {
+//     ix: usize,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct RemoveChannel {
+//     channel_id: ChannelId,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct InviteMembers {
+//     channel_id: ChannelId,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct ManageMembers {
+//     channel_id: ChannelId,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// pub struct OpenChannelNotes {
+//     pub channel_id: ChannelId,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// pub struct JoinChannelCall {
+//     pub channel_id: u64,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// pub struct JoinChannelChat {
+//     pub channel_id: u64,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// pub struct CopyChannelLink {
+//     pub channel_id: u64,
+// }
+
+// #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct StartMoveChannelFor {
+//     channel_id: ChannelId,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct MoveChannel {
+//     to: ChannelId,
+// }
+
+actions!(
+    ToggleFocus,
+    Remove,
+    Secondary,
+    CollapseSelectedChannel,
+    ExpandSelectedChannel,
+    StartMoveChannel,
+    MoveSelected,
+    InsertSpace,
+);
+
+// impl_actions!(
+//     collab_panel,
+//     [
+//         RemoveChannel,
+//         NewChannel,
+//         InviteMembers,
+//         ManageMembers,
+//         RenameChannel,
+//         ToggleCollapse,
+//         OpenChannelNotes,
+//         JoinChannelCall,
+//         JoinChannelChat,
+//         CopyChannelLink,
+//         StartMoveChannelFor,
+//         MoveChannel,
+//         ToggleSelectedIx
+//     ]
+// );
+
+// #[derive(Debug, Copy, Clone, PartialEq, Eq)]
+// struct ChannelMoveClipboard {
+//     channel_id: ChannelId,
+// }
+
+const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
+
+use std::sync::Arc;
+
+use db::kvp::KEY_VALUE_STORE;
+use gpui::{
+    actions, div, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle,
+    Focusable, FocusableView, InteractiveComponent, ParentComponent, Render, View, ViewContext,
+    VisualContext, WeakView,
+};
+use project::Fs;
+use serde_derive::{Deserialize, Serialize};
+use settings::Settings;
+use util::ResultExt;
+use workspace::{
+    dock::{DockPosition, Panel, PanelEvent},
+    Workspace,
+};
+
+use crate::CollaborationPanelSettings;
+
+pub fn init(cx: &mut AppContext) {
+    cx.observe_new_views(|workspace: &mut Workspace, _| {
+        workspace.register_action(|workspace, _: &ToggleFocus, cx| {
+            workspace.toggle_panel_focus::<CollabPanel>(cx);
+        });
+    })
+    .detach();
+    //     contact_finder::init(cx);
+    //     channel_modal::init(cx);
+    //     channel_view::init(cx);
+
+    //     cx.add_action(CollabPanel::cancel);
+    //     cx.add_action(CollabPanel::select_next);
+    //     cx.add_action(CollabPanel::select_prev);
+    //     cx.add_action(CollabPanel::confirm);
+    //     cx.add_action(CollabPanel::insert_space);
+    //     cx.add_action(CollabPanel::remove);
+    //     cx.add_action(CollabPanel::remove_selected_channel);
+    //     cx.add_action(CollabPanel::show_inline_context_menu);
+    //     cx.add_action(CollabPanel::new_subchannel);
+    //     cx.add_action(CollabPanel::invite_members);
+    //     cx.add_action(CollabPanel::manage_members);
+    //     cx.add_action(CollabPanel::rename_selected_channel);
+    //     cx.add_action(CollabPanel::rename_channel);
+    //     cx.add_action(CollabPanel::toggle_channel_collapsed_action);
+    //     cx.add_action(CollabPanel::collapse_selected_channel);
+    //     cx.add_action(CollabPanel::expand_selected_channel);
+    //     cx.add_action(CollabPanel::open_channel_notes);
+    //     cx.add_action(CollabPanel::join_channel_chat);
+    //     cx.add_action(CollabPanel::copy_channel_link);
+
+    //     cx.add_action(
+    //         |panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext<CollabPanel>| {
+    //             if panel.selection.take() != Some(action.ix) {
+    //                 panel.selection = Some(action.ix)
+    //             }
+
+    //             cx.notify();
+    //         },
+    //     );
+
+    //     cx.add_action(
+    //         |panel: &mut CollabPanel,
+    //          action: &StartMoveChannelFor,
+    //          _: &mut ViewContext<CollabPanel>| {
+    //             panel.channel_clipboard = Some(ChannelMoveClipboard {
+    //                 channel_id: action.channel_id,
+    //             });
+    //         },
+    //     );
+
+    //     cx.add_action(
+    //         |panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext<CollabPanel>| {
+    //             if let Some(channel) = panel.selected_channel() {
+    //                 panel.channel_clipboard = Some(ChannelMoveClipboard {
+    //                     channel_id: channel.id,
+    //                 })
+    //             }
+    //         },
+    //     );
+
+    //     cx.add_action(
+    //         |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext<CollabPanel>| {
+    //             let Some(clipboard) = panel.channel_clipboard.take() else {
+    //                 return;
+    //             };
+    //             let Some(selected_channel) = panel.selected_channel() else {
+    //                 return;
+    //             };
+
+    //             panel
+    //                 .channel_store
+    //                 .update(cx, |channel_store, cx| {
+    //                     channel_store.move_channel(clipboard.channel_id, Some(selected_channel.id), cx)
+    //                 })
+    //                 .detach_and_log_err(cx)
+    //         },
+    //     );
+
+    //     cx.add_action(
+    //         |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext<CollabPanel>| {
+    //             if let Some(clipboard) = panel.channel_clipboard.take() {
+    //                 panel.channel_store.update(cx, |channel_store, cx| {
+    //                     channel_store
+    //                         .move_channel(clipboard.channel_id, Some(action.to), cx)
+    //                         .detach_and_log_err(cx)
+    //                 })
+    //             }
+    //         },
+    //     );
+}
+
+// #[derive(Debug)]
+// pub enum ChannelEditingState {
+//     Create {
+//         location: Option<ChannelId>,
+//         pending_name: Option<String>,
+//     },
+//     Rename {
+//         location: ChannelId,
+//         pending_name: Option<String>,
+//     },
+// }
+
+// impl ChannelEditingState {
+//     fn pending_name(&self) -> Option<&str> {
+//         match self {
+//             ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(),
+//             ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(),
+//         }
+//     }
+// }
+
+pub struct CollabPanel {
+    width: Option<f32>,
+    fs: Arc<dyn Fs>,
+    focus_handle: FocusHandle,
+    // channel_clipboard: Option<ChannelMoveClipboard>,
+    // pending_serialization: Task<Option<()>>,
+    // context_menu: ViewHandle<ContextMenu>,
+    // filter_editor: ViewHandle<Editor>,
+    // channel_name_editor: ViewHandle<Editor>,
+    // channel_editing_state: Option<ChannelEditingState>,
+    // entries: Vec<ListEntry>,
+    // selection: Option<usize>,
+    // user_store: ModelHandle<UserStore>,
+    // client: Arc<Client>,
+    // channel_store: ModelHandle<ChannelStore>,
+    // project: ModelHandle<Project>,
+    // match_candidates: Vec<StringMatchCandidate>,
+    // list_state: ListState<Self>,
+    // subscriptions: Vec<Subscription>,
+    // collapsed_sections: Vec<Section>,
+    // collapsed_channels: Vec<ChannelId>,
+    // drag_target_channel: ChannelDragTarget,
+    _workspace: WeakView<Workspace>,
+    // context_menu_on_selected: bool,
+}
+
+// #[derive(PartialEq, Eq)]
+// enum ChannelDragTarget {
+//     None,
+//     Root,
+//     Channel(ChannelId),
+// }
+
+#[derive(Serialize, Deserialize)]
+struct SerializedCollabPanel {
+    width: Option<f32>,
+    collapsed_channels: Option<Vec<u64>>,
+}
+
+// #[derive(Debug)]
+// pub enum Event {
+//     DockPositionChanged,
+//     Focus,
+//     Dismissed,
+// }
+
+// #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
+// enum Section {
+//     ActiveCall,
+//     Channels,
+//     ChannelInvites,
+//     ContactRequests,
+//     Contacts,
+//     Online,
+//     Offline,
+// }
+
+// #[derive(Clone, Debug)]
+// enum ListEntry {
+//     Header(Section),
+//     CallParticipant {
+//         user: Arc<User>,
+//         peer_id: Option<PeerId>,
+//         is_pending: bool,
+//     },
+//     ParticipantProject {
+//         project_id: u64,
+//         worktree_root_names: Vec<String>,
+//         host_user_id: u64,
+//         is_last: bool,
+//     },
+//     ParticipantScreen {
+//         peer_id: Option<PeerId>,
+//         is_last: bool,
+//     },
+//     IncomingRequest(Arc<User>),
+//     OutgoingRequest(Arc<User>),
+//     ChannelInvite(Arc<Channel>),
+//     Channel {
+//         channel: Arc<Channel>,
+//         depth: usize,
+//         has_children: bool,
+//     },
+//     ChannelNotes {
+//         channel_id: ChannelId,
+//     },
+//     ChannelChat {
+//         channel_id: ChannelId,
+//     },
+//     ChannelEditor {
+//         depth: usize,
+//     },
+//     Contact {
+//         contact: Arc<Contact>,
+//         calling: bool,
+//     },
+//     ContactPlaceholder,
+// }
+
+// impl Entity for CollabPanel {
+//     type Event = Event;
+// }
+
+impl CollabPanel {
+    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
+        cx.build_view(|cx| {
+            //             let view_id = cx.view_id();
+
+            //             let filter_editor = cx.add_view(|cx| {
+            //                 let mut editor = Editor::single_line(
+            //                     Some(Arc::new(|theme| {
+            //                         theme.collab_panel.user_query_editor.clone()
+            //                     })),
+            //                     cx,
+            //                 );
+            //                 editor.set_placeholder_text("Filter channels, contacts", cx);
+            //                 editor
+            //             });
+
+            //             cx.subscribe(&filter_editor, |this, _, event, cx| {
+            //                 if let editor::Event::BufferEdited = event {
+            //                     let query = this.filter_editor.read(cx).text(cx);
+            //                     if !query.is_empty() {
+            //                         this.selection.take();
+            //                     }
+            //                     this.update_entries(true, cx);
+            //                     if !query.is_empty() {
+            //                         this.selection = this
+            //                             .entries
+            //                             .iter()
+            //                             .position(|entry| !matches!(entry, ListEntry::Header(_)));
+            //                     }
+            //                 } else if let editor::Event::Blurred = event {
+            //                     let query = this.filter_editor.read(cx).text(cx);
+            //                     if query.is_empty() {
+            //                         this.selection.take();
+            //                         this.update_entries(true, cx);
+            //                     }
+            //                 }
+            //             })
+            //             .detach();
+
+            //             let channel_name_editor = cx.add_view(|cx| {
+            //                 Editor::single_line(
+            //                     Some(Arc::new(|theme| {
+            //                         theme.collab_panel.user_query_editor.clone()
+            //                     })),
+            //                     cx,
+            //                 )
+            //             });
+
+            //             cx.subscribe(&channel_name_editor, |this, _, event, cx| {
+            //                 if let editor::Event::Blurred = event {
+            //                     if let Some(state) = &this.channel_editing_state {
+            //                         if state.pending_name().is_some() {
+            //                             return;
+            //                         }
+            //                     }
+            //                     this.take_editing_state(cx);
+            //                     this.update_entries(false, cx);
+            //                     cx.notify();
+            //                 }
+            //             })
+            //             .detach();
+
+            //             let list_state =
+            //                 ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
+            //                     let theme = theme::current(cx).clone();
+            //                     let is_selected = this.selection == Some(ix);
+            //                     let current_project_id = this.project.read(cx).remote_id();
+
+            //                     match &this.entries[ix] {
+            //                         ListEntry::Header(section) => {
+            //                             let is_collapsed = this.collapsed_sections.contains(section);
+            //                             this.render_header(*section, &theme, is_selected, is_collapsed, cx)
+            //                         }
+            //                         ListEntry::CallParticipant {
+            //                             user,
+            //                             peer_id,
+            //                             is_pending,
+            //                         } => Self::render_call_participant(
+            //                             user,
+            //                             *peer_id,
+            //                             this.user_store.clone(),
+            //                             *is_pending,
+            //                             is_selected,
+            //                             &theme,
+            //                             cx,
+            //                         ),
+            //                         ListEntry::ParticipantProject {
+            //                             project_id,
+            //                             worktree_root_names,
+            //                             host_user_id,
+            //                             is_last,
+            //                         } => Self::render_participant_project(
+            //                             *project_id,
+            //                             worktree_root_names,
+            //                             *host_user_id,
+            //                             Some(*project_id) == current_project_id,
+            //                             *is_last,
+            //                             is_selected,
+            //                             &theme,
+            //                             cx,
+            //                         ),
+            //                         ListEntry::ParticipantScreen { peer_id, is_last } => {
+            //                             Self::render_participant_screen(
+            //                                 *peer_id,
+            //                                 *is_last,
+            //                                 is_selected,
+            //                                 &theme.collab_panel,
+            //                                 cx,
+            //                             )
+            //                         }
+            //                         ListEntry::Channel {
+            //                             channel,
+            //                             depth,
+            //                             has_children,
+            //                         } => {
+            //                             let channel_row = this.render_channel(
+            //                                 &*channel,
+            //                                 *depth,
+            //                                 &theme,
+            //                                 is_selected,
+            //                                 *has_children,
+            //                                 ix,
+            //                                 cx,
+            //                             );
+
+            //                             if is_selected && this.context_menu_on_selected {
+            //                                 Stack::new()
+            //                                     .with_child(channel_row)
+            //                                     .with_child(
+            //                                         ChildView::new(&this.context_menu, cx)
+            //                                             .aligned()
+            //                                             .bottom()
+            //                                             .right(),
+            //                                     )
+            //                                     .into_any()
+            //                             } else {
+            //                                 return channel_row;
+            //                             }
+            //                         }
+            //                         ListEntry::ChannelNotes { channel_id } => this.render_channel_notes(
+            //                             *channel_id,
+            //                             &theme.collab_panel,
+            //                             is_selected,
+            //                             ix,
+            //                             cx,
+            //                         ),
+            //                         ListEntry::ChannelChat { channel_id } => this.render_channel_chat(
+            //                             *channel_id,
+            //                             &theme.collab_panel,
+            //                             is_selected,
+            //                             ix,
+            //                             cx,
+            //                         ),
+            //                         ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
+            //                             channel.clone(),
+            //                             this.channel_store.clone(),
+            //                             &theme.collab_panel,
+            //                             is_selected,
+            //                             cx,
+            //                         ),
+            //                         ListEntry::IncomingRequest(user) => Self::render_contact_request(
+            //                             user.clone(),
+            //                             this.user_store.clone(),
+            //                             &theme.collab_panel,
+            //                             true,
+            //                             is_selected,
+            //                             cx,
+            //                         ),
+            //                         ListEntry::OutgoingRequest(user) => Self::render_contact_request(
+            //                             user.clone(),
+            //                             this.user_store.clone(),
+            //                             &theme.collab_panel,
+            //                             false,
+            //                             is_selected,
+            //                             cx,
+            //                         ),
+            //                         ListEntry::Contact { contact, calling } => Self::render_contact(
+            //                             contact,
+            //                             *calling,
+            //                             &this.project,
+            //                             &theme,
+            //                             is_selected,
+            //                             cx,
+            //                         ),
+            //                         ListEntry::ChannelEditor { depth } => {
+            //                             this.render_channel_editor(&theme, *depth, cx)
+            //                         }
+            //                         ListEntry::ContactPlaceholder => {
+            //                             this.render_contact_placeholder(&theme.collab_panel, is_selected, cx)
+            //                         }
+            //                     }
+            //                 });
+
+            let this = Self {
+                width: None,
+                focus_handle: cx.focus_handle(),
+                //                 channel_clipboard: None,
+                fs: workspace.app_state().fs.clone(),
+                //                 pending_serialization: Task::ready(None),
+                //                 context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
+                //                 channel_name_editor,
+                //                 filter_editor,
+                //                 entries: Vec::default(),
+                //                 channel_editing_state: None,
+                //                 selection: None,
+                //                 user_store: workspace.user_store().clone(),
+                //                 channel_store: ChannelStore::global(cx),
+                //                 project: workspace.project().clone(),
+                //                 subscriptions: Vec::default(),
+                //                 match_candidates: Vec::default(),
+                //                 collapsed_sections: vec![Section::Offline],
+                //                 collapsed_channels: Vec::default(),
+                _workspace: workspace.weak_handle(),
+                //                 client: workspace.app_state().client.clone(),
+                //                 context_menu_on_selected: true,
+                //                 drag_target_channel: ChannelDragTarget::None,
+                //                 list_state,
+            };
+
+            //             this.update_entries(false, cx);
+
+            //             // Update the dock position when the setting changes.
+            //             let mut old_dock_position = this.position(cx);
+            //             this.subscriptions
+            //                 .push(
+            //                     cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
+            //                         let new_dock_position = this.position(cx);
+            //                         if new_dock_position != old_dock_position {
+            //                             old_dock_position = new_dock_position;
+            //                             cx.emit(Event::DockPositionChanged);
+            //                         }
+            //                         cx.notify();
+            //                     }),
+            //                 );
+
+            //             let active_call = ActiveCall::global(cx);
+            //             this.subscriptions
+            //                 .push(cx.observe(&this.user_store, |this, _, cx| {
+            //                     this.update_entries(true, cx)
+            //                 }));
+            //             this.subscriptions
+            //                 .push(cx.observe(&this.channel_store, |this, _, cx| {
+            //                     this.update_entries(true, cx)
+            //                 }));
+            //             this.subscriptions
+            //                 .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
+            //             this.subscriptions
+            //                 .push(cx.observe_flag::<ChannelsAlpha, _>(move |_, this, cx| {
+            //                     this.update_entries(true, cx)
+            //                 }));
+            //             this.subscriptions.push(cx.subscribe(
+            //                 &this.channel_store,
+            //                 |this, _channel_store, e, cx| match e {
+            //                     ChannelEvent::ChannelCreated(channel_id)
+            //                     | ChannelEvent::ChannelRenamed(channel_id) => {
+            //                         if this.take_editing_state(cx) {
+            //                             this.update_entries(false, cx);
+            //                             this.selection = this.entries.iter().position(|entry| {
+            //                                 if let ListEntry::Channel { channel, .. } = entry {
+            //                                     channel.id == *channel_id
+            //                                 } else {
+            //                                     false
+            //                                 }
+            //                             });
+            //                         }
+            //                     }
+            //                 },
+            //             ));
+
+            this
+        })
+    }
+
+    pub async fn load(
+        workspace: WeakView<Workspace>,
+        mut cx: AsyncWindowContext,
+    ) -> anyhow::Result<View<Self>> {
+        let serialized_panel = cx
+            .background_executor()
+            .spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) })
+            .await
+            .map_err(|_| anyhow::anyhow!("Failed to read collaboration panel from key value store"))
+            .log_err()
+            .flatten()
+            .map(|panel| serde_json::from_str::<SerializedCollabPanel>(&panel))
+            .transpose()
+            .log_err()
+            .flatten();
+
+        workspace.update(&mut cx, |workspace, cx| {
+            let panel = CollabPanel::new(workspace, cx);
+            if let Some(serialized_panel) = serialized_panel {
+                panel.update(cx, |panel, cx| {
+                    panel.width = serialized_panel.width;
+                    //todo!(collapsed_channels)
+                    // panel.collapsed_channels = serialized_panel
+                    //     .collapsed_channels
+                    //     .unwrap_or_else(|| Vec::new());
+                    cx.notify();
+                });
+            }
+            panel
+        })
+    }
+
+    //     fn serialize(&mut self, cx: &mut ViewContext<Self>) {
+    //         let width = self.width;
+    //         let collapsed_channels = self.collapsed_channels.clone();
+    //         self.pending_serialization = cx.background().spawn(
+    //             async move {
+    //                 KEY_VALUE_STORE
+    //                     .write_kvp(
+    //                         COLLABORATION_PANEL_KEY.into(),
+    //                         serde_json::to_string(&SerializedCollabPanel {
+    //                             width,
+    //                             collapsed_channels: Some(collapsed_channels),
+    //                         })?,
+    //                     )
+    //                     .await?;
+    //                 anyhow::Ok(())
+    //             }
+    //             .log_err(),
+    //         );
+    //     }
+
+    //     fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
+    //         let channel_store = self.channel_store.read(cx);
+    //         let user_store = self.user_store.read(cx);
+    //         let query = self.filter_editor.read(cx).text(cx);
+    //         let executor = cx.background().clone();
+
+    //         let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
+    //         let old_entries = mem::take(&mut self.entries);
+    //         let mut scroll_to_top = false;
+
+    //         if let Some(room) = ActiveCall::global(cx).read(cx).room() {
+    //             self.entries.push(ListEntry::Header(Section::ActiveCall));
+    //             if !old_entries
+    //                 .iter()
+    //                 .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))
+    //             {
+    //                 scroll_to_top = true;
+    //             }
+
+    //             if !self.collapsed_sections.contains(&Section::ActiveCall) {
+    //                 let room = room.read(cx);
+
+    //                 if let Some(channel_id) = room.channel_id() {
+    //                     self.entries.push(ListEntry::ChannelNotes { channel_id });
+    //                     self.entries.push(ListEntry::ChannelChat { channel_id })
+    //                 }
+
+    //                 // Populate the active user.
+    //                 if let Some(user) = user_store.current_user() {
+    //                     self.match_candidates.clear();
+    //                     self.match_candidates.push(StringMatchCandidate {
+    //                         id: 0,
+    //                         string: user.github_login.clone(),
+    //                         char_bag: user.github_login.chars().collect(),
+    //                     });
+    //                     let matches = executor.block(match_strings(
+    //                         &self.match_candidates,
+    //                         &query,
+    //                         true,
+    //                         usize::MAX,
+    //                         &Default::default(),
+    //                         executor.clone(),
+    //                     ));
+    //                     if !matches.is_empty() {
+    //                         let user_id = user.id;
+    //                         self.entries.push(ListEntry::CallParticipant {
+    //                             user,
+    //                             peer_id: None,
+    //                             is_pending: false,
+    //                         });
+    //                         let mut projects = room.local_participant().projects.iter().peekable();
+    //                         while let Some(project) = projects.next() {
+    //                             self.entries.push(ListEntry::ParticipantProject {
+    //                                 project_id: project.id,
+    //                                 worktree_root_names: project.worktree_root_names.clone(),
+    //                                 host_user_id: user_id,
+    //                                 is_last: projects.peek().is_none() && !room.is_screen_sharing(),
+    //                             });
+    //                         }
+    //                         if room.is_screen_sharing() {
+    //                             self.entries.push(ListEntry::ParticipantScreen {
+    //                                 peer_id: None,
+    //                                 is_last: true,
+    //                             });
+    //                         }
+    //                     }
+    //                 }
+
+    //                 // Populate remote participants.
+    //                 self.match_candidates.clear();
+    //                 self.match_candidates
+    //                     .extend(room.remote_participants().iter().map(|(_, participant)| {
+    //                         StringMatchCandidate {
+    //                             id: participant.user.id as usize,
+    //                             string: participant.user.github_login.clone(),
+    //                             char_bag: participant.user.github_login.chars().collect(),
+    //                         }
+    //                     }));
+    //                 let matches = executor.block(match_strings(
+    //                     &self.match_candidates,
+    //                     &query,
+    //                     true,
+    //                     usize::MAX,
+    //                     &Default::default(),
+    //                     executor.clone(),
+    //                 ));
+    //                 for mat in matches {
+    //                     let user_id = mat.candidate_id as u64;
+    //                     let participant = &room.remote_participants()[&user_id];
+    //                     self.entries.push(ListEntry::CallParticipant {
+    //                         user: participant.user.clone(),
+    //                         peer_id: Some(participant.peer_id),
+    //                         is_pending: false,
+    //                     });
+    //                     let mut projects = participant.projects.iter().peekable();
+    //                     while let Some(project) = projects.next() {
+    //                         self.entries.push(ListEntry::ParticipantProject {
+    //                             project_id: project.id,
+    //                             worktree_root_names: project.worktree_root_names.clone(),
+    //                             host_user_id: participant.user.id,
+    //                             is_last: projects.peek().is_none()
+    //                                 && participant.video_tracks.is_empty(),
+    //                         });
+    //                     }
+    //                     if !participant.video_tracks.is_empty() {
+    //                         self.entries.push(ListEntry::ParticipantScreen {
+    //                             peer_id: Some(participant.peer_id),
+    //                             is_last: true,
+    //                         });
+    //                     }
+    //                 }
+
+    //                 // Populate pending participants.
+    //                 self.match_candidates.clear();
+    //                 self.match_candidates
+    //                     .extend(room.pending_participants().iter().enumerate().map(
+    //                         |(id, participant)| StringMatchCandidate {
+    //                             id,
+    //                             string: participant.github_login.clone(),
+    //                             char_bag: participant.github_login.chars().collect(),
+    //                         },
+    //                     ));
+    //                 let matches = executor.block(match_strings(
+    //                     &self.match_candidates,
+    //                     &query,
+    //                     true,
+    //                     usize::MAX,
+    //                     &Default::default(),
+    //                     executor.clone(),
+    //                 ));
+    //                 self.entries
+    //                     .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
+    //                         user: room.pending_participants()[mat.candidate_id].clone(),
+    //                         peer_id: None,
+    //                         is_pending: true,
+    //                     }));
+    //             }
+    //         }
+
+    //         let mut request_entries = Vec::new();
+
+    //         if cx.has_flag::<ChannelsAlpha>() {
+    //             self.entries.push(ListEntry::Header(Section::Channels));
+
+    //             if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
+    //                 self.match_candidates.clear();
+    //                 self.match_candidates
+    //                     .extend(channel_store.ordered_channels().enumerate().map(
+    //                         |(ix, (_, channel))| StringMatchCandidate {
+    //                             id: ix,
+    //                             string: channel.name.clone(),
+    //                             char_bag: channel.name.chars().collect(),
+    //                         },
+    //                     ));
+    //                 let matches = executor.block(match_strings(
+    //                     &self.match_candidates,
+    //                     &query,
+    //                     true,
+    //                     usize::MAX,
+    //                     &Default::default(),
+    //                     executor.clone(),
+    //                 ));
+    //                 if let Some(state) = &self.channel_editing_state {
+    //                     if matches!(state, ChannelEditingState::Create { location: None, .. }) {
+    //                         self.entries.push(ListEntry::ChannelEditor { depth: 0 });
+    //                     }
+    //                 }
+    //                 let mut collapse_depth = None;
+    //                 for mat in matches {
+    //                     let channel = channel_store.channel_at_index(mat.candidate_id).unwrap();
+    //                     let depth = channel.parent_path.len();
+
+    //                     if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
+    //                         collapse_depth = Some(depth);
+    //                     } else if let Some(collapsed_depth) = collapse_depth {
+    //                         if depth > collapsed_depth {
+    //                             continue;
+    //                         }
+    //                         if self.is_channel_collapsed(channel.id) {
+    //                             collapse_depth = Some(depth);
+    //                         } else {
+    //                             collapse_depth = None;
+    //                         }
+    //                     }
+
+    //                     let has_children = channel_store
+    //                         .channel_at_index(mat.candidate_id + 1)
+    //                         .map_or(false, |next_channel| {
+    //                             next_channel.parent_path.ends_with(&[channel.id])
+    //                         });
+
+    //                     match &self.channel_editing_state {
+    //                         Some(ChannelEditingState::Create {
+    //                             location: parent_id,
+    //                             ..
+    //                         }) if *parent_id == Some(channel.id) => {
+    //                             self.entries.push(ListEntry::Channel {
+    //                                 channel: channel.clone(),
+    //                                 depth,
+    //                                 has_children: false,
+    //                             });
+    //                             self.entries
+    //                                 .push(ListEntry::ChannelEditor { depth: depth + 1 });
+    //                         }
+    //                         Some(ChannelEditingState::Rename {
+    //                             location: parent_id,
+    //                             ..
+    //                         }) if parent_id == &channel.id => {
+    //                             self.entries.push(ListEntry::ChannelEditor { depth });
+    //                         }
+    //                         _ => {
+    //                             self.entries.push(ListEntry::Channel {
+    //                                 channel: channel.clone(),
+    //                                 depth,
+    //                                 has_children,
+    //                             });
+    //                         }
+    //                     }
+    //                 }
+    //             }
+
+    //             let channel_invites = channel_store.channel_invitations();
+    //             if !channel_invites.is_empty() {
+    //                 self.match_candidates.clear();
+    //                 self.match_candidates
+    //                     .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
+    //                         StringMatchCandidate {
+    //                             id: ix,
+    //                             string: channel.name.clone(),
+    //                             char_bag: channel.name.chars().collect(),
+    //                         }
+    //                     }));
+    //                 let matches = executor.block(match_strings(
+    //                     &self.match_candidates,
+    //                     &query,
+    //                     true,
+    //                     usize::MAX,
+    //                     &Default::default(),
+    //                     executor.clone(),
+    //                 ));
+    //                 request_entries.extend(matches.iter().map(|mat| {
+    //                     ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
+    //                 }));
+
+    //                 if !request_entries.is_empty() {
+    //                     self.entries
+    //                         .push(ListEntry::Header(Section::ChannelInvites));
+    //                     if !self.collapsed_sections.contains(&Section::ChannelInvites) {
+    //                         self.entries.append(&mut request_entries);
+    //                     }
+    //                 }
+    //             }
+    //         }
+
+    //         self.entries.push(ListEntry::Header(Section::Contacts));
+
+    //         request_entries.clear();
+    //         let incoming = user_store.incoming_contact_requests();
+    //         if !incoming.is_empty() {
+    //             self.match_candidates.clear();
+    //             self.match_candidates
+    //                 .extend(
+    //                     incoming
+    //                         .iter()
+    //                         .enumerate()
+    //                         .map(|(ix, user)| StringMatchCandidate {
+    //                             id: ix,
+    //                             string: user.github_login.clone(),
+    //                             char_bag: user.github_login.chars().collect(),
+    //                         }),
+    //                 );
+    //             let matches = executor.block(match_strings(
+    //                 &self.match_candidates,
+    //                 &query,
+    //                 true,
+    //                 usize::MAX,
+    //                 &Default::default(),
+    //                 executor.clone(),
+    //             ));
+    //             request_entries.extend(
+    //                 matches
+    //                     .iter()
+    //                     .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
+    //             );
+    //         }
+
+    //         let outgoing = user_store.outgoing_contact_requests();
+    //         if !outgoing.is_empty() {
+    //             self.match_candidates.clear();
+    //             self.match_candidates
+    //                 .extend(
+    //                     outgoing
+    //                         .iter()
+    //                         .enumerate()
+    //                         .map(|(ix, user)| StringMatchCandidate {
+    //                             id: ix,
+    //                             string: user.github_login.clone(),
+    //                             char_bag: user.github_login.chars().collect(),
+    //                         }),
+    //                 );
+    //             let matches = executor.block(match_strings(
+    //                 &self.match_candidates,
+    //                 &query,
+    //                 true,
+    //                 usize::MAX,
+    //                 &Default::default(),
+    //                 executor.clone(),
+    //             ));
+    //             request_entries.extend(
+    //                 matches
+    //                     .iter()
+    //                     .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
+    //             );
+    //         }
+
+    //         if !request_entries.is_empty() {
+    //             self.entries
+    //                 .push(ListEntry::Header(Section::ContactRequests));
+    //             if !self.collapsed_sections.contains(&Section::ContactRequests) {
+    //                 self.entries.append(&mut request_entries);
+    //             }
+    //         }
+
+    //         let contacts = user_store.contacts();
+    //         if !contacts.is_empty() {
+    //             self.match_candidates.clear();
+    //             self.match_candidates
+    //                 .extend(
+    //                     contacts
+    //                         .iter()
+    //                         .enumerate()
+    //                         .map(|(ix, contact)| StringMatchCandidate {
+    //                             id: ix,
+    //                             string: contact.user.github_login.clone(),
+    //                             char_bag: contact.user.github_login.chars().collect(),
+    //                         }),
+    //                 );
+
+    //             let matches = executor.block(match_strings(
+    //                 &self.match_candidates,
+    //                 &query,
+    //                 true,
+    //                 usize::MAX,
+    //                 &Default::default(),
+    //                 executor.clone(),
+    //             ));
+
+    //             let (online_contacts, offline_contacts) = matches
+    //                 .iter()
+    //                 .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
+
+    //             for (matches, section) in [
+    //                 (online_contacts, Section::Online),
+    //                 (offline_contacts, Section::Offline),
+    //             ] {
+    //                 if !matches.is_empty() {
+    //                     self.entries.push(ListEntry::Header(section));
+    //                     if !self.collapsed_sections.contains(&section) {
+    //                         let active_call = &ActiveCall::global(cx).read(cx);
+    //                         for mat in matches {
+    //                             let contact = &contacts[mat.candidate_id];
+    //                             self.entries.push(ListEntry::Contact {
+    //                                 contact: contact.clone(),
+    //                                 calling: active_call.pending_invites().contains(&contact.user.id),
+    //                             });
+    //                         }
+    //                     }
+    //                 }
+    //             }
+    //         }
+
+    //         if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() {
+    //             self.entries.push(ListEntry::ContactPlaceholder);
+    //         }
+
+    //         if select_same_item {
+    //             if let Some(prev_selected_entry) = prev_selected_entry {
+    //                 self.selection.take();
+    //                 for (ix, entry) in self.entries.iter().enumerate() {
+    //                     if *entry == prev_selected_entry {
+    //                         self.selection = Some(ix);
+    //                         break;
+    //                     }
+    //                 }
+    //             }
+    //         } else {
+    //             self.selection = self.selection.and_then(|prev_selection| {
+    //                 if self.entries.is_empty() {
+    //                     None
+    //                 } else {
+    //                     Some(prev_selection.min(self.entries.len() - 1))
+    //                 }
+    //             });
+    //         }
+
+    //         let old_scroll_top = self.list_state.logical_scroll_top();
+
+    //         self.list_state.reset(self.entries.len());
+
+    //         if scroll_to_top {
+    //             self.list_state.scroll_to(ListOffset::default());
+    //         } else {
+    //             // Attempt to maintain the same scroll position.
+    //             if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
+    //                 let new_scroll_top = self
+    //                     .entries
+    //                     .iter()
+    //                     .position(|entry| entry == old_top_entry)
+    //                     .map(|item_ix| ListOffset {
+    //                         item_ix,
+    //                         offset_in_item: old_scroll_top.offset_in_item,
+    //                     })
+    //                     .or_else(|| {
+    //                         let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
+    //                         let item_ix = self
+    //                             .entries
+    //                             .iter()
+    //                             .position(|entry| entry == entry_after_old_top)?;
+    //                         Some(ListOffset {
+    //                             item_ix,
+    //                             offset_in_item: 0.,
+    //                         })
+    //                     })
+    //                     .or_else(|| {
+    //                         let entry_before_old_top =
+    //                             old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
+    //                         let item_ix = self
+    //                             .entries
+    //                             .iter()
+    //                             .position(|entry| entry == entry_before_old_top)?;
+    //                         Some(ListOffset {
+    //                             item_ix,
+    //                             offset_in_item: 0.,
+    //                         })
+    //                     });
+
+    //                 self.list_state
+    //                     .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
+    //             }
+    //         }
+
+    //         cx.notify();
+    //     }
+
+    //     fn render_call_participant(
+    //         user: &User,
+    //         peer_id: Option<PeerId>,
+    //         user_store: ModelHandle<UserStore>,
+    //         is_pending: bool,
+    //         is_selected: bool,
+    //         theme: &theme::Theme,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> AnyElement<Self> {
+    //         enum CallParticipant {}
+    //         enum CallParticipantTooltip {}
+    //         enum LeaveCallButton {}
+    //         enum LeaveCallTooltip {}
+
+    //         let collab_theme = &theme.collab_panel;
+
+    //         let is_current_user =
+    //             user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
+
+    //         let content = MouseEventHandler::new::<CallParticipant, _>(
+    //             user.id as usize,
+    //             cx,
+    //             |mouse_state, cx| {
+    //                 let style = if is_current_user {
+    //                     *collab_theme
+    //                         .contact_row
+    //                         .in_state(is_selected)
+    //                         .style_for(&mut Default::default())
+    //                 } else {
+    //                     *collab_theme
+    //                         .contact_row
+    //                         .in_state(is_selected)
+    //                         .style_for(mouse_state)
+    //                 };
+
+    //                 Flex::row()
+    //                     .with_children(user.avatar.clone().map(|avatar| {
+    //                         Image::from_data(avatar)
+    //                             .with_style(collab_theme.contact_avatar)
+    //                             .aligned()
+    //                             .left()
+    //                     }))
+    //                     .with_child(
+    //                         Label::new(
+    //                             user.github_login.clone(),
+    //                             collab_theme.contact_username.text.clone(),
+    //                         )
+    //                         .contained()
+    //                         .with_style(collab_theme.contact_username.container)
+    //                         .aligned()
+    //                         .left()
+    //                         .flex(1., true),
+    //                     )
+    //                     .with_children(if is_pending {
+    //                         Some(
+    //                             Label::new("Calling", collab_theme.calling_indicator.text.clone())
+    //                                 .contained()
+    //                                 .with_style(collab_theme.calling_indicator.container)
+    //                                 .aligned()
+    //                                 .into_any(),
+    //                         )
+    //                     } else if is_current_user {
+    //                         Some(
+    //                             MouseEventHandler::new::<LeaveCallButton, _>(0, cx, |state, _| {
+    //                                 render_icon_button(
+    //                                     theme
+    //                                         .collab_panel
+    //                                         .leave_call_button
+    //                                         .style_for(is_selected, state),
+    //                                     "icons/exit.svg",
+    //                                 )
+    //                             })
+    //                             .with_cursor_style(CursorStyle::PointingHand)
+    //                             .on_click(MouseButton::Left, |_, _, cx| {
+    //                                 Self::leave_call(cx);
+    //                             })
+    //                             .with_tooltip::<LeaveCallTooltip>(
+    //                                 0,
+    //                                 "Leave call",
+    //                                 None,
+    //                                 theme.tooltip.clone(),
+    //                                 cx,
+    //                             )
+    //                             .into_any(),
+    //                         )
+    //                     } else {
+    //                         None
+    //                     })
+    //                     .constrained()
+    //                     .with_height(collab_theme.row_height)
+    //                     .contained()
+    //                     .with_style(style)
+    //             },
+    //         );
+
+    //         if is_current_user || is_pending || peer_id.is_none() {
+    //             return content.into_any();
+    //         }
+
+    //         let tooltip = format!("Follow {}", user.github_login);
+
+    //         content
+    //             .on_click(MouseButton::Left, move |_, this, cx| {
+    //                 if let Some(workspace) = this.workspace.upgrade(cx) {
+    //                     workspace
+    //                         .update(cx, |workspace, cx| workspace.follow(peer_id.unwrap(), cx))
+    //                         .map(|task| task.detach_and_log_err(cx));
+    //                 }
+    //             })
+    //             .with_cursor_style(CursorStyle::PointingHand)
+    //             .with_tooltip::<CallParticipantTooltip>(
+    //                 user.id as usize,
+    //                 tooltip,
+    //                 Some(Box::new(FollowNextCollaborator)),
+    //                 theme.tooltip.clone(),
+    //                 cx,
+    //             )
+    //             .into_any()
+    //     }
+
+    //     fn render_participant_project(
+    //         project_id: u64,
+    //         worktree_root_names: &[String],
+    //         host_user_id: u64,
+    //         is_current: bool,
+    //         is_last: bool,
+    //         is_selected: bool,
+    //         theme: &theme::Theme,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> AnyElement<Self> {
+    //         enum JoinProject {}
+    //         enum JoinProjectTooltip {}
+
+    //         let collab_theme = &theme.collab_panel;
+    //         let host_avatar_width = collab_theme
+    //             .contact_avatar
+    //             .width
+    //             .or(collab_theme.contact_avatar.height)
+    //             .unwrap_or(0.);
+    //         let tree_branch = collab_theme.tree_branch;
+    //         let project_name = if worktree_root_names.is_empty() {
+    //             "untitled".to_string()
+    //         } else {
+    //             worktree_root_names.join(", ")
+    //         };
+
+    //         let content =
+    //             MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
+    //                 let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
+    //                 let row = if is_current {
+    //                     collab_theme
+    //                         .project_row
+    //                         .in_state(true)
+    //                         .style_for(&mut Default::default())
+    //                 } else {
+    //                     collab_theme
+    //                         .project_row
+    //                         .in_state(is_selected)
+    //                         .style_for(mouse_state)
+    //                 };
+
+    //                 Flex::row()
+    //                     .with_child(render_tree_branch(
+    //                         tree_branch,
+    //                         &row.name.text,
+    //                         is_last,
+    //                         vec2f(host_avatar_width, collab_theme.row_height),
+    //                         cx.font_cache(),
+    //                     ))
+    //                     .with_child(
+    //                         Svg::new("icons/file_icons/folder.svg")
+    //                             .with_color(collab_theme.channel_hash.color)
+    //                             .constrained()
+    //                             .with_width(collab_theme.channel_hash.width)
+    //                             .aligned()
+    //                             .left(),
+    //                     )
+    //                     .with_child(
+    //                         Label::new(project_name.clone(), row.name.text.clone())
+    //                             .aligned()
+    //                             .left()
+    //                             .contained()
+    //                             .with_style(row.name.container)
+    //                             .flex(1., false),
+    //                     )
+    //                     .constrained()
+    //                     .with_height(collab_theme.row_height)
+    //                     .contained()
+    //                     .with_style(row.container)
+    //             });
+
+    //         if is_current {
+    //             return content.into_any();
+    //         }
+
+    //         content
+    //             .with_cursor_style(CursorStyle::PointingHand)
+    //             .on_click(MouseButton::Left, move |_, this, cx| {
+    //                 if let Some(workspace) = this.workspace.upgrade(cx) {
+    //                     let app_state = workspace.read(cx).app_state().clone();
+    //                     workspace::join_remote_project(project_id, host_user_id, app_state, cx)
+    //                         .detach_and_log_err(cx);
+    //                 }
+    //             })
+    //             .with_tooltip::<JoinProjectTooltip>(
+    //                 project_id as usize,
+    //                 format!("Open {}", project_name),
+    //                 None,
+    //                 theme.tooltip.clone(),
+    //                 cx,
+    //             )
+    //             .into_any()
+    //     }
+
+    //     fn render_participant_screen(
+    //         peer_id: Option<PeerId>,
+    //         is_last: bool,
+    //         is_selected: bool,
+    //         theme: &theme::CollabPanel,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> AnyElement<Self> {
+    //         enum OpenSharedScreen {}
+
+    //         let host_avatar_width = theme
+    //             .contact_avatar
+    //             .width
+    //             .or(theme.contact_avatar.height)
+    //             .unwrap_or(0.);
+    //         let tree_branch = theme.tree_branch;
+
+    //         let handler = MouseEventHandler::new::<OpenSharedScreen, _>(
+    //             peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize,
+    //             cx,
+    //             |mouse_state, cx| {
+    //                 let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
+    //                 let row = theme
+    //                     .project_row
+    //                     .in_state(is_selected)
+    //                     .style_for(mouse_state);
+
+    //                 Flex::row()
+    //                     .with_child(render_tree_branch(
+    //                         tree_branch,
+    //                         &row.name.text,
+    //                         is_last,
+    //                         vec2f(host_avatar_width, theme.row_height),
+    //                         cx.font_cache(),
+    //                     ))
+    //                     .with_child(
+    //                         Svg::new("icons/desktop.svg")
+    //                             .with_color(theme.channel_hash.color)
+    //                             .constrained()
+    //                             .with_width(theme.channel_hash.width)
+    //                             .aligned()
+    //                             .left(),
+    //                     )
+    //                     .with_child(
+    //                         Label::new("Screen", row.name.text.clone())
+    //                             .aligned()
+    //                             .left()
+    //                             .contained()
+    //                             .with_style(row.name.container)
+    //                             .flex(1., false),
+    //                     )
+    //                     .constrained()
+    //                     .with_height(theme.row_height)
+    //                     .contained()
+    //                     .with_style(row.container)
+    //             },
+    //         );
+    //         if peer_id.is_none() {
+    //             return handler.into_any();
+    //         }
+    //         handler
+    //             .with_cursor_style(CursorStyle::PointingHand)
+    //             .on_click(MouseButton::Left, move |_, this, cx| {
+    //                 if let Some(workspace) = this.workspace.upgrade(cx) {
+    //                     workspace.update(cx, |workspace, cx| {
+    //                         workspace.open_shared_screen(peer_id.unwrap(), cx)
+    //                     });
+    //                 }
+    //             })
+    //             .into_any()
+    //     }
+
+    //     fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
+    //         if let Some(_) = self.channel_editing_state.take() {
+    //             self.channel_name_editor.update(cx, |editor, cx| {
+    //                 editor.set_text("", cx);
+    //             });
+    //             true
+    //         } else {
+    //             false
+    //         }
+    //     }
+
+    //     fn render_header(
+    //         &self,
+    //         section: Section,
+    //         theme: &theme::Theme,
+    //         is_selected: bool,
+    //         is_collapsed: bool,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> AnyElement<Self> {
+    //         enum Header {}
+    //         enum LeaveCallContactList {}
+    //         enum AddChannel {}
+
+    //         let tooltip_style = &theme.tooltip;
+    //         let mut channel_link = None;
+    //         let mut channel_tooltip_text = None;
+    //         let mut channel_icon = None;
+    //         let mut is_dragged_over = false;
+
+    //         let text = match section {
+    //             Section::ActiveCall => {
+    //                 let channel_name = maybe!({
+    //                     let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
+
+    //                     let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
+
+    //                     channel_link = Some(channel.link());
+    //                     (channel_icon, channel_tooltip_text) = match channel.visibility {
+    //                         proto::ChannelVisibility::Public => {
+    //                             (Some("icons/public.svg"), Some("Copy public channel link."))
+    //                         }
+    //                         proto::ChannelVisibility::Members => {
+    //                             (Some("icons/hash.svg"), Some("Copy private channel link."))
+    //                         }
+    //                     };
+
+    //                     Some(channel.name.as_str())
+    //                 });
+
+    //                 if let Some(name) = channel_name {
+    //                     Cow::Owned(format!("{}", name))
+    //                 } else {
+    //                     Cow::Borrowed("Current Call")
+    //                 }
+    //             }
+    //             Section::ContactRequests => Cow::Borrowed("Requests"),
+    //             Section::Contacts => Cow::Borrowed("Contacts"),
+    //             Section::Channels => Cow::Borrowed("Channels"),
+    //             Section::ChannelInvites => Cow::Borrowed("Invites"),
+    //             Section::Online => Cow::Borrowed("Online"),
+    //             Section::Offline => Cow::Borrowed("Offline"),
+    //         };
+
+    //         enum AddContact {}
+    //         let button = match section {
+    //             Section::ActiveCall => channel_link.map(|channel_link| {
+    //                 let channel_link_copy = channel_link.clone();
+    //                 MouseEventHandler::new::<AddContact, _>(0, cx, |state, _| {
+    //                     render_icon_button(
+    //                         theme
+    //                             .collab_panel
+    //                             .leave_call_button
+    //                             .style_for(is_selected, state),
+    //                         "icons/link.svg",
+    //                     )
+    //                 })
+    //                 .with_cursor_style(CursorStyle::PointingHand)
+    //                 .on_click(MouseButton::Left, move |_, _, cx| {
+    //                     let item = ClipboardItem::new(channel_link_copy.clone());
+    //                     cx.write_to_clipboard(item)
+    //                 })
+    //                 .with_tooltip::<AddContact>(
+    //                     0,
+    //                     channel_tooltip_text.unwrap(),
+    //                     None,
+    //                     tooltip_style.clone(),
+    //                     cx,
+    //                 )
+    //             }),
+    //             Section::Contacts => Some(
+    //                 MouseEventHandler::new::<LeaveCallContactList, _>(0, cx, |state, _| {
+    //                     render_icon_button(
+    //                         theme
+    //                             .collab_panel
+    //                             .add_contact_button
+    //                             .style_for(is_selected, state),
+    //                         "icons/plus.svg",
+    //                     )
+    //                 })
+    //                 .with_cursor_style(CursorStyle::PointingHand)
+    //                 .on_click(MouseButton::Left, |_, this, cx| {
+    //                     this.toggle_contact_finder(cx);
+    //                 })
+    //                 .with_tooltip::<LeaveCallContactList>(
+    //                     0,
+    //                     "Search for new contact",
+    //                     None,
+    //                     tooltip_style.clone(),
+    //                     cx,
+    //                 ),
+    //             ),
+    //             Section::Channels => {
+    //                 if cx
+    //                     .global::<DragAndDrop<Workspace>>()
+    //                     .currently_dragged::<Channel>(cx.window())
+    //                     .is_some()
+    //                     && self.drag_target_channel == ChannelDragTarget::Root
+    //                 {
+    //                     is_dragged_over = true;
+    //                 }
+
+    //                 Some(
+    //                     MouseEventHandler::new::<AddChannel, _>(0, cx, |state, _| {
+    //                         render_icon_button(
+    //                             theme
+    //                                 .collab_panel
+    //                                 .add_contact_button
+    //                                 .style_for(is_selected, state),
+    //                             "icons/plus.svg",
+    //                         )
+    //                     })
+    //                     .with_cursor_style(CursorStyle::PointingHand)
+    //                     .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx))
+    //                     .with_tooltip::<AddChannel>(
+    //                         0,
+    //                         "Create a channel",
+    //                         None,
+    //                         tooltip_style.clone(),
+    //                         cx,
+    //                     ),
+    //                 )
+    //             }
+    //             _ => None,
+    //         };
+
+    //         let can_collapse = match section {
+    //             Section::ActiveCall | Section::Channels | Section::Contacts => false,
+    //             Section::ChannelInvites
+    //             | Section::ContactRequests
+    //             | Section::Online
+    //             | Section::Offline => true,
+    //         };
+    //         let icon_size = (&theme.collab_panel).section_icon_size;
+    //         let mut result = MouseEventHandler::new::<Header, _>(section as usize, cx, |state, _| {
+    //             let header_style = if can_collapse {
+    //                 theme
+    //                     .collab_panel
+    //                     .subheader_row
+    //                     .in_state(is_selected)
+    //                     .style_for(state)
+    //             } else {
+    //                 &theme.collab_panel.header_row
+    //             };
+
+    //             Flex::row()
+    //                 .with_children(if can_collapse {
+    //                     Some(
+    //                         Svg::new(if is_collapsed {
+    //                             "icons/chevron_right.svg"
+    //                         } else {
+    //                             "icons/chevron_down.svg"
+    //                         })
+    //                         .with_color(header_style.text.color)
+    //                         .constrained()
+    //                         .with_max_width(icon_size)
+    //                         .with_max_height(icon_size)
+    //                         .aligned()
+    //                         .constrained()
+    //                         .with_width(icon_size)
+    //                         .contained()
+    //                         .with_margin_right(
+    //                             theme.collab_panel.contact_username.container.margin.left,
+    //                         ),
+    //                     )
+    //                 } else if let Some(channel_icon) = channel_icon {
+    //                     Some(
+    //                         Svg::new(channel_icon)
+    //                             .with_color(header_style.text.color)
+    //                             .constrained()
+    //                             .with_max_width(icon_size)
+    //                             .with_max_height(icon_size)
+    //                             .aligned()
+    //                             .constrained()
+    //                             .with_width(icon_size)
+    //                             .contained()
+    //                             .with_margin_right(
+    //                                 theme.collab_panel.contact_username.container.margin.left,
+    //                             ),
+    //                     )
+    //                 } else {
+    //                     None
+    //                 })
+    //                 .with_child(
+    //                     Label::new(text, header_style.text.clone())
+    //                         .aligned()
+    //                         .left()
+    //                         .flex(1., true),
+    //                 )
+    //                 .with_children(button.map(|button| button.aligned().right()))
+    //                 .constrained()
+    //                 .with_height(theme.collab_panel.row_height)
+    //                 .contained()
+    //                 .with_style(if is_dragged_over {
+    //                     theme.collab_panel.dragged_over_header
+    //                 } else {
+    //                     header_style.container
+    //                 })
+    //         });
+
+    //         result = result
+    //             .on_move(move |_, this, cx| {
+    //                 if cx
+    //                     .global::<DragAndDrop<Workspace>>()
+    //                     .currently_dragged::<Channel>(cx.window())
+    //                     .is_some()
+    //                 {
+    //                     this.drag_target_channel = ChannelDragTarget::Root;
+    //                     cx.notify()
+    //                 }
+    //             })
+    //             .on_up(MouseButton::Left, move |_, this, cx| {
+    //                 if let Some((_, dragged_channel)) = cx
+    //                     .global::<DragAndDrop<Workspace>>()
+    //                     .currently_dragged::<Channel>(cx.window())
+    //                 {
+    //                     this.channel_store
+    //                         .update(cx, |channel_store, cx| {
+    //                             channel_store.move_channel(dragged_channel.id, None, cx)
+    //                         })
+    //                         .detach_and_log_err(cx)
+    //                 }
+    //             });
+
+    //         if can_collapse {
+    //             result = result
+    //                 .with_cursor_style(CursorStyle::PointingHand)
+    //                 .on_click(MouseButton::Left, move |_, this, cx| {
+    //                     if can_collapse {
+    //                         this.toggle_section_expanded(section, cx);
+    //                     }
+    //                 })
+    //         }
+
+    //         result.into_any()
+    //     }
+
+    //     fn render_contact(
+    //         contact: &Contact,
+    //         calling: bool,
+    //         project: &ModelHandle<Project>,
+    //         theme: &theme::Theme,
+    //         is_selected: bool,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> AnyElement<Self> {
+    //         enum ContactTooltip {}
+
+    //         let collab_theme = &theme.collab_panel;
+    //         let online = contact.online;
+    //         let busy = contact.busy || calling;
+    //         let user_id = contact.user.id;
+    //         let github_login = contact.user.github_login.clone();
+    //         let initial_project = project.clone();
+
+    //         let event_handler =
+    //             MouseEventHandler::new::<Contact, _>(contact.user.id as usize, cx, |state, cx| {
+    //                 Flex::row()
+    //                     .with_children(contact.user.avatar.clone().map(|avatar| {
+    //                         let status_badge = if contact.online {
+    //                             Some(
+    //                                 Empty::new()
+    //                                     .collapsed()
+    //                                     .contained()
+    //                                     .with_style(if busy {
+    //                                         collab_theme.contact_status_busy
+    //                                     } else {
+    //                                         collab_theme.contact_status_free
+    //                                     })
+    //                                     .aligned(),
+    //                             )
+    //                         } else {
+    //                             None
+    //                         };
+    //                         Stack::new()
+    //                             .with_child(
+    //                                 Image::from_data(avatar)
+    //                                     .with_style(collab_theme.contact_avatar)
+    //                                     .aligned()
+    //                                     .left(),
+    //                             )
+    //                             .with_children(status_badge)
+    //                     }))
+    //                     .with_child(
+    //                         Label::new(
+    //                             contact.user.github_login.clone(),
+    //                             collab_theme.contact_username.text.clone(),
+    //                         )
+    //                         .contained()
+    //                         .with_style(collab_theme.contact_username.container)
+    //                         .aligned()
+    //                         .left()
+    //                         .flex(1., true),
+    //                     )
+    //                     .with_children(if state.hovered() {
+    //                         Some(
+    //                             MouseEventHandler::new::<Cancel, _>(
+    //                                 contact.user.id as usize,
+    //                                 cx,
+    //                                 |mouse_state, _| {
+    //                                     let button_style =
+    //                                         collab_theme.contact_button.style_for(mouse_state);
+    //                                     render_icon_button(button_style, "icons/x.svg")
+    //                                         .aligned()
+    //                                         .flex_float()
+    //                                 },
+    //                             )
+    //                             .with_padding(Padding::uniform(2.))
+    //                             .with_cursor_style(CursorStyle::PointingHand)
+    //                             .on_click(MouseButton::Left, move |_, this, cx| {
+    //                                 this.remove_contact(user_id, &github_login, cx);
+    //                             })
+    //                             .flex_float(),
+    //                         )
+    //                     } else {
+    //                         None
+    //                     })
+    //                     .with_children(if calling {
+    //                         Some(
+    //                             Label::new("Calling", collab_theme.calling_indicator.text.clone())
+    //                                 .contained()
+    //                                 .with_style(collab_theme.calling_indicator.container)
+    //                                 .aligned(),
+    //                         )
+    //                     } else {
+    //                         None
+    //                     })
+    //                     .constrained()
+    //                     .with_height(collab_theme.row_height)
+    //                     .contained()
+    //                     .with_style(
+    //                         *collab_theme
+    //                             .contact_row
+    //                             .in_state(is_selected)
+    //                             .style_for(state),
+    //                     )
+    //             });
+
+    //         if online && !busy {
+    //             let room = ActiveCall::global(cx).read(cx).room();
+    //             let label = if room.is_some() {
+    //                 format!("Invite {} to join call", contact.user.github_login)
+    //             } else {
+    //                 format!("Call {}", contact.user.github_login)
+    //             };
+
+    //             event_handler
+    //                 .on_click(MouseButton::Left, move |_, this, cx| {
+    //                     this.call(user_id, Some(initial_project.clone()), cx);
+    //                 })
+    //                 .with_cursor_style(CursorStyle::PointingHand)
+    //                 .with_tooltip::<ContactTooltip>(
+    //                     contact.user.id as usize,
+    //                     label,
+    //                     None,
+    //                     theme.tooltip.clone(),
+    //                     cx,
+    //                 )
+    //                 .into_any()
+    //         } else {
+    //             event_handler
+    //                 .with_tooltip::<ContactTooltip>(
+    //                     contact.user.id as usize,
+    //                     format!(
+    //                         "{} is {}",
+    //                         contact.user.github_login,
+    //                         if busy { "on a call" } else { "offline" }
+    //                     ),
+    //                     None,
+    //                     theme.tooltip.clone(),
+    //                     cx,
+    //                 )
+    //                 .into_any()
+    //         }
+    //     }
+
+    //     fn render_contact_placeholder(
+    //         &self,
+    //         theme: &theme::CollabPanel,
+    //         is_selected: bool,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> AnyElement<Self> {
+    //         enum AddContacts {}
+    //         MouseEventHandler::new::<AddContacts, _>(0, cx, |state, _| {
+    //             let style = theme.list_empty_state.style_for(is_selected, state);
+    //             Flex::row()
+    //                 .with_child(
+    //                     Svg::new("icons/plus.svg")
+    //                         .with_color(theme.list_empty_icon.color)
+    //                         .constrained()
+    //                         .with_width(theme.list_empty_icon.width)
+    //                         .aligned()
+    //                         .left(),
+    //                 )
+    //                 .with_child(
+    //                     Label::new("Add a contact", style.text.clone())
+    //                         .contained()
+    //                         .with_style(theme.list_empty_label_container),
+    //                 )
+    //                 .align_children_center()
+    //                 .contained()
+    //                 .with_style(style.container)
+    //                 .into_any()
+    //         })
+    //         .on_click(MouseButton::Left, |_, this, cx| {
+    //             this.toggle_contact_finder(cx);
+    //         })
+    //         .into_any()
+    //     }
+
+    //     fn render_channel_editor(
+    //         &self,
+    //         theme: &theme::Theme,
+    //         depth: usize,
+    //         cx: &AppContext,
+    //     ) -> AnyElement<Self> {
+    //         Flex::row()
+    //             .with_child(
+    //                 Empty::new()
+    //                     .constrained()
+    //                     .with_width(theme.collab_panel.disclosure.button_space()),
+    //             )
+    //             .with_child(
+    //                 Svg::new("icons/hash.svg")
+    //                     .with_color(theme.collab_panel.channel_hash.color)
+    //                     .constrained()
+    //                     .with_width(theme.collab_panel.channel_hash.width)
+    //                     .aligned()
+    //                     .left(),
+    //             )
+    //             .with_child(
+    //                 if let Some(pending_name) = self
+    //                     .channel_editing_state
+    //                     .as_ref()
+    //                     .and_then(|state| state.pending_name())
+    //                 {
+    //                     Label::new(
+    //                         pending_name.to_string(),
+    //                         theme.collab_panel.contact_username.text.clone(),
+    //                     )
+    //                     .contained()
+    //                     .with_style(theme.collab_panel.contact_username.container)
+    //                     .aligned()
+    //                     .left()
+    //                     .flex(1., true)
+    //                     .into_any()
+    //                 } else {
+    //                     ChildView::new(&self.channel_name_editor, cx)
+    //                         .aligned()
+    //                         .left()
+    //                         .contained()
+    //                         .with_style(theme.collab_panel.channel_editor)
+    //                         .flex(1.0, true)
+    //                         .into_any()
+    //                 },
+    //             )
+    //             .align_children_center()
+    //             .constrained()
+    //             .with_height(theme.collab_panel.row_height)
+    //             .contained()
+    //             .with_style(ContainerStyle {
+    //                 background_color: Some(theme.editor.background),
+    //                 ..*theme.collab_panel.contact_row.default_style()
+    //             })
+    //             .with_padding_left(
+    //                 theme.collab_panel.contact_row.default_style().padding.left
+    //                     + theme.collab_panel.channel_indent * depth as f32,
+    //             )
+    //             .into_any()
+    //     }
+
+    //     fn render_channel(
+    //         &self,
+    //         channel: &Channel,
+    //         depth: usize,
+    //         theme: &theme::Theme,
+    //         is_selected: bool,
+    //         has_children: bool,
+    //         ix: usize,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> AnyElement<Self> {
+    //         let channel_id = channel.id;
+    //         let collab_theme = &theme.collab_panel;
+    //         let is_public = self
+    //             .channel_store
+    //             .read(cx)
+    //             .channel_for_id(channel_id)
+    //             .map(|channel| channel.visibility)
+    //             == Some(proto::ChannelVisibility::Public);
+    //         let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id);
+    //         let disclosed =
+    //             has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
+
+    //         let is_active = maybe!({
+    //             let call_channel = ActiveCall::global(cx)
+    //                 .read(cx)
+    //                 .room()?
+    //                 .read(cx)
+    //                 .channel_id()?;
+    //             Some(call_channel == channel_id)
+    //         })
+    //         .unwrap_or(false);
+
+    //         const FACEPILE_LIMIT: usize = 3;
+
+    //         enum ChannelCall {}
+    //         enum ChannelNote {}
+    //         enum NotesTooltip {}
+    //         enum ChatTooltip {}
+    //         enum ChannelTooltip {}
+
+    //         let mut is_dragged_over = false;
+    //         if cx
+    //             .global::<DragAndDrop<Workspace>>()
+    //             .currently_dragged::<Channel>(cx.window())
+    //             .is_some()
+    //             && self.drag_target_channel == ChannelDragTarget::Channel(channel_id)
+    //         {
+    //             is_dragged_over = true;
+    //         }
+
+    //         let has_messages_notification = channel.unseen_message_id.is_some();
+
+    //         MouseEventHandler::new::<Channel, _>(ix, cx, |state, cx| {
+    //             let row_hovered = state.hovered();
+
+    //             let mut select_state = |interactive: &Interactive<ContainerStyle>| {
+    //                 if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() {
+    //                     interactive.clicked.as_ref().unwrap().clone()
+    //                 } else if state.hovered() || other_selected {
+    //                     interactive
+    //                         .hovered
+    //                         .as_ref()
+    //                         .unwrap_or(&interactive.default)
+    //                         .clone()
+    //                 } else {
+    //                     interactive.default.clone()
+    //                 }
+    //             };
+
+    //             Flex::<Self>::row()
+    //                 .with_child(
+    //                     Svg::new(if is_public {
+    //                         "icons/public.svg"
+    //                     } else {
+    //                         "icons/hash.svg"
+    //                     })
+    //                     .with_color(collab_theme.channel_hash.color)
+    //                     .constrained()
+    //                     .with_width(collab_theme.channel_hash.width)
+    //                     .aligned()
+    //                     .left(),
+    //                 )
+    //                 .with_child({
+    //                     let style = collab_theme.channel_name.inactive_state();
+    //                     Flex::row()
+    //                         .with_child(
+    //                             Label::new(channel.name.clone(), style.text.clone())
+    //                                 .contained()
+    //                                 .with_style(style.container)
+    //                                 .aligned()
+    //                                 .left()
+    //                                 .with_tooltip::<ChannelTooltip>(
+    //                                     ix,
+    //                                     "Join channel",
+    //                                     None,
+    //                                     theme.tooltip.clone(),
+    //                                     cx,
+    //                                 ),
+    //                         )
+    //                         .with_children({
+    //                             let participants =
+    //                                 self.channel_store.read(cx).channel_participants(channel_id);
+
+    //                             if !participants.is_empty() {
+    //                                 let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
+
+    //                                 let result = FacePile::new(collab_theme.face_overlap)
+    //                                     .with_children(
+    //                                         participants
+    //                                             .iter()
+    //                                             .filter_map(|user| {
+    //                                                 Some(
+    //                                                     Image::from_data(user.avatar.clone()?)
+    //                                                         .with_style(collab_theme.channel_avatar),
+    //                                                 )
+    //                                             })
+    //                                             .take(FACEPILE_LIMIT),
+    //                                     )
+    //                                     .with_children((extra_count > 0).then(|| {
+    //                                         Label::new(
+    //                                             format!("+{}", extra_count),
+    //                                             collab_theme.extra_participant_label.text.clone(),
+    //                                         )
+    //                                         .contained()
+    //                                         .with_style(collab_theme.extra_participant_label.container)
+    //                                     }));
+
+    //                                 Some(result)
+    //                             } else {
+    //                                 None
+    //                             }
+    //                         })
+    //                         .with_spacing(8.)
+    //                         .align_children_center()
+    //                         .flex(1., true)
+    //                 })
+    //                 .with_child(
+    //                     MouseEventHandler::new::<ChannelNote, _>(ix, cx, move |mouse_state, _| {
+    //                         let container_style = collab_theme
+    //                             .disclosure
+    //                             .button
+    //                             .style_for(mouse_state)
+    //                             .container;
+
+    //                         if channel.unseen_message_id.is_some() {
+    //                             Svg::new("icons/conversations.svg")
+    //                                 .with_color(collab_theme.channel_note_active_color)
+    //                                 .constrained()
+    //                                 .with_width(collab_theme.channel_hash.width)
+    //                                 .contained()
+    //                                 .with_style(container_style)
+    //                                 .with_uniform_padding(4.)
+    //                                 .into_any()
+    //                         } else if row_hovered {
+    //                             Svg::new("icons/conversations.svg")
+    //                                 .with_color(collab_theme.channel_hash.color)
+    //                                 .constrained()
+    //                                 .with_width(collab_theme.channel_hash.width)
+    //                                 .contained()
+    //                                 .with_style(container_style)
+    //                                 .with_uniform_padding(4.)
+    //                                 .into_any()
+    //                         } else {
+    //                             Empty::new().into_any()
+    //                         }
+    //                     })
+    //                     .on_click(MouseButton::Left, move |_, this, cx| {
+    //                         this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
+    //                     })
+    //                     .with_tooltip::<ChatTooltip>(
+    //                         ix,
+    //                         "Open channel chat",
+    //                         None,
+    //                         theme.tooltip.clone(),
+    //                         cx,
+    //                     )
+    //                     .contained()
+    //                     .with_margin_right(4.),
+    //                 )
+    //                 .with_child(
+    //                     MouseEventHandler::new::<ChannelCall, _>(ix, cx, move |mouse_state, cx| {
+    //                         let container_style = collab_theme
+    //                             .disclosure
+    //                             .button
+    //                             .style_for(mouse_state)
+    //                             .container;
+    //                         if row_hovered || channel.unseen_note_version.is_some() {
+    //                             Svg::new("icons/file.svg")
+    //                                 .with_color(if channel.unseen_note_version.is_some() {
+    //                                     collab_theme.channel_note_active_color
+    //                                 } else {
+    //                                     collab_theme.channel_hash.color
+    //                                 })
+    //                                 .constrained()
+    //                                 .with_width(collab_theme.channel_hash.width)
+    //                                 .contained()
+    //                                 .with_style(container_style)
+    //                                 .with_uniform_padding(4.)
+    //                                 .with_margin_right(collab_theme.channel_hash.container.margin.left)
+    //                                 .with_tooltip::<NotesTooltip>(
+    //                                     ix as usize,
+    //                                     "Open channel notes",
+    //                                     None,
+    //                                     theme.tooltip.clone(),
+    //                                     cx,
+    //                                 )
+    //                                 .into_any()
+    //                         } else if has_messages_notification {
+    //                             Empty::new()
+    //                                 .constrained()
+    //                                 .with_width(collab_theme.channel_hash.width)
+    //                                 .contained()
+    //                                 .with_uniform_padding(4.)
+    //                                 .with_margin_right(collab_theme.channel_hash.container.margin.left)
+    //                                 .into_any()
+    //                         } else {
+    //                             Empty::new().into_any()
+    //                         }
+    //                     })
+    //                     .on_click(MouseButton::Left, move |_, this, cx| {
+    //                         this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
+    //                     }),
+    //                 )
+    //                 .align_children_center()
+    //                 .styleable_component()
+    //                 .disclosable(
+    //                     disclosed,
+    //                     Box::new(ToggleCollapse {
+    //                         location: channel.id.clone(),
+    //                     }),
+    //                 )
+    //                 .with_id(ix)
+    //                 .with_style(collab_theme.disclosure.clone())
+    //                 .element()
+    //                 .constrained()
+    //                 .with_height(collab_theme.row_height)
+    //                 .contained()
+    //                 .with_style(select_state(
+    //                     collab_theme
+    //                         .channel_row
+    //                         .in_state(is_selected || is_active || is_dragged_over),
+    //                 ))
+    //                 .with_padding_left(
+    //                     collab_theme.channel_row.default_style().padding.left
+    //                         + collab_theme.channel_indent * depth as f32,
+    //                 )
+    //         })
+    //         .on_click(MouseButton::Left, move |_, this, cx| {
+    //             if this.drag_target_channel == ChannelDragTarget::None {
+    //                 if is_active {
+    //                     this.open_channel_notes(&OpenChannelNotes { channel_id }, cx)
+    //                 } else {
+    //                     this.join_channel(channel_id, cx)
+    //                 }
+    //             }
+    //         })
+    //         .on_click(MouseButton::Right, {
+    //             let channel = channel.clone();
+    //             move |e, this, cx| {
+    //                 this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx);
+    //             }
+    //         })
+    //         .on_up(MouseButton::Left, move |_, this, cx| {
+    //             if let Some((_, dragged_channel)) = cx
+    //                 .global::<DragAndDrop<Workspace>>()
+    //                 .currently_dragged::<Channel>(cx.window())
+    //             {
+    //                 this.channel_store
+    //                     .update(cx, |channel_store, cx| {
+    //                         channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
+    //                     })
+    //                     .detach_and_log_err(cx)
+    //             }
+    //         })
+    //         .on_move({
+    //             let channel = channel.clone();
+    //             move |_, this, cx| {
+    //                 if let Some((_, dragged_channel)) = cx
+    //                     .global::<DragAndDrop<Workspace>>()
+    //                     .currently_dragged::<Channel>(cx.window())
+    //                 {
+    //                     if channel.id != dragged_channel.id {
+    //                         this.drag_target_channel = ChannelDragTarget::Channel(channel.id);
+    //                     }
+    //                     cx.notify()
+    //                 }
+    //             }
+    //         })
+    //         .as_draggable::<_, Channel>(
+    //             channel.clone(),
+    //             move |_, channel, cx: &mut ViewContext<Workspace>| {
+    //                 let theme = &theme::current(cx).collab_panel;
+
+    //                 Flex::<Workspace>::row()
+    //                     .with_child(
+    //                         Svg::new("icons/hash.svg")
+    //                             .with_color(theme.channel_hash.color)
+    //                             .constrained()
+    //                             .with_width(theme.channel_hash.width)
+    //                             .aligned()
+    //                             .left(),
+    //                     )
+    //                     .with_child(
+    //                         Label::new(channel.name.clone(), theme.channel_name.text.clone())
+    //                             .contained()
+    //                             .with_style(theme.channel_name.container)
+    //                             .aligned()
+    //                             .left(),
+    //                     )
+    //                     .align_children_center()
+    //                     .contained()
+    //                     .with_background_color(
+    //                         theme
+    //                             .container
+    //                             .background_color
+    //                             .unwrap_or(gpui::color::Color::transparent_black()),
+    //                     )
+    //                     .contained()
+    //                     .with_padding_left(
+    //                         theme.channel_row.default_style().padding.left
+    //                             + theme.channel_indent * depth as f32,
+    //                     )
+    //                     .into_any()
+    //             },
+    //         )
+    //         .with_cursor_style(CursorStyle::PointingHand)
+    //         .into_any()
+    //     }
+
+    //     fn render_channel_notes(
+    //         &self,
+    //         channel_id: ChannelId,
+    //         theme: &theme::CollabPanel,
+    //         is_selected: bool,
+    //         ix: usize,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> AnyElement<Self> {
+    //         enum ChannelNotes {}
+    //         let host_avatar_width = theme
+    //             .contact_avatar
+    //             .width
+    //             .or(theme.contact_avatar.height)
+    //             .unwrap_or(0.);
+
+    //         MouseEventHandler::new::<ChannelNotes, _>(ix as usize, cx, |state, cx| {
+    //             let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
+    //             let row = theme.project_row.in_state(is_selected).style_for(state);
+
+    //             Flex::<Self>::row()
+    //                 .with_child(render_tree_branch(
+    //                     tree_branch,
+    //                     &row.name.text,
+    //                     false,
+    //                     vec2f(host_avatar_width, theme.row_height),
+    //                     cx.font_cache(),
+    //                 ))
+    //                 .with_child(
+    //                     Svg::new("icons/file.svg")
+    //                         .with_color(theme.channel_hash.color)
+    //                         .constrained()
+    //                         .with_width(theme.channel_hash.width)
+    //                         .aligned()
+    //                         .left(),
+    //                 )
+    //                 .with_child(
+    //                     Label::new("notes", theme.channel_name.text.clone())
+    //                         .contained()
+    //                         .with_style(theme.channel_name.container)
+    //                         .aligned()
+    //                         .left()
+    //                         .flex(1., true),
+    //                 )
+    //                 .constrained()
+    //                 .with_height(theme.row_height)
+    //                 .contained()
+    //                 .with_style(*theme.channel_row.style_for(is_selected, state))
+    //                 .with_padding_left(theme.channel_row.default_style().padding.left)
+    //         })
+    //         .on_click(MouseButton::Left, move |_, this, cx| {
+    //             this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
+    //         })
+    //         .with_cursor_style(CursorStyle::PointingHand)
+    //         .into_any()
+    //     }
+
+    //     fn render_channel_chat(
+    //         &self,
+    //         channel_id: ChannelId,
+    //         theme: &theme::CollabPanel,
+    //         is_selected: bool,
+    //         ix: usize,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> AnyElement<Self> {
+    //         enum ChannelChat {}
+    //         let host_avatar_width = theme
+    //             .contact_avatar
+    //             .width
+    //             .or(theme.contact_avatar.height)
+    //             .unwrap_or(0.);
+
+    //         MouseEventHandler::new::<ChannelChat, _>(ix as usize, cx, |state, cx| {
+    //             let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
+    //             let row = theme.project_row.in_state(is_selected).style_for(state);
+
+    //             Flex::<Self>::row()
+    //                 .with_child(render_tree_branch(
+    //                     tree_branch,
+    //                     &row.name.text,
+    //                     true,
+    //                     vec2f(host_avatar_width, theme.row_height),
+    //                     cx.font_cache(),
+    //                 ))
+    //                 .with_child(
+    //                     Svg::new("icons/conversations.svg")
+    //                         .with_color(theme.channel_hash.color)
+    //                         .constrained()
+    //                         .with_width(theme.channel_hash.width)
+    //                         .aligned()
+    //                         .left(),
+    //                 )
+    //                 .with_child(
+    //                     Label::new("chat", theme.channel_name.text.clone())
+    //                         .contained()
+    //                         .with_style(theme.channel_name.container)
+    //                         .aligned()
+    //                         .left()
+    //                         .flex(1., true),
+    //                 )
+    //                 .constrained()
+    //                 .with_height(theme.row_height)
+    //                 .contained()
+    //                 .with_style(*theme.channel_row.style_for(is_selected, state))
+    //                 .with_padding_left(theme.channel_row.default_style().padding.left)
+    //         })
+    //         .on_click(MouseButton::Left, move |_, this, cx| {
+    //             this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
+    //         })
+    //         .with_cursor_style(CursorStyle::PointingHand)
+    //         .into_any()
+    //     }
+
+    //     fn render_channel_invite(
+    //         channel: Arc<Channel>,
+    //         channel_store: ModelHandle<ChannelStore>,
+    //         theme: &theme::CollabPanel,
+    //         is_selected: bool,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> AnyElement<Self> {
+    //         enum Decline {}
+    //         enum Accept {}
+
+    //         let channel_id = channel.id;
+    //         let is_invite_pending = channel_store
+    //             .read(cx)
+    //             .has_pending_channel_invite_response(&channel);
+    //         let button_spacing = theme.contact_button_spacing;
+
+    //         Flex::row()
+    //             .with_child(
+    //                 Svg::new("icons/hash.svg")
+    //                     .with_color(theme.channel_hash.color)
+    //                     .constrained()
+    //                     .with_width(theme.channel_hash.width)
+    //                     .aligned()
+    //                     .left(),
+    //             )
+    //             .with_child(
+    //                 Label::new(channel.name.clone(), theme.contact_username.text.clone())
+    //                     .contained()
+    //                     .with_style(theme.contact_username.container)
+    //                     .aligned()
+    //                     .left()
+    //                     .flex(1., true),
+    //             )
+    //             .with_child(
+    //                 MouseEventHandler::new::<Decline, _>(channel.id as usize, cx, |mouse_state, _| {
+    //                     let button_style = if is_invite_pending {
+    //                         &theme.disabled_button
+    //                     } else {
+    //                         theme.contact_button.style_for(mouse_state)
+    //                     };
+    //                     render_icon_button(button_style, "icons/x.svg").aligned()
+    //                 })
+    //                 .with_cursor_style(CursorStyle::PointingHand)
+    //                 .on_click(MouseButton::Left, move |_, this, cx| {
+    //                     this.respond_to_channel_invite(channel_id, false, cx);
+    //                 })
+    //                 .contained()
+    //                 .with_margin_right(button_spacing),
+    //             )
+    //             .with_child(
+    //                 MouseEventHandler::new::<Accept, _>(channel.id as usize, cx, |mouse_state, _| {
+    //                     let button_style = if is_invite_pending {
+    //                         &theme.disabled_button
+    //                     } else {
+    //                         theme.contact_button.style_for(mouse_state)
+    //                     };
+    //                     render_icon_button(button_style, "icons/check.svg")
+    //                         .aligned()
+    //                         .flex_float()
+    //                 })
+    //                 .with_cursor_style(CursorStyle::PointingHand)
+    //                 .on_click(MouseButton::Left, move |_, this, cx| {
+    //                     this.respond_to_channel_invite(channel_id, true, cx);
+    //                 }),
+    //             )
+    //             .constrained()
+    //             .with_height(theme.row_height)
+    //             .contained()
+    //             .with_style(
+    //                 *theme
+    //                     .contact_row
+    //                     .in_state(is_selected)
+    //                     .style_for(&mut Default::default()),
+    //             )
+    //             .with_padding_left(
+    //                 theme.contact_row.default_style().padding.left + theme.channel_indent,
+    //             )
+    //             .into_any()
+    //     }
+
+    //     fn render_contact_request(
+    //         user: Arc<User>,
+    //         user_store: ModelHandle<UserStore>,
+    //         theme: &theme::CollabPanel,
+    //         is_incoming: bool,
+    //         is_selected: bool,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> AnyElement<Self> {
+    //         enum Decline {}
+    //         enum Accept {}
+    //         enum Cancel {}
+
+    //         let mut row = Flex::row()
+    //             .with_children(user.avatar.clone().map(|avatar| {
+    //                 Image::from_data(avatar)
+    //                     .with_style(theme.contact_avatar)
+    //                     .aligned()
+    //                     .left()
+    //             }))
+    //             .with_child(
+    //                 Label::new(
+    //                     user.github_login.clone(),
+    //                     theme.contact_username.text.clone(),
+    //                 )
+    //                 .contained()
+    //                 .with_style(theme.contact_username.container)
+    //                 .aligned()
+    //                 .left()
+    //                 .flex(1., true),
+    //             );
+
+    //         let user_id = user.id;
+    //         let github_login = user.github_login.clone();
+    //         let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
+    //         let button_spacing = theme.contact_button_spacing;
+
+    //         if is_incoming {
+    //             row.add_child(
+    //                 MouseEventHandler::new::<Decline, _>(user.id as usize, cx, |mouse_state, _| {
+    //                     let button_style = if is_contact_request_pending {
+    //                         &theme.disabled_button
+    //                     } else {
+    //                         theme.contact_button.style_for(mouse_state)
+    //                     };
+    //                     render_icon_button(button_style, "icons/x.svg").aligned()
+    //                 })
+    //                 .with_cursor_style(CursorStyle::PointingHand)
+    //                 .on_click(MouseButton::Left, move |_, this, cx| {
+    //                     this.respond_to_contact_request(user_id, false, cx);
+    //                 })
+    //                 .contained()
+    //                 .with_margin_right(button_spacing),
+    //             );
+
+    //             row.add_child(
+    //                 MouseEventHandler::new::<Accept, _>(user.id as usize, cx, |mouse_state, _| {
+    //                     let button_style = if is_contact_request_pending {
+    //                         &theme.disabled_button
+    //                     } else {
+    //                         theme.contact_button.style_for(mouse_state)
+    //                     };
+    //                     render_icon_button(button_style, "icons/check.svg")
+    //                         .aligned()
+    //                         .flex_float()
+    //                 })
+    //                 .with_cursor_style(CursorStyle::PointingHand)
+    //                 .on_click(MouseButton::Left, move |_, this, cx| {
+    //                     this.respond_to_contact_request(user_id, true, cx);
+    //                 }),
+    //             );
+    //         } else {
+    //             row.add_child(
+    //                 MouseEventHandler::new::<Cancel, _>(user.id as usize, cx, |mouse_state, _| {
+    //                     let button_style = if is_contact_request_pending {
+    //                         &theme.disabled_button
+    //                     } else {
+    //                         theme.contact_button.style_for(mouse_state)
+    //                     };
+    //                     render_icon_button(button_style, "icons/x.svg")
+    //                         .aligned()
+    //                         .flex_float()
+    //                 })
+    //                 .with_padding(Padding::uniform(2.))
+    //                 .with_cursor_style(CursorStyle::PointingHand)
+    //                 .on_click(MouseButton::Left, move |_, this, cx| {
+    //                     this.remove_contact(user_id, &github_login, cx);
+    //                 })
+    //                 .flex_float(),
+    //             );
+    //         }
+
+    //         row.constrained()
+    //             .with_height(theme.row_height)
+    //             .contained()
+    //             .with_style(
+    //                 *theme
+    //                     .contact_row
+    //                     .in_state(is_selected)
+    //                     .style_for(&mut Default::default()),
+    //             )
+    //             .into_any()
+    //     }
+
+    //     fn has_subchannels(&self, ix: usize) -> bool {
+    //         self.entries.get(ix).map_or(false, |entry| {
+    //             if let ListEntry::Channel { has_children, .. } = entry {
+    //                 *has_children
+    //             } else {
+    //                 false
+    //             }
+    //         })
+    //     }
+
+    //     fn deploy_channel_context_menu(
+    //         &mut self,
+    //         position: Option<Vector2F>,
+    //         channel: &Channel,
+    //         ix: usize,
+    //         cx: &mut ViewContext<Self>,
+    //     ) {
+    //         self.context_menu_on_selected = position.is_none();
+
+    //         let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| {
+    //             self.channel_store
+    //                 .read(cx)
+    //                 .channel_for_id(clipboard.channel_id)
+    //                 .map(|channel| channel.name.clone())
+    //         });
+
+    //         self.context_menu.update(cx, |context_menu, cx| {
+    //             context_menu.set_position_mode(if self.context_menu_on_selected {
+    //                 OverlayPositionMode::Local
+    //             } else {
+    //                 OverlayPositionMode::Window
+    //             });
+
+    //             let mut items = Vec::new();
+
+    //             let select_action_name = if self.selection == Some(ix) {
+    //                 "Unselect"
+    //             } else {
+    //                 "Select"
+    //             };
+
+    //             items.push(ContextMenuItem::action(
+    //                 select_action_name,
+    //                 ToggleSelectedIx { ix },
+    //             ));
+
+    //             if self.has_subchannels(ix) {
+    //                 let expand_action_name = if self.is_channel_collapsed(channel.id) {
+    //                     "Expand Subchannels"
+    //                 } else {
+    //                     "Collapse Subchannels"
+    //                 };
+    //                 items.push(ContextMenuItem::action(
+    //                     expand_action_name,
+    //                     ToggleCollapse {
+    //                         location: channel.id,
+    //                     },
+    //                 ));
+    //             }
+
+    //             items.push(ContextMenuItem::action(
+    //                 "Open Notes",
+    //                 OpenChannelNotes {
+    //                     channel_id: channel.id,
+    //                 },
+    //             ));
+
+    //             items.push(ContextMenuItem::action(
+    //                 "Open Chat",
+    //                 JoinChannelChat {
+    //                     channel_id: channel.id,
+    //                 },
+    //             ));
+
+    //             items.push(ContextMenuItem::action(
+    //                 "Copy Channel Link",
+    //                 CopyChannelLink {
+    //                     channel_id: channel.id,
+    //                 },
+    //             ));
+
+    //             if self.channel_store.read(cx).is_channel_admin(channel.id) {
+    //                 items.extend([
+    //                     ContextMenuItem::Separator,
+    //                     ContextMenuItem::action(
+    //                         "New Subchannel",
+    //                         NewChannel {
+    //                             location: channel.id,
+    //                         },
+    //                     ),
+    //                     ContextMenuItem::action(
+    //                         "Rename",
+    //                         RenameChannel {
+    //                             channel_id: channel.id,
+    //                         },
+    //                     ),
+    //                     ContextMenuItem::action(
+    //                         "Move this channel",
+    //                         StartMoveChannelFor {
+    //                             channel_id: channel.id,
+    //                         },
+    //                     ),
+    //                 ]);
+
+    //                 if let Some(channel_name) = clipboard_channel_name {
+    //                     items.push(ContextMenuItem::Separator);
+    //                     items.push(ContextMenuItem::action(
+    //                         format!("Move '#{}' here", channel_name),
+    //                         MoveChannel { to: channel.id },
+    //                     ));
+    //                 }
+
+    //                 items.extend([
+    //                     ContextMenuItem::Separator,
+    //                     ContextMenuItem::action(
+    //                         "Invite Members",
+    //                         InviteMembers {
+    //                             channel_id: channel.id,
+    //                         },
+    //                     ),
+    //                     ContextMenuItem::action(
+    //                         "Manage Members",
+    //                         ManageMembers {
+    //                             channel_id: channel.id,
+    //                         },
+    //                     ),
+    //                     ContextMenuItem::Separator,
+    //                     ContextMenuItem::action(
+    //                         "Delete",
+    //                         RemoveChannel {
+    //                             channel_id: channel.id,
+    //                         },
+    //                     ),
+    //                 ]);
+    //             }
+
+    //             context_menu.show(
+    //                 position.unwrap_or_default(),
+    //                 if self.context_menu_on_selected {
+    //                     gpui::elements::AnchorCorner::TopRight
+    //                 } else {
+    //                     gpui::elements::AnchorCorner::BottomLeft
+    //                 },
+    //                 items,
+    //                 cx,
+    //             );
+    //         });
+
+    //         cx.notify();
+    //     }
+
+    //     fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
+    //         if self.take_editing_state(cx) {
+    //             cx.focus(&self.filter_editor);
+    //         } else {
+    //             self.filter_editor.update(cx, |editor, cx| {
+    //                 if editor.buffer().read(cx).len(cx) > 0 {
+    //                     editor.set_text("", cx);
+    //                 }
+    //             });
+    //         }
+
+    //         self.update_entries(false, cx);
+    //     }
+
+    //     fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
+    //         let ix = self.selection.map_or(0, |ix| ix + 1);
+    //         if ix < self.entries.len() {
+    //             self.selection = Some(ix);
+    //         }
+
+    //         self.list_state.reset(self.entries.len());
+    //         if let Some(ix) = self.selection {
+    //             self.list_state.scroll_to(ListOffset {
+    //                 item_ix: ix,
+    //                 offset_in_item: 0.,
+    //             });
+    //         }
+    //         cx.notify();
+    //     }
+
+    //     fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
+    //         let ix = self.selection.take().unwrap_or(0);
+    //         if ix > 0 {
+    //             self.selection = Some(ix - 1);
+    //         }
+
+    //         self.list_state.reset(self.entries.len());
+    //         if let Some(ix) = self.selection {
+    //             self.list_state.scroll_to(ListOffset {
+    //                 item_ix: ix,
+    //                 offset_in_item: 0.,
+    //             });
+    //         }
+    //         cx.notify();
+    //     }
+
+    //     fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+    //         if self.confirm_channel_edit(cx) {
+    //             return;
+    //         }
+
+    //         if let Some(selection) = self.selection {
+    //             if let Some(entry) = self.entries.get(selection) {
+    //                 match entry {
+    //                     ListEntry::Header(section) => match section {
+    //                         Section::ActiveCall => Self::leave_call(cx),
+    //                         Section::Channels => self.new_root_channel(cx),
+    //                         Section::Contacts => self.toggle_contact_finder(cx),
+    //                         Section::ContactRequests
+    //                         | Section::Online
+    //                         | Section::Offline
+    //                         | Section::ChannelInvites => {
+    //                             self.toggle_section_expanded(*section, cx);
+    //                         }
+    //                     },
+    //                     ListEntry::Contact { contact, calling } => {
+    //                         if contact.online && !contact.busy && !calling {
+    //                             self.call(contact.user.id, Some(self.project.clone()), cx);
+    //                         }
+    //                     }
+    //                     ListEntry::ParticipantProject {
+    //                         project_id,
+    //                         host_user_id,
+    //                         ..
+    //                     } => {
+    //                         if let Some(workspace) = self.workspace.upgrade(cx) {
+    //                             let app_state = workspace.read(cx).app_state().clone();
+    //                             workspace::join_remote_project(
+    //                                 *project_id,
+    //                                 *host_user_id,
+    //                                 app_state,
+    //                                 cx,
+    //                             )
+    //                             .detach_and_log_err(cx);
+    //                         }
+    //                     }
+    //                     ListEntry::ParticipantScreen { peer_id, .. } => {
+    //                         let Some(peer_id) = peer_id else {
+    //                             return;
+    //                         };
+    //                         if let Some(workspace) = self.workspace.upgrade(cx) {
+    //                             workspace.update(cx, |workspace, cx| {
+    //                                 workspace.open_shared_screen(*peer_id, cx)
+    //                             });
+    //                         }
+    //                     }
+    //                     ListEntry::Channel { channel, .. } => {
+    //                         let is_active = maybe!({
+    //                             let call_channel = ActiveCall::global(cx)
+    //                                 .read(cx)
+    //                                 .room()?
+    //                                 .read(cx)
+    //                                 .channel_id()?;
+
+    //                             Some(call_channel == channel.id)
+    //                         })
+    //                         .unwrap_or(false);
+    //                         if is_active {
+    //                             self.open_channel_notes(
+    //                                 &OpenChannelNotes {
+    //                                     channel_id: channel.id,
+    //                                 },
+    //                                 cx,
+    //                             )
+    //                         } else {
+    //                             self.join_channel(channel.id, cx)
+    //                         }
+    //                     }
+    //                     ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
+    //                     _ => {}
+    //                 }
+    //             }
+    //         }
+    //     }
+
+    //     fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) {
+    //         if self.channel_editing_state.is_some() {
+    //             self.channel_name_editor.update(cx, |editor, cx| {
+    //                 editor.insert(" ", cx);
+    //             });
+    //         }
+    //     }
+
+    //     fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
+    //         if let Some(editing_state) = &mut self.channel_editing_state {
+    //             match editing_state {
+    //                 ChannelEditingState::Create {
+    //                     location,
+    //                     pending_name,
+    //                     ..
+    //                 } => {
+    //                     if pending_name.is_some() {
+    //                         return false;
+    //                     }
+    //                     let channel_name = self.channel_name_editor.read(cx).text(cx);
+
+    //                     *pending_name = Some(channel_name.clone());
+
+    //                     self.channel_store
+    //                         .update(cx, |channel_store, cx| {
+    //                             channel_store.create_channel(&channel_name, *location, cx)
+    //                         })
+    //                         .detach();
+    //                     cx.notify();
+    //                 }
+    //                 ChannelEditingState::Rename {
+    //                     location,
+    //                     pending_name,
+    //                 } => {
+    //                     if pending_name.is_some() {
+    //                         return false;
+    //                     }
+    //                     let channel_name = self.channel_name_editor.read(cx).text(cx);
+    //                     *pending_name = Some(channel_name.clone());
+
+    //                     self.channel_store
+    //                         .update(cx, |channel_store, cx| {
+    //                             channel_store.rename(*location, &channel_name, cx)
+    //                         })
+    //                         .detach();
+    //                     cx.notify();
+    //                 }
+    //             }
+    //             cx.focus_self();
+    //             true
+    //         } else {
+    //             false
+    //         }
+    //     }
+
+    //     fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
+    //         if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
+    //             self.collapsed_sections.remove(ix);
+    //         } else {
+    //             self.collapsed_sections.push(section);
+    //         }
+    //         self.update_entries(false, cx);
+    //     }
+
+    //     fn collapse_selected_channel(
+    //         &mut self,
+    //         _: &CollapseSelectedChannel,
+    //         cx: &mut ViewContext<Self>,
+    //     ) {
+    //         let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
+    //             return;
+    //         };
+
+    //         if self.is_channel_collapsed(channel_id) {
+    //             return;
+    //         }
+
+    //         self.toggle_channel_collapsed(channel_id, cx);
+    //     }
+
+    //     fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
+    //         let Some(id) = self.selected_channel().map(|channel| channel.id) else {
+    //             return;
+    //         };
+
+    //         if !self.is_channel_collapsed(id) {
+    //             return;
+    //         }
+
+    //         self.toggle_channel_collapsed(id, cx)
+    //     }
+
+    //     fn toggle_channel_collapsed_action(
+    //         &mut self,
+    //         action: &ToggleCollapse,
+    //         cx: &mut ViewContext<Self>,
+    //     ) {
+    //         self.toggle_channel_collapsed(action.location, cx);
+    //     }
+
+    //     fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
+    //         match self.collapsed_channels.binary_search(&channel_id) {
+    //             Ok(ix) => {
+    //                 self.collapsed_channels.remove(ix);
+    //             }
+    //             Err(ix) => {
+    //                 self.collapsed_channels.insert(ix, channel_id);
+    //             }
+    //         };
+    //         self.serialize(cx);
+    //         self.update_entries(true, cx);
+    //         cx.notify();
+    //         cx.focus_self();
+    //     }
+
+    //     fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool {
+    //         self.collapsed_channels.binary_search(&channel_id).is_ok()
+    //     }
+
+    //     fn leave_call(cx: &mut ViewContext<Self>) {
+    //         ActiveCall::global(cx)
+    //             .update(cx, |call, cx| call.hang_up(cx))
+    //             .detach_and_log_err(cx);
+    //     }
+
+    //     fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
+    //         if let Some(workspace) = self.workspace.upgrade(cx) {
+    //             workspace.update(cx, |workspace, cx| {
+    //                 workspace.toggle_modal(cx, |_, cx| {
+    //                     cx.add_view(|cx| {
+    //                         let mut finder = ContactFinder::new(self.user_store.clone(), cx);
+    //                         finder.set_query(self.filter_editor.read(cx).text(cx), cx);
+    //                         finder
+    //                     })
+    //                 });
+    //             });
+    //         }
+    //     }
+
+    //     fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
+    //         self.channel_editing_state = Some(ChannelEditingState::Create {
+    //             location: None,
+    //             pending_name: None,
+    //         });
+    //         self.update_entries(false, cx);
+    //         self.select_channel_editor();
+    //         cx.focus(self.channel_name_editor.as_any());
+    //         cx.notify();
+    //     }
+
+    //     fn select_channel_editor(&mut self) {
+    //         self.selection = self.entries.iter().position(|entry| match entry {
+    //             ListEntry::ChannelEditor { .. } => true,
+    //             _ => false,
+    //         });
+    //     }
+
+    //     fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
+    //         self.collapsed_channels
+    //             .retain(|channel| *channel != action.location);
+    //         self.channel_editing_state = Some(ChannelEditingState::Create {
+    //             location: Some(action.location.to_owned()),
+    //             pending_name: None,
+    //         });
+    //         self.update_entries(false, cx);
+    //         self.select_channel_editor();
+    //         cx.focus(self.channel_name_editor.as_any());
+    //         cx.notify();
+    //     }
+
+    //     fn invite_members(&mut self, action: &InviteMembers, cx: &mut ViewContext<Self>) {
+    //         self.show_channel_modal(action.channel_id, channel_modal::Mode::InviteMembers, cx);
+    //     }
+
+    //     fn manage_members(&mut self, action: &ManageMembers, cx: &mut ViewContext<Self>) {
+    //         self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx);
+    //     }
+
+    //     fn remove(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
+    //         if let Some(channel) = self.selected_channel() {
+    //             self.remove_channel(channel.id, cx)
+    //         }
+    //     }
+
+    //     fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
+    //         if let Some(channel) = self.selected_channel() {
+    //             self.rename_channel(
+    //                 &RenameChannel {
+    //                     channel_id: channel.id,
+    //                 },
+    //                 cx,
+    //             );
+    //         }
+    //     }
+
+    //     fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext<Self>) {
+    //         let channel_store = self.channel_store.read(cx);
+    //         if !channel_store.is_channel_admin(action.channel_id) {
+    //             return;
+    //         }
+    //         if let Some(channel) = channel_store.channel_for_id(action.channel_id).cloned() {
+    //             self.channel_editing_state = Some(ChannelEditingState::Rename {
+    //                 location: action.channel_id.to_owned(),
+    //                 pending_name: None,
+    //             });
+    //             self.channel_name_editor.update(cx, |editor, cx| {
+    //                 editor.set_text(channel.name.clone(), cx);
+    //                 editor.select_all(&Default::default(), cx);
+    //             });
+    //             cx.focus(self.channel_name_editor.as_any());
+    //             self.update_entries(false, cx);
+    //             self.select_channel_editor();
+    //         }
+    //     }
+
+    //     fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
+    //         if let Some(workspace) = self.workspace.upgrade(cx) {
+    //             ChannelView::open(action.channel_id, workspace, cx).detach();
+    //         }
+    //     }
+
+    //     fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
+    //         let Some(channel) = self.selected_channel() else {
+    //             return;
+    //         };
+
+    //         self.deploy_channel_context_menu(None, &channel.clone(), self.selection.unwrap(), cx);
+    //     }
+
+    //     fn selected_channel(&self) -> Option<&Arc<Channel>> {
+    //         self.selection
+    //             .and_then(|ix| self.entries.get(ix))
+    //             .and_then(|entry| match entry {
+    //                 ListEntry::Channel { channel, .. } => Some(channel),
+    //                 _ => None,
+    //             })
+    //     }
+
+    //     fn show_channel_modal(
+    //         &mut self,
+    //         channel_id: ChannelId,
+    //         mode: channel_modal::Mode,
+    //         cx: &mut ViewContext<Self>,
+    //     ) {
+    //         let workspace = self.workspace.clone();
+    //         let user_store = self.user_store.clone();
+    //         let channel_store = self.channel_store.clone();
+    //         let members = self.channel_store.update(cx, |channel_store, cx| {
+    //             channel_store.get_channel_member_details(channel_id, cx)
+    //         });
+
+    //         cx.spawn(|_, mut cx| async move {
+    //             let members = members.await?;
+    //             workspace.update(&mut cx, |workspace, cx| {
+    //                 workspace.toggle_modal(cx, |_, cx| {
+    //                     cx.add_view(|cx| {
+    //                         ChannelModal::new(
+    //                             user_store.clone(),
+    //                             channel_store.clone(),
+    //                             channel_id,
+    //                             mode,
+    //                             members,
+    //                             cx,
+    //                         )
+    //                     })
+    //                 });
+    //             })
+    //         })
+    //         .detach();
+    //     }
+
+    //     fn remove_selected_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext<Self>) {
+    //         self.remove_channel(action.channel_id, cx)
+    //     }
+
+    //     fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
+    //         let channel_store = self.channel_store.clone();
+    //         if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
+    //             let prompt_message = format!(
+    //                 "Are you sure you want to remove the channel \"{}\"?",
+    //                 channel.name
+    //             );
+    //             let mut answer =
+    //                 cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
+    //             let window = cx.window();
+    //             cx.spawn(|this, mut cx| async move {
+    //                 if answer.next().await == Some(0) {
+    //                     if let Err(e) = channel_store
+    //                         .update(&mut cx, |channels, _| channels.remove_channel(channel_id))
+    //                         .await
+    //                     {
+    //                         window.prompt(
+    //                             PromptLevel::Info,
+    //                             &format!("Failed to remove channel: {}", e),
+    //                             &["Ok"],
+    //                             &mut cx,
+    //                         );
+    //                     }
+    //                     this.update(&mut cx, |_, cx| cx.focus_self()).ok();
+    //                 }
+    //             })
+    //             .detach();
+    //         }
+    //     }
+
+    //     // Should move to the filter editor if clicking on it
+    //     // Should move selection to the channel editor if activating it
+
+    //     fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
+    //         let user_store = self.user_store.clone();
+    //         let prompt_message = format!(
+    //             "Are you sure you want to remove \"{}\" from your contacts?",
+    //             github_login
+    //         );
+    //         let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
+    //         let window = cx.window();
+    //         cx.spawn(|_, mut cx| async move {
+    //             if answer.next().await == Some(0) {
+    //                 if let Err(e) = user_store
+    //                     .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
+    //                     .await
+    //                 {
+    //                     window.prompt(
+    //                         PromptLevel::Info,
+    //                         &format!("Failed to remove contact: {}", e),
+    //                         &["Ok"],
+    //                         &mut cx,
+    //                     );
+    //                 }
+    //             }
+    //         })
+    //         .detach();
+    //     }
+
+    //     fn respond_to_contact_request(
+    //         &mut self,
+    //         user_id: u64,
+    //         accept: bool,
+    //         cx: &mut ViewContext<Self>,
+    //     ) {
+    //         self.user_store
+    //             .update(cx, |store, cx| {
+    //                 store.respond_to_contact_request(user_id, accept, cx)
+    //             })
+    //             .detach();
+    //     }
+
+    //     fn respond_to_channel_invite(
+    //         &mut self,
+    //         channel_id: u64,
+    //         accept: bool,
+    //         cx: &mut ViewContext<Self>,
+    //     ) {
+    //         self.channel_store
+    //             .update(cx, |store, cx| {
+    //                 store.respond_to_channel_invite(channel_id, accept, cx)
+    //             })
+    //             .detach();
+    //     }
+
+    //     fn call(
+    //         &mut self,
+    //         recipient_user_id: u64,
+    //         initial_project: Option<ModelHandle<Project>>,
+    //         cx: &mut ViewContext<Self>,
+    //     ) {
+    //         ActiveCall::global(cx)
+    //             .update(cx, |call, cx| {
+    //                 call.invite(recipient_user_id, initial_project, cx)
+    //             })
+    //             .detach_and_log_err(cx);
+    //     }
+
+    //     fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
+    //         let Some(workspace) = self.workspace.upgrade(cx) else {
+    //             return;
+    //         };
+    //         let Some(handle) = cx.window().downcast::<Workspace>() else {
+    //             return;
+    //         };
+    //         workspace::join_channel(
+    //             channel_id,
+    //             workspace.read(cx).app_state().clone(),
+    //             Some(handle),
+    //             cx,
+    //         )
+    //         .detach_and_log_err(cx)
+    //     }
+
+    //     fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext<Self>) {
+    //         let channel_id = action.channel_id;
+    //         if let Some(workspace) = self.workspace.upgrade(cx) {
+    //             cx.app_context().defer(move |cx| {
+    //                 workspace.update(cx, |workspace, cx| {
+    //                     if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
+    //                         panel.update(cx, |panel, cx| {
+    //                             panel
+    //                                 .select_channel(channel_id, None, cx)
+    //                                 .detach_and_log_err(cx);
+    //                         });
+    //                     }
+    //                 });
+    //             });
+    //         }
+    //     }
+
+    //     fn copy_channel_link(&mut self, action: &CopyChannelLink, cx: &mut ViewContext<Self>) {
+    //         let channel_store = self.channel_store.read(cx);
+    //         let Some(channel) = channel_store.channel_for_id(action.channel_id) else {
+    //             return;
+    //         };
+    //         let item = ClipboardItem::new(channel.link());
+    //         cx.write_to_clipboard(item)
+    //     }
+}
+
+// fn render_tree_branch(
+//     branch_style: theme::TreeBranch,
+//     row_style: &TextStyle,
+//     is_last: bool,
+//     size: Vector2F,
+//     font_cache: &FontCache,
+// ) -> gpui::elements::ConstrainedBox<CollabPanel> {
+//     let line_height = row_style.line_height(font_cache);
+//     let cap_height = row_style.cap_height(font_cache);
+//     let baseline_offset = row_style.baseline_offset(font_cache) + (size.y() - line_height) / 2.;
+
+//     Canvas::new(move |bounds, _, _, cx| {
+//         cx.paint_layer(None, |cx| {
+//             let start_x = bounds.min_x() + (bounds.width() / 2.) - (branch_style.width / 2.);
+//             let end_x = bounds.max_x();
+//             let start_y = bounds.min_y();
+//             let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
+
+//             cx.scene().push_quad(gpui::Quad {
+//                 bounds: RectF::from_points(
+//                     vec2f(start_x, start_y),
+//                     vec2f(
+//                         start_x + branch_style.width,
+//                         if is_last { end_y } else { bounds.max_y() },
+//                     ),
+//                 ),
+//                 background: Some(branch_style.color),
+//                 border: gpui::Border::default(),
+//                 corner_radii: (0.).into(),
+//             });
+//             cx.scene().push_quad(gpui::Quad {
+//                 bounds: RectF::from_points(
+//                     vec2f(start_x, end_y),
+//                     vec2f(end_x, end_y + branch_style.width),
+//                 ),
+//                 background: Some(branch_style.color),
+//                 border: gpui::Border::default(),
+//                 corner_radii: (0.).into(),
+//             });
+//         })
+//     })
+//     .constrained()
+//     .with_width(size.x())
+// }
+
+impl Render for CollabPanel {
+    type Element = Focusable<Self, Div<Self>>;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        div()
+            .key_context("CollabPanel")
+            .track_focus(&self.focus_handle)
+            .child("COLLAB PANEL")
+    }
+}
+
+// impl View for CollabPanel {
+//     fn ui_name() -> &'static str {
+//         "CollabPanel"
+//     }
+
+//     fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+//         if !self.has_focus {
+//             self.has_focus = true;
+//             if !self.context_menu.is_focused(cx) {
+//                 if let Some(editing_state) = &self.channel_editing_state {
+//                     if editing_state.pending_name().is_none() {
+//                         cx.focus(&self.channel_name_editor);
+//                     } else {
+//                         cx.focus(&self.filter_editor);
+//                     }
+//                 } else {
+//                     cx.focus(&self.filter_editor);
+//                 }
+//             }
+//             cx.emit(Event::Focus);
+//         }
+//     }
+
+//     fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
+//         self.has_focus = false;
+//     }
+
+//     fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
+//         let theme = &theme::current(cx).collab_panel;
+
+//         if self.user_store.read(cx).current_user().is_none() {
+//             enum LogInButton {}
+
+//             return Flex::column()
+//                 .with_child(
+//                     MouseEventHandler::new::<LogInButton, _>(0, cx, |state, _| {
+//                         let button = theme.log_in_button.style_for(state);
+//                         Label::new("Sign in to collaborate", button.text.clone())
+//                             .aligned()
+//                             .left()
+//                             .contained()
+//                             .with_style(button.container)
+//                     })
+//                     .on_click(MouseButton::Left, |_, this, cx| {
+//                         let client = this.client.clone();
+//                         cx.spawn(|_, cx| async move {
+//                             client.authenticate_and_connect(true, &cx).await.log_err();
+//                         })
+//                         .detach();
+//                     })
+//                     .with_cursor_style(CursorStyle::PointingHand),
+//                 )
+//                 .contained()
+//                 .with_style(theme.container)
+//                 .into_any();
+//         }
+
+//         enum PanelFocus {}
+//         MouseEventHandler::new::<PanelFocus, _>(0, cx, |_, cx| {
+//             Stack::new()
+//                 .with_child(
+//                     Flex::column()
+//                         .with_child(
+//                             Flex::row().with_child(
+//                                 ChildView::new(&self.filter_editor, cx)
+//                                     .contained()
+//                                     .with_style(theme.user_query_editor.container)
+//                                     .flex(1.0, true),
+//                             ),
+//                         )
+//                         .with_child(List::new(self.list_state.clone()).flex(1., true).into_any())
+//                         .contained()
+//                         .with_style(theme.container)
+//                         .into_any(),
+//                 )
+//                 .with_children(
+//                     (!self.context_menu_on_selected)
+//                         .then(|| ChildView::new(&self.context_menu, cx)),
+//                 )
+//                 .into_any()
+//         })
+//         .on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
+//         .into_any_named("collab panel")
+//     }
+
+//     fn update_keymap_context(
+//         &self,
+//         keymap: &mut gpui::keymap_matcher::KeymapContext,
+//         _: &AppContext,
+//     ) {
+//         Self::reset_to_default_keymap_context(keymap);
+//         if self.channel_editing_state.is_some() {
+//             keymap.add_identifier("editing");
+//         } else {
+//             keymap.add_identifier("not_editing");
+//         }
+//     }
+// }
+
+impl EventEmitter<PanelEvent> for CollabPanel {}
+
+impl Panel for CollabPanel {
+    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
+        CollaborationPanelSettings::get_global(cx).dock
+    }
+
+    fn position_is_valid(&self, position: DockPosition) -> bool {
+        matches!(position, DockPosition::Left | DockPosition::Right)
+    }
+
+    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
+        settings::update_settings_file::<CollaborationPanelSettings>(
+            self.fs.clone(),
+            cx,
+            move |settings| settings.dock = Some(position),
+        );
+    }
+
+    fn size(&self, cx: &gpui::WindowContext) -> f32 {
+        self.width
+            .unwrap_or_else(|| CollaborationPanelSettings::get_global(cx).default_width)
+    }
+
+    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
+        self.width = size;
+        // todo!()
+        // self.serialize(cx);
+        cx.notify();
+    }
+
+    fn icon(&self, cx: &gpui::WindowContext) -> Option<ui::Icon> {
+        CollaborationPanelSettings::get_global(cx)
+            .button
+            .then(|| ui::Icon::Collab)
+    }
+
+    fn toggle_action(&self) -> Box<dyn gpui::Action> {
+        Box::new(ToggleFocus)
+    }
+
+    fn has_focus(&self, cx: &gpui::WindowContext) -> bool {
+        self.focus_handle.contains_focused(cx)
+    }
+
+    fn persistent_name() -> &'static str {
+        "CollabPanel"
+    }
+}
+
+impl FocusableView for CollabPanel {
+    fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+// impl PartialEq for ListEntry {
+//     fn eq(&self, other: &Self) -> bool {
+//         match self {
+//             ListEntry::Header(section_1) => {
+//                 if let ListEntry::Header(section_2) = other {
+//                     return section_1 == section_2;
+//                 }
+//             }
+//             ListEntry::CallParticipant { user: user_1, .. } => {
+//                 if let ListEntry::CallParticipant { user: user_2, .. } = other {
+//                     return user_1.id == user_2.id;
+//                 }
+//             }
+//             ListEntry::ParticipantProject {
+//                 project_id: project_id_1,
+//                 ..
+//             } => {
+//                 if let ListEntry::ParticipantProject {
+//                     project_id: project_id_2,
+//                     ..
+//                 } = other
+//                 {
+//                     return project_id_1 == project_id_2;
+//                 }
+//             }
+//             ListEntry::ParticipantScreen {
+//                 peer_id: peer_id_1, ..
+//             } => {
+//                 if let ListEntry::ParticipantScreen {
+//                     peer_id: peer_id_2, ..
+//                 } = other
+//                 {
+//                     return peer_id_1 == peer_id_2;
+//                 }
+//             }
+//             ListEntry::Channel {
+//                 channel: channel_1, ..
+//             } => {
+//                 if let ListEntry::Channel {
+//                     channel: channel_2, ..
+//                 } = other
+//                 {
+//                     return channel_1.id == channel_2.id;
+//                 }
+//             }
+//             ListEntry::ChannelNotes { channel_id } => {
+//                 if let ListEntry::ChannelNotes {
+//                     channel_id: other_id,
+//                 } = other
+//                 {
+//                     return channel_id == other_id;
+//                 }
+//             }
+//             ListEntry::ChannelChat { channel_id } => {
+//                 if let ListEntry::ChannelChat {
+//                     channel_id: other_id,
+//                 } = other
+//                 {
+//                     return channel_id == other_id;
+//                 }
+//             }
+//             ListEntry::ChannelInvite(channel_1) => {
+//                 if let ListEntry::ChannelInvite(channel_2) = other {
+//                     return channel_1.id == channel_2.id;
+//                 }
+//             }
+//             ListEntry::IncomingRequest(user_1) => {
+//                 if let ListEntry::IncomingRequest(user_2) = other {
+//                     return user_1.id == user_2.id;
+//                 }
+//             }
+//             ListEntry::OutgoingRequest(user_1) => {
+//                 if let ListEntry::OutgoingRequest(user_2) = other {
+//                     return user_1.id == user_2.id;
+//                 }
+//             }
+//             ListEntry::Contact {
+//                 contact: contact_1, ..
+//             } => {
+//                 if let ListEntry::Contact {
+//                     contact: contact_2, ..
+//                 } = other
+//                 {
+//                     return contact_1.user.id == contact_2.user.id;
+//                 }
+//             }
+//             ListEntry::ChannelEditor { depth } => {
+//                 if let ListEntry::ChannelEditor { depth: other_depth } = other {
+//                     return depth == other_depth;
+//                 }
+//             }
+//             ListEntry::ContactPlaceholder => {
+//                 if let ListEntry::ContactPlaceholder = other {
+//                     return true;
+//                 }
+//             }
+//         }
+//         false
+//     }
+// }
+
+// fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> {
+//     Svg::new(svg_path)
+//         .with_color(style.color)
+//         .constrained()
+//         .with_width(style.icon_width)
+//         .aligned()
+//         .constrained()
+//         .with_width(style.button_width)
+//         .with_height(style.button_width)
+//         .contained()
+//         .with_style(style.container)
+// }

crates/collab_ui2/src/collab_panel/channel_modal.rs 🔗

@@ -0,0 +1,717 @@
+use channel::{ChannelId, ChannelMembership, ChannelStore};
+use client::{
+    proto::{self, ChannelRole, ChannelVisibility},
+    User, UserId, UserStore,
+};
+use context_menu::{ContextMenu, ContextMenuItem};
+use fuzzy::{match_strings, StringMatchCandidate};
+use gpui::{
+    actions,
+    elements::*,
+    platform::{CursorStyle, MouseButton},
+    AppContext, ClipboardItem, Entity, ModelHandle, MouseState, Task, View, ViewContext,
+    ViewHandle,
+};
+use picker::{Picker, PickerDelegate, PickerEvent};
+use std::sync::Arc;
+use util::TryFutureExt;
+use workspace::Modal;
+
+actions!(
+    channel_modal,
+    [
+        SelectNextControl,
+        ToggleMode,
+        ToggleMemberAdmin,
+        RemoveMember
+    ]
+);
+
+pub fn init(cx: &mut AppContext) {
+    Picker::<ChannelModalDelegate>::init(cx);
+    cx.add_action(ChannelModal::toggle_mode);
+    cx.add_action(ChannelModal::toggle_member_admin);
+    cx.add_action(ChannelModal::remove_member);
+    cx.add_action(ChannelModal::dismiss);
+}
+
+pub struct ChannelModal {
+    picker: ViewHandle<Picker<ChannelModalDelegate>>,
+    channel_store: ModelHandle<ChannelStore>,
+    channel_id: ChannelId,
+    has_focus: bool,
+}
+
+impl ChannelModal {
+    pub fn new(
+        user_store: ModelHandle<UserStore>,
+        channel_store: ModelHandle<ChannelStore>,
+        channel_id: ChannelId,
+        mode: Mode,
+        members: Vec<ChannelMembership>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
+        let picker = cx.add_view(|cx| {
+            Picker::new(
+                ChannelModalDelegate {
+                    matching_users: Vec::new(),
+                    matching_member_indices: Vec::new(),
+                    selected_index: 0,
+                    user_store: user_store.clone(),
+                    channel_store: channel_store.clone(),
+                    channel_id,
+                    match_candidates: Vec::new(),
+                    members,
+                    mode,
+                    context_menu: cx.add_view(|cx| {
+                        let mut menu = ContextMenu::new(cx.view_id(), cx);
+                        menu.set_position_mode(OverlayPositionMode::Local);
+                        menu
+                    }),
+                },
+                cx,
+            )
+            .with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
+        });
+
+        cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
+
+        let has_focus = picker.read(cx).has_focus();
+
+        Self {
+            picker,
+            channel_store,
+            channel_id,
+            has_focus,
+        }
+    }
+
+    fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext<Self>) {
+        let mode = match self.picker.read(cx).delegate().mode {
+            Mode::ManageMembers => Mode::InviteMembers,
+            Mode::InviteMembers => Mode::ManageMembers,
+        };
+        self.set_mode(mode, cx);
+    }
+
+    fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
+        let channel_store = self.channel_store.clone();
+        let channel_id = self.channel_id;
+        cx.spawn(|this, mut cx| async move {
+            if mode == Mode::ManageMembers {
+                let mut members = channel_store
+                    .update(&mut cx, |channel_store, cx| {
+                        channel_store.get_channel_member_details(channel_id, cx)
+                    })
+                    .await?;
+
+                members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
+
+                this.update(&mut cx, |this, cx| {
+                    this.picker
+                        .update(cx, |picker, _| picker.delegate_mut().members = members);
+                })?;
+            }
+
+            this.update(&mut cx, |this, cx| {
+                this.picker.update(cx, |picker, cx| {
+                    let delegate = picker.delegate_mut();
+                    delegate.mode = mode;
+                    delegate.selected_index = 0;
+                    picker.set_query("", cx);
+                    picker.update_matches(picker.query(cx), cx);
+                    cx.notify()
+                });
+                cx.notify()
+            })
+        })
+        .detach();
+    }
+
+    fn toggle_member_admin(&mut self, _: &ToggleMemberAdmin, cx: &mut ViewContext<Self>) {
+        self.picker.update(cx, |picker, cx| {
+            picker.delegate_mut().toggle_selected_member_admin(cx);
+        })
+    }
+
+    fn remove_member(&mut self, _: &RemoveMember, cx: &mut ViewContext<Self>) {
+        self.picker.update(cx, |picker, cx| {
+            picker.delegate_mut().remove_selected_member(cx);
+        });
+    }
+
+    fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
+        cx.emit(PickerEvent::Dismiss);
+    }
+}
+
+impl Entity for ChannelModal {
+    type Event = PickerEvent;
+}
+
+impl View for ChannelModal {
+    fn ui_name() -> &'static str {
+        "ChannelModal"
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        let theme = &theme::current(cx).collab_panel.tabbed_modal;
+
+        let mode = self.picker.read(cx).delegate().mode;
+        let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else {
+            return Empty::new().into_any();
+        };
+
+        enum InviteMembers {}
+        enum ManageMembers {}
+
+        fn render_mode_button<T: 'static>(
+            mode: Mode,
+            text: &'static str,
+            current_mode: Mode,
+            theme: &theme::TabbedModal,
+            cx: &mut ViewContext<ChannelModal>,
+        ) -> AnyElement<ChannelModal> {
+            let active = mode == current_mode;
+            MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
+                let contained_text = theme.tab_button.style_for(active, state);
+                Label::new(text, contained_text.text.clone())
+                    .contained()
+                    .with_style(contained_text.container.clone())
+            })
+            .on_click(MouseButton::Left, move |_, this, cx| {
+                if !active {
+                    this.set_mode(mode, cx);
+                }
+            })
+            .with_cursor_style(CursorStyle::PointingHand)
+            .into_any()
+        }
+
+        fn render_visibility(
+            channel_id: ChannelId,
+            visibility: ChannelVisibility,
+            theme: &theme::TabbedModal,
+            cx: &mut ViewContext<ChannelModal>,
+        ) -> AnyElement<ChannelModal> {
+            enum TogglePublic {}
+
+            if visibility == ChannelVisibility::Members {
+                return Flex::row()
+                    .with_child(
+                        MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
+                            let style = theme.visibility_toggle.style_for(state);
+                            Label::new(format!("{}", "Public access: OFF"), style.text.clone())
+                                .contained()
+                                .with_style(style.container.clone())
+                        })
+                        .on_click(MouseButton::Left, move |_, this, cx| {
+                            this.channel_store
+                                .update(cx, |channel_store, cx| {
+                                    channel_store.set_channel_visibility(
+                                        channel_id,
+                                        ChannelVisibility::Public,
+                                        cx,
+                                    )
+                                })
+                                .detach_and_log_err(cx);
+                        })
+                        .with_cursor_style(CursorStyle::PointingHand),
+                    )
+                    .into_any();
+            }
+
+            Flex::row()
+                .with_child(
+                    MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
+                        let style = theme.visibility_toggle.style_for(state);
+                        Label::new(format!("{}", "Public access: ON"), style.text.clone())
+                            .contained()
+                            .with_style(style.container.clone())
+                    })
+                    .on_click(MouseButton::Left, move |_, this, cx| {
+                        this.channel_store
+                            .update(cx, |channel_store, cx| {
+                                channel_store.set_channel_visibility(
+                                    channel_id,
+                                    ChannelVisibility::Members,
+                                    cx,
+                                )
+                            })
+                            .detach_and_log_err(cx);
+                    })
+                    .with_cursor_style(CursorStyle::PointingHand),
+                )
+                .with_spacing(14.0)
+                .with_child(
+                    MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
+                        let style = theme.channel_link.style_for(state);
+                        Label::new(format!("{}", "copy link"), style.text.clone())
+                            .contained()
+                            .with_style(style.container.clone())
+                    })
+                    .on_click(MouseButton::Left, move |_, this, cx| {
+                        if let Some(channel) =
+                            this.channel_store.read(cx).channel_for_id(channel_id)
+                        {
+                            let item = ClipboardItem::new(channel.link());
+                            cx.write_to_clipboard(item);
+                        }
+                    })
+                    .with_cursor_style(CursorStyle::PointingHand),
+                )
+                .into_any()
+        }
+
+        Flex::column()
+            .with_child(
+                Flex::column()
+                    .with_child(
+                        Label::new(format!("#{}", channel.name), theme.title.text.clone())
+                            .contained()
+                            .with_style(theme.title.container.clone()),
+                    )
+                    .with_child(render_visibility(channel.id, channel.visibility, theme, cx))
+                    .with_child(Flex::row().with_children([
+                        render_mode_button::<InviteMembers>(
+                            Mode::InviteMembers,
+                            "Invite members",
+                            mode,
+                            theme,
+                            cx,
+                        ),
+                        render_mode_button::<ManageMembers>(
+                            Mode::ManageMembers,
+                            "Manage members",
+                            mode,
+                            theme,
+                            cx,
+                        ),
+                    ]))
+                    .expanded()
+                    .contained()
+                    .with_style(theme.header),
+            )
+            .with_child(
+                ChildView::new(&self.picker, cx)
+                    .contained()
+                    .with_style(theme.body),
+            )
+            .constrained()
+            .with_max_height(theme.max_height)
+            .with_max_width(theme.max_width)
+            .contained()
+            .with_style(theme.modal)
+            .into_any()
+    }
+
+    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+        self.has_focus = true;
+        if cx.is_self_focused() {
+            cx.focus(&self.picker)
+        }
+    }
+
+    fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
+        self.has_focus = false;
+    }
+}
+
+impl Modal for ChannelModal {
+    fn has_focus(&self) -> bool {
+        self.has_focus
+    }
+
+    fn dismiss_on_event(event: &Self::Event) -> bool {
+        match event {
+            PickerEvent::Dismiss => true,
+        }
+    }
+}
+
+#[derive(Copy, Clone, PartialEq)]
+pub enum Mode {
+    ManageMembers,
+    InviteMembers,
+}
+
+pub struct ChannelModalDelegate {
+    matching_users: Vec<Arc<User>>,
+    matching_member_indices: Vec<usize>,
+    user_store: ModelHandle<UserStore>,
+    channel_store: ModelHandle<ChannelStore>,
+    channel_id: ChannelId,
+    selected_index: usize,
+    mode: Mode,
+    match_candidates: Vec<StringMatchCandidate>,
+    members: Vec<ChannelMembership>,
+    context_menu: ViewHandle<ContextMenu>,
+}
+
+impl PickerDelegate for ChannelModalDelegate {
+    fn placeholder_text(&self) -> Arc<str> {
+        "Search collaborator by username...".into()
+    }
+
+    fn match_count(&self) -> usize {
+        match self.mode {
+            Mode::ManageMembers => self.matching_member_indices.len(),
+            Mode::InviteMembers => self.matching_users.len(),
+        }
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_index
+    }
+
+    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
+        self.selected_index = ix;
+    }
+
+    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
+        match self.mode {
+            Mode::ManageMembers => {
+                self.match_candidates.clear();
+                self.match_candidates
+                    .extend(self.members.iter().enumerate().map(|(id, member)| {
+                        StringMatchCandidate {
+                            id,
+                            string: member.user.github_login.clone(),
+                            char_bag: member.user.github_login.chars().collect(),
+                        }
+                    }));
+
+                let matches = cx.background().block(match_strings(
+                    &self.match_candidates,
+                    &query,
+                    true,
+                    usize::MAX,
+                    &Default::default(),
+                    cx.background().clone(),
+                ));
+
+                cx.spawn(|picker, mut cx| async move {
+                    picker
+                        .update(&mut cx, |picker, cx| {
+                            let delegate = picker.delegate_mut();
+                            delegate.matching_member_indices.clear();
+                            delegate
+                                .matching_member_indices
+                                .extend(matches.into_iter().map(|m| m.candidate_id));
+                            cx.notify();
+                        })
+                        .ok();
+                })
+            }
+            Mode::InviteMembers => {
+                let search_users = self
+                    .user_store
+                    .update(cx, |store, cx| store.fuzzy_search_users(query, cx));
+                cx.spawn(|picker, mut cx| async move {
+                    async {
+                        let users = search_users.await?;
+                        picker.update(&mut cx, |picker, cx| {
+                            let delegate = picker.delegate_mut();
+                            delegate.matching_users = users;
+                            cx.notify();
+                        })?;
+                        anyhow::Ok(())
+                    }
+                    .log_err()
+                    .await;
+                })
+            }
+        }
+    }
+
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
+        if let Some((selected_user, role)) = self.user_at_index(self.selected_index) {
+            match self.mode {
+                Mode::ManageMembers => {
+                    self.show_context_menu(role.unwrap_or(ChannelRole::Member), cx)
+                }
+                Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
+                    Some(proto::channel_member::Kind::Invitee) => {
+                        self.remove_selected_member(cx);
+                    }
+                    Some(proto::channel_member::Kind::AncestorMember) | None => {
+                        self.invite_member(selected_user, cx)
+                    }
+                    Some(proto::channel_member::Kind::Member) => {}
+                },
+            }
+        }
+    }
+
+    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+        cx.emit(PickerEvent::Dismiss);
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        mouse_state: &mut MouseState,
+        selected: bool,
+        cx: &gpui::AppContext,
+    ) -> AnyElement<Picker<Self>> {
+        let full_theme = &theme::current(cx);
+        let theme = &full_theme.collab_panel.channel_modal;
+        let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
+        let (user, role) = self.user_at_index(ix).unwrap();
+        let request_status = self.member_status(user.id, cx);
+
+        let style = tabbed_modal
+            .picker
+            .item
+            .in_state(selected)
+            .style_for(mouse_state);
+
+        let in_manage = matches!(self.mode, Mode::ManageMembers);
+
+        let mut result = Flex::row()
+            .with_children(user.avatar.clone().map(|avatar| {
+                Image::from_data(avatar)
+                    .with_style(theme.contact_avatar)
+                    .aligned()
+                    .left()
+            }))
+            .with_child(
+                Label::new(user.github_login.clone(), style.label.clone())
+                    .contained()
+                    .with_style(theme.contact_username)
+                    .aligned()
+                    .left(),
+            )
+            .with_children({
+                (in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
+                    || {
+                        Label::new("Invited", theme.member_tag.text.clone())
+                            .contained()
+                            .with_style(theme.member_tag.container)
+                            .aligned()
+                            .left()
+                    },
+                )
+            })
+            .with_children(if in_manage && role == Some(ChannelRole::Admin) {
+                Some(
+                    Label::new("Admin", theme.member_tag.text.clone())
+                        .contained()
+                        .with_style(theme.member_tag.container)
+                        .aligned()
+                        .left(),
+                )
+            } else if in_manage && role == Some(ChannelRole::Guest) {
+                Some(
+                    Label::new("Guest", theme.member_tag.text.clone())
+                        .contained()
+                        .with_style(theme.member_tag.container)
+                        .aligned()
+                        .left(),
+                )
+            } else {
+                None
+            })
+            .with_children({
+                let svg = match self.mode {
+                    Mode::ManageMembers => Some(
+                        Svg::new("icons/ellipsis.svg")
+                            .with_color(theme.member_icon.color)
+                            .constrained()
+                            .with_width(theme.member_icon.icon_width)
+                            .aligned()
+                            .constrained()
+                            .with_width(theme.member_icon.button_width)
+                            .with_height(theme.member_icon.button_width)
+                            .contained()
+                            .with_style(theme.member_icon.container),
+                    ),
+                    Mode::InviteMembers => match request_status {
+                        Some(proto::channel_member::Kind::Member) => Some(
+                            Svg::new("icons/check.svg")
+                                .with_color(theme.member_icon.color)
+                                .constrained()
+                                .with_width(theme.member_icon.icon_width)
+                                .aligned()
+                                .constrained()
+                                .with_width(theme.member_icon.button_width)
+                                .with_height(theme.member_icon.button_width)
+                                .contained()
+                                .with_style(theme.member_icon.container),
+                        ),
+                        Some(proto::channel_member::Kind::Invitee) => Some(
+                            Svg::new("icons/check.svg")
+                                .with_color(theme.invitee_icon.color)
+                                .constrained()
+                                .with_width(theme.invitee_icon.icon_width)
+                                .aligned()
+                                .constrained()
+                                .with_width(theme.invitee_icon.button_width)
+                                .with_height(theme.invitee_icon.button_width)
+                                .contained()
+                                .with_style(theme.invitee_icon.container),
+                        ),
+                        Some(proto::channel_member::Kind::AncestorMember) | None => None,
+                    },
+                };
+
+                svg.map(|svg| svg.aligned().flex_float().into_any())
+            })
+            .contained()
+            .with_style(style.container)
+            .constrained()
+            .with_height(tabbed_modal.row_height)
+            .into_any();
+
+        if selected {
+            result = Stack::new()
+                .with_child(result)
+                .with_child(
+                    ChildView::new(&self.context_menu, cx)
+                        .aligned()
+                        .top()
+                        .right(),
+                )
+                .into_any();
+        }
+
+        result
+    }
+}
+
+impl ChannelModalDelegate {
+    fn member_status(
+        &self,
+        user_id: UserId,
+        cx: &AppContext,
+    ) -> Option<proto::channel_member::Kind> {
+        self.members
+            .iter()
+            .find_map(|membership| (membership.user.id == user_id).then_some(membership.kind))
+            .or_else(|| {
+                self.channel_store
+                    .read(cx)
+                    .has_pending_channel_invite(self.channel_id, user_id)
+                    .then_some(proto::channel_member::Kind::Invitee)
+            })
+    }
+
+    fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<ChannelRole>)> {
+        match self.mode {
+            Mode::ManageMembers => self.matching_member_indices.get(ix).and_then(|ix| {
+                let channel_membership = self.members.get(*ix)?;
+                Some((
+                    channel_membership.user.clone(),
+                    Some(channel_membership.role),
+                ))
+            }),
+            Mode::InviteMembers => Some((self.matching_users.get(ix).cloned()?, None)),
+        }
+    }
+
+    fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
+        let (user, role) = self.user_at_index(self.selected_index)?;
+        let new_role = if role == Some(ChannelRole::Admin) {
+            ChannelRole::Member
+        } else {
+            ChannelRole::Admin
+        };
+        let update = self.channel_store.update(cx, |store, cx| {
+            store.set_member_role(self.channel_id, user.id, new_role, cx)
+        });
+        cx.spawn(|picker, mut cx| async move {
+            update.await?;
+            picker.update(&mut cx, |picker, cx| {
+                let this = picker.delegate_mut();
+                if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
+                    member.role = new_role;
+                }
+                cx.focus_self();
+                cx.notify();
+            })
+        })
+        .detach_and_log_err(cx);
+        Some(())
+    }
+
+    fn remove_selected_member(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
+        let (user, _) = self.user_at_index(self.selected_index)?;
+        let user_id = user.id;
+        let update = self.channel_store.update(cx, |store, cx| {
+            store.remove_member(self.channel_id, user_id, cx)
+        });
+        cx.spawn(|picker, mut cx| async move {
+            update.await?;
+            picker.update(&mut cx, |picker, cx| {
+                let this = picker.delegate_mut();
+                if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) {
+                    this.members.remove(ix);
+                    this.matching_member_indices.retain_mut(|member_ix| {
+                        if *member_ix == ix {
+                            return false;
+                        } else if *member_ix > ix {
+                            *member_ix -= 1;
+                        }
+                        true
+                    })
+                }
+
+                this.selected_index = this
+                    .selected_index
+                    .min(this.matching_member_indices.len().saturating_sub(1));
+
+                cx.focus_self();
+                cx.notify();
+            })
+        })
+        .detach_and_log_err(cx);
+        Some(())
+    }
+
+    fn invite_member(&mut self, user: Arc<User>, cx: &mut ViewContext<Picker<Self>>) {
+        let invite_member = self.channel_store.update(cx, |store, cx| {
+            store.invite_member(self.channel_id, user.id, ChannelRole::Member, cx)
+        });
+
+        cx.spawn(|this, mut cx| async move {
+            invite_member.await?;
+
+            this.update(&mut cx, |this, cx| {
+                let new_member = ChannelMembership {
+                    user,
+                    kind: proto::channel_member::Kind::Invitee,
+                    role: ChannelRole::Member,
+                };
+                let members = &mut this.delegate_mut().members;
+                match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
+                    Ok(ix) | Err(ix) => members.insert(ix, new_member),
+                }
+
+                cx.notify();
+            })
+        })
+        .detach_and_log_err(cx);
+    }
+
+    fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext<Picker<Self>>) {
+        self.context_menu.update(cx, |context_menu, cx| {
+            context_menu.show(
+                Default::default(),
+                AnchorCorner::TopRight,
+                vec![
+                    ContextMenuItem::action("Remove", RemoveMember),
+                    ContextMenuItem::action(
+                        if role == ChannelRole::Admin {
+                            "Make non-admin"
+                        } else {
+                            "Make admin"
+                        },
+                        ToggleMemberAdmin,
+                    ),
+                ],
+                cx,
+            )
+        })
+    }
+}

crates/collab_ui2/src/collab_panel/contact_finder.rs 🔗

@@ -0,0 +1,261 @@
+use client::{ContactRequestStatus, User, UserStore};
+use gpui::{
+    elements::*, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
+};
+use picker::{Picker, PickerDelegate, PickerEvent};
+use std::sync::Arc;
+use util::TryFutureExt;
+use workspace::Modal;
+
+pub fn init(cx: &mut AppContext) {
+    Picker::<ContactFinderDelegate>::init(cx);
+    cx.add_action(ContactFinder::dismiss)
+}
+
+pub struct ContactFinder {
+    picker: ViewHandle<Picker<ContactFinderDelegate>>,
+    has_focus: bool,
+}
+
+impl ContactFinder {
+    pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
+        let picker = cx.add_view(|cx| {
+            Picker::new(
+                ContactFinderDelegate {
+                    user_store,
+                    potential_contacts: Arc::from([]),
+                    selected_index: 0,
+                },
+                cx,
+            )
+            .with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
+        });
+
+        cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
+
+        Self {
+            picker,
+            has_focus: false,
+        }
+    }
+
+    pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
+        self.picker.update(cx, |picker, cx| {
+            picker.set_query(query, cx);
+        });
+    }
+
+    fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
+        cx.emit(PickerEvent::Dismiss);
+    }
+}
+
+impl Entity for ContactFinder {
+    type Event = PickerEvent;
+}
+
+impl View for ContactFinder {
+    fn ui_name() -> &'static str {
+        "ContactFinder"
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        let full_theme = &theme::current(cx);
+        let theme = &full_theme.collab_panel.tabbed_modal;
+
+        fn render_mode_button(
+            text: &'static str,
+            theme: &theme::TabbedModal,
+            _cx: &mut ViewContext<ContactFinder>,
+        ) -> AnyElement<ContactFinder> {
+            let contained_text = &theme.tab_button.active_state().default;
+            Label::new(text, contained_text.text.clone())
+                .contained()
+                .with_style(contained_text.container.clone())
+                .into_any()
+        }
+
+        Flex::column()
+            .with_child(
+                Flex::column()
+                    .with_child(
+                        Label::new("Contacts", theme.title.text.clone())
+                            .contained()
+                            .with_style(theme.title.container.clone()),
+                    )
+                    .with_child(Flex::row().with_children([render_mode_button(
+                        "Invite new contacts",
+                        &theme,
+                        cx,
+                    )]))
+                    .expanded()
+                    .contained()
+                    .with_style(theme.header),
+            )
+            .with_child(
+                ChildView::new(&self.picker, cx)
+                    .contained()
+                    .with_style(theme.body),
+            )
+            .constrained()
+            .with_max_height(theme.max_height)
+            .with_max_width(theme.max_width)
+            .contained()
+            .with_style(theme.modal)
+            .into_any()
+    }
+
+    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+        self.has_focus = true;
+        if cx.is_self_focused() {
+            cx.focus(&self.picker)
+        }
+    }
+
+    fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
+        self.has_focus = false;
+    }
+}
+
+impl Modal for ContactFinder {
+    fn has_focus(&self) -> bool {
+        self.has_focus
+    }
+
+    fn dismiss_on_event(event: &Self::Event) -> bool {
+        match event {
+            PickerEvent::Dismiss => true,
+        }
+    }
+}
+
+pub struct ContactFinderDelegate {
+    potential_contacts: Arc<[Arc<User>]>,
+    user_store: ModelHandle<UserStore>,
+    selected_index: usize,
+}
+
+impl PickerDelegate for ContactFinderDelegate {
+    fn placeholder_text(&self) -> Arc<str> {
+        "Search collaborator by username...".into()
+    }
+
+    fn match_count(&self) -> usize {
+        self.potential_contacts.len()
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_index
+    }
+
+    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
+        self.selected_index = ix;
+    }
+
+    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
+        let search_users = self
+            .user_store
+            .update(cx, |store, cx| store.fuzzy_search_users(query, cx));
+
+        cx.spawn(|picker, mut cx| async move {
+            async {
+                let potential_contacts = search_users.await?;
+                picker.update(&mut cx, |picker, cx| {
+                    picker.delegate_mut().potential_contacts = potential_contacts.into();
+                    cx.notify();
+                })?;
+                anyhow::Ok(())
+            }
+            .log_err()
+            .await;
+        })
+    }
+
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
+        if let Some(user) = self.potential_contacts.get(self.selected_index) {
+            let user_store = self.user_store.read(cx);
+            match user_store.contact_request_status(user) {
+                ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
+                    self.user_store
+                        .update(cx, |store, cx| store.request_contact(user.id, cx))
+                        .detach();
+                }
+                ContactRequestStatus::RequestSent => {
+                    self.user_store
+                        .update(cx, |store, cx| store.remove_contact(user.id, cx))
+                        .detach();
+                }
+                _ => {}
+            }
+        }
+    }
+
+    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+        cx.emit(PickerEvent::Dismiss);
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        mouse_state: &mut MouseState,
+        selected: bool,
+        cx: &gpui::AppContext,
+    ) -> AnyElement<Picker<Self>> {
+        let full_theme = &theme::current(cx);
+        let theme = &full_theme.collab_panel.contact_finder;
+        let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
+        let user = &self.potential_contacts[ix];
+        let request_status = self.user_store.read(cx).contact_request_status(user);
+
+        let icon_path = match request_status {
+            ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
+                Some("icons/check.svg")
+            }
+            ContactRequestStatus::RequestSent => Some("icons/x.svg"),
+            ContactRequestStatus::RequestAccepted => None,
+        };
+        let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
+            &theme.disabled_contact_button
+        } else {
+            &theme.contact_button
+        };
+        let style = tabbed_modal
+            .picker
+            .item
+            .in_state(selected)
+            .style_for(mouse_state);
+        Flex::row()
+            .with_children(user.avatar.clone().map(|avatar| {
+                Image::from_data(avatar)
+                    .with_style(theme.contact_avatar)
+                    .aligned()
+                    .left()
+            }))
+            .with_child(
+                Label::new(user.github_login.clone(), style.label.clone())
+                    .contained()
+                    .with_style(theme.contact_username)
+                    .aligned()
+                    .left(),
+            )
+            .with_children(icon_path.map(|icon_path| {
+                Svg::new(icon_path)
+                    .with_color(button_style.color)
+                    .constrained()
+                    .with_width(button_style.icon_width)
+                    .aligned()
+                    .contained()
+                    .with_style(button_style.container)
+                    .constrained()
+                    .with_width(button_style.button_width)
+                    .with_height(button_style.button_width)
+                    .aligned()
+                    .flex_float()
+            }))
+            .contained()
+            .with_style(style.container)
+            .constrained()
+            .with_height(tabbed_modal.row_height)
+            .into_any()
+    }
+}

crates/collab_ui2/src/collab_titlebar_item.rs 🔗

@@ -0,0 +1,1371 @@
+// use crate::{
+//     face_pile::FacePile, toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall,
+//     ToggleDeafen, ToggleMute, ToggleScreenSharing,
+// };
+// use auto_update::AutoUpdateStatus;
+// use call::{ActiveCall, ParticipantLocation, Room};
+// use client::{proto::PeerId, Client, SignIn, SignOut, User, UserStore};
+// use clock::ReplicaId;
+// use context_menu::{ContextMenu, ContextMenuItem};
+// use gpui::{
+//     actions,
+//     color::Color,
+//     elements::*,
+//     geometry::{rect::RectF, vector::vec2f, PathBuilder},
+//     json::{self, ToJson},
+//     platform::{CursorStyle, MouseButton},
+//     AppContext, Entity, ImageData, ModelHandle, Subscription, View, ViewContext, ViewHandle,
+//     WeakViewHandle,
+// };
+// use picker::PickerEvent;
+// use project::{Project, RepositoryEntry};
+// use recent_projects::{build_recent_projects, RecentProjects};
+// use std::{ops::Range, sync::Arc};
+// use theme::{AvatarStyle, Theme};
+// use util::ResultExt;
+// use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
+// use workspace::{FollowNextCollaborator, Workspace, WORKSPACE_DB};
+
+use std::sync::Arc;
+
+use call::ActiveCall;
+use client::{Client, UserStore};
+use gpui::{
+    div, px, rems, AppContext, Component, Div, InteractiveComponent, Model, ParentComponent,
+    Render, Stateful, StatefulInteractiveComponent, Styled, Subscription, ViewContext,
+    VisualContext, WeakView, WindowBounds,
+};
+use project::Project;
+use theme::ActiveTheme;
+use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextColor, Tooltip};
+use workspace::Workspace;
+
+// const MAX_PROJECT_NAME_LENGTH: usize = 40;
+// const MAX_BRANCH_NAME_LENGTH: usize = 40;
+
+// actions!(
+//     collab,
+//     [
+//         ToggleUserMenu,
+//         ToggleProjectMenu,
+//         SwitchBranch,
+//         ShareProject,
+//         UnshareProject,
+//     ]
+// );
+
+pub fn init(cx: &mut AppContext) {
+    cx.observe_new_views(|workspace: &mut Workspace, cx| {
+        let titlebar_item = cx.build_view(|cx| CollabTitlebarItem::new(workspace, cx));
+        workspace.set_titlebar_item(titlebar_item.into(), cx)
+    })
+    .detach();
+    // cx.add_action(CollabTitlebarItem::share_project);
+    // cx.add_action(CollabTitlebarItem::unshare_project);
+    // cx.add_action(CollabTitlebarItem::toggle_user_menu);
+    // cx.add_action(CollabTitlebarItem::toggle_vcs_menu);
+    // cx.add_action(CollabTitlebarItem::toggle_project_menu);
+}
+
+pub struct CollabTitlebarItem {
+    project: Model<Project>,
+    #[allow(unused)] // todo!()
+    user_store: Model<UserStore>,
+    #[allow(unused)] // todo!()
+    client: Arc<Client>,
+    #[allow(unused)] // todo!()
+    workspace: WeakView<Workspace>,
+    //branch_popover: Option<ViewHandle<BranchList>>,
+    //project_popover: Option<ViewHandle<recent_projects::RecentProjects>>,
+    //user_menu: ViewHandle<ContextMenu>,
+    _subscriptions: Vec<Subscription>,
+}
+
+impl Render for CollabTitlebarItem {
+    type Element = Stateful<Self, Div<Self>>;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        h_stack()
+            .id("titlebar")
+            .justify_between()
+            .w_full()
+            .h(rems(1.75))
+            // Set a non-scaling min-height here to ensure the titlebar is
+            // always at least the height of the traffic lights.
+            .min_h(px(32.))
+            .when(
+                !matches!(cx.window_bounds(), WindowBounds::Fullscreen),
+                // Use pixels here instead of a rem-based size because the macOS traffic
+                // lights are a static size, and don't scale with the rest of the UI.
+                |s| s.pl(px(68.)),
+            )
+            .bg(cx.theme().colors().title_bar_background)
+            .on_click(|_, event, cx| {
+                if event.up.click_count == 2 {
+                    cx.zoom_window();
+                }
+            })
+            .child(
+                h_stack()
+                    .gap_1()
+                    // TODO - Add player menu
+                    .child(
+                        div()
+                            .id("project_owner_indicator")
+                            .child(
+                                Button::new("player")
+                                    .variant(ButtonVariant::Ghost)
+                                    .color(Some(TextColor::Player(0))),
+                            )
+                            .tooltip(move |_, cx| Tooltip::text("Toggle following", cx)),
+                    )
+                    // TODO - Add project menu
+                    .child(
+                        div()
+                            .id("titlebar_project_menu_button")
+                            .child(Button::new("project_name").variant(ButtonVariant::Ghost))
+                            .tooltip(move |_, cx| Tooltip::text("Recent Projects", cx)),
+                    )
+                    // TODO - Add git menu
+                    .child(
+                        div()
+                            .id("titlebar_git_menu_button")
+                            .child(
+                                Button::new("branch_name")
+                                    .variant(ButtonVariant::Ghost)
+                                    .color(Some(TextColor::Muted)),
+                            )
+                            .tooltip(move |_, cx| {
+                                cx.build_view(|_| {
+                                    Tooltip::new("Recent Branches")
+                                        .key_binding(KeyBinding::new(gpui::KeyBinding::new(
+                                            "cmd-b",
+                                            // todo!() Replace with real action.
+                                            gpui::NoAction,
+                                            None,
+                                        )))
+                                        .meta("Only local branches shown")
+                                })
+                                .into()
+                            }),
+                    ),
+            ) // self.titlebar_item
+            .child(h_stack().child(Label::new("Right side titlebar item")))
+    }
+}
+
+// impl Entity for CollabTitlebarItem {
+//     type Event = ();
+// }
+
+// impl View for CollabTitlebarItem {
+//     fn ui_name() -> &'static str {
+//         "CollabTitlebarItem"
+//     }
+
+//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+//         let workspace = if let Some(workspace) = self.workspace.upgrade(cx) {
+//             workspace
+//         } else {
+//             return Empty::new().into_any();
+//         };
+
+//         let theme = theme::current(cx).clone();
+//         let mut left_container = Flex::row();
+//         let mut right_container = Flex::row().align_children_center();
+
+//         left_container.add_child(self.collect_title_root_names(theme.clone(), cx));
+
+//         let user = self.user_store.read(cx).current_user();
+//         let peer_id = self.client.peer_id();
+//         if let Some(((user, peer_id), room)) = user
+//             .as_ref()
+//             .zip(peer_id)
+//             .zip(ActiveCall::global(cx).read(cx).room().cloned())
+//         {
+//             if room.read(cx).can_publish() {
+//                 right_container
+//                     .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
+//             }
+//             right_container.add_child(self.render_leave_call(&theme, cx));
+//             let muted = room.read(cx).is_muted(cx);
+//             let speaking = room.read(cx).is_speaking();
+//             left_container.add_child(
+//                 self.render_current_user(&workspace, &theme, &user, peer_id, muted, speaking, cx),
+//             );
+//             left_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx));
+//             if room.read(cx).can_publish() {
+//                 right_container.add_child(self.render_toggle_mute(&theme, &room, cx));
+//             }
+//             right_container.add_child(self.render_toggle_deafen(&theme, &room, cx));
+//             if room.read(cx).can_publish() {
+//                 right_container
+//                     .add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
+//             }
+//         }
+
+//         let status = workspace.read(cx).client().status();
+//         let status = &*status.borrow();
+//         if matches!(status, client::Status::Connected { .. }) {
+//             let avatar = user.as_ref().and_then(|user| user.avatar.clone());
+//             right_container.add_child(self.render_user_menu_button(&theme, avatar, cx));
+//         } else {
+//             right_container.add_children(self.render_connection_status(status, cx));
+//             right_container.add_child(self.render_sign_in_button(&theme, cx));
+//             right_container.add_child(self.render_user_menu_button(&theme, None, cx));
+//         }
+
+//         Stack::new()
+//             .with_child(left_container)
+//             .with_child(
+//                 Flex::row()
+//                     .with_child(
+//                         right_container.contained().with_background_color(
+//                             theme
+//                                 .titlebar
+//                                 .container
+//                                 .background_color
+//                                 .unwrap_or_else(|| Color::transparent_black()),
+//                         ),
+//                     )
+//                     .aligned()
+//                     .right(),
+//             )
+//             .into_any()
+//     }
+// }
+
+impl CollabTitlebarItem {
+    pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
+        let project = workspace.project().clone();
+        let user_store = workspace.app_state().user_store.clone();
+        let client = workspace.app_state().client.clone();
+        let active_call = ActiveCall::global(cx);
+        let mut subscriptions = Vec::new();
+        subscriptions.push(
+            cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| {
+                cx.notify()
+            }),
+        );
+        subscriptions.push(cx.observe(&project, |_, _, cx| cx.notify()));
+        subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
+        subscriptions.push(cx.observe_window_activation(Self::window_activation_changed));
+        subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
+
+        Self {
+            workspace: workspace.weak_handle(),
+            project,
+            user_store,
+            client,
+            //         user_menu: cx.add_view(|cx| {
+            //             let view_id = cx.view_id();
+            //             let mut menu = ContextMenu::new(view_id, cx);
+            //             menu.set_position_mode(OverlayPositionMode::Local);
+            //             menu
+            //         }),
+            //         branch_popover: None,
+            //         project_popover: None,
+            _subscriptions: subscriptions,
+        }
+    }
+
+    // fn collect_title_root_names(
+    //     &self,
+    //     theme: Arc<Theme>,
+    //     cx: &mut ViewContext<Self>,
+    // ) -> AnyElement<Self> {
+    //     let project = self.project.read(cx);
+
+    //     let (name, entry) = {
+    //         let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| {
+    //             let worktree = worktree.read(cx);
+    //             (worktree.root_name(), worktree.root_git_entry())
+    //         });
+
+    //         names_and_branches.next().unwrap_or(("", None))
+    //     };
+
+    //     let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
+    //     let branch_prepended = entry
+    //         .as_ref()
+    //         .and_then(RepositoryEntry::branch)
+    //         .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH));
+    //     let project_style = theme.titlebar.project_menu_button.clone();
+    //     let git_style = theme.titlebar.git_menu_button.clone();
+    //     let item_spacing = theme.titlebar.item_spacing;
+
+    //     let mut ret = Flex::row();
+
+    //     if let Some(project_host) = self.collect_project_host(theme.clone(), cx) {
+    //         ret = ret.with_child(project_host)
+    //     }
+
+    //     ret = ret.with_child(
+    //         Stack::new()
+    //             .with_child(
+    //                 MouseEventHandler::new::<ToggleProjectMenu, _>(0, cx, |mouse_state, cx| {
+    //                     let style = project_style
+    //                         .in_state(self.project_popover.is_some())
+    //                         .style_for(mouse_state);
+    //                     enum RecentProjectsTooltip {}
+    //                     Label::new(name, style.text.clone())
+    //                         .contained()
+    //                         .with_style(style.container)
+    //                         .aligned()
+    //                         .left()
+    //                         .with_tooltip::<RecentProjectsTooltip>(
+    //                             0,
+    //                             "Recent projects",
+    //                             Some(Box::new(recent_projects::OpenRecent)),
+    //                             theme.tooltip.clone(),
+    //                             cx,
+    //                         )
+    //                         .into_any_named("title-project-name")
+    //                 })
+    //                 .with_cursor_style(CursorStyle::PointingHand)
+    //                 .on_down(MouseButton::Left, move |_, this, cx| {
+    //                     this.toggle_project_menu(&Default::default(), cx)
+    //                 })
+    //                 .on_click(MouseButton::Left, move |_, _, _| {}),
+    //             )
+    //             .with_children(self.render_project_popover_host(&theme.titlebar, cx)),
+    //     );
+    //     if let Some(git_branch) = branch_prepended {
+    //         ret = ret.with_child(
+    //             Flex::row().with_child(
+    //                 Stack::new()
+    //                     .with_child(
+    //                         MouseEventHandler::new::<ToggleVcsMenu, _>(0, cx, |mouse_state, cx| {
+    //                             enum BranchPopoverTooltip {}
+    //                             let style = git_style
+    //                                 .in_state(self.branch_popover.is_some())
+    //                                 .style_for(mouse_state);
+    //                             Label::new(git_branch, style.text.clone())
+    //                                 .contained()
+    //                                 .with_style(style.container.clone())
+    //                                 .with_margin_right(item_spacing)
+    //                                 .aligned()
+    //                                 .left()
+    //                                 .with_tooltip::<BranchPopoverTooltip>(
+    //                                     0,
+    //                                     "Recent branches",
+    //                                     Some(Box::new(ToggleVcsMenu)),
+    //                                     theme.tooltip.clone(),
+    //                                     cx,
+    //                                 )
+    //                                 .into_any_named("title-project-branch")
+    //                         })
+    //                         .with_cursor_style(CursorStyle::PointingHand)
+    //                         .on_down(MouseButton::Left, move |_, this, cx| {
+    //                             this.toggle_vcs_menu(&Default::default(), cx)
+    //                         })
+    //                         .on_click(MouseButton::Left, move |_, _, _| {}),
+    //                     )
+    //                     .with_children(self.render_branches_popover_host(&theme.titlebar, cx)),
+    //             ),
+    //         )
+    //     }
+    //     ret.into_any()
+    // }
+
+    // fn collect_project_host(
+    //     &self,
+    //     theme: Arc<Theme>,
+    //     cx: &mut ViewContext<Self>,
+    // ) -> Option<AnyElement<Self>> {
+    //     if ActiveCall::global(cx).read(cx).room().is_none() {
+    //         return None;
+    //     }
+    //     let project = self.project.read(cx);
+    //     let user_store = self.user_store.read(cx);
+
+    //     if project.is_local() {
+    //         return None;
+    //     }
+
+    //     let Some(host) = project.host() else {
+    //         return None;
+    //     };
+    //     let (Some(host_user), Some(participant_index)) = (
+    //         user_store.get_cached_user(host.user_id),
+    //         user_store.participant_indices().get(&host.user_id),
+    //     ) else {
+    //         return None;
+    //     };
+
+    //     enum ProjectHost {}
+    //     enum ProjectHostTooltip {}
+
+    //     let host_style = theme.titlebar.project_host.clone();
+    //     let selection_style = theme
+    //         .editor
+    //         .selection_style_for_room_participant(participant_index.0);
+    //     let peer_id = host.peer_id.clone();
+
+    //     Some(
+    //         MouseEventHandler::new::<ProjectHost, _>(0, cx, |mouse_state, _| {
+    //             let mut host_style = host_style.style_for(mouse_state).clone();
+    //             host_style.text.color = selection_style.cursor;
+    //             Label::new(host_user.github_login.clone(), host_style.text)
+    //                 .contained()
+    //                 .with_style(host_style.container)
+    //                 .aligned()
+    //                 .left()
+    //         })
+    //         .with_cursor_style(CursorStyle::PointingHand)
+    //         .on_click(MouseButton::Left, move |_, this, cx| {
+    //             if let Some(workspace) = this.workspace.upgrade(cx) {
+    //                 if let Some(task) =
+    //                     workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
+    //                 {
+    //                     task.detach_and_log_err(cx);
+    //                 }
+    //             }
+    //         })
+    //         .with_tooltip::<ProjectHostTooltip>(
+    //             0,
+    //             host_user.github_login.clone() + " is sharing this project. Click to follow.",
+    //             None,
+    //             theme.tooltip.clone(),
+    //             cx,
+    //         )
+    //         .into_any_named("project-host"),
+    //     )
+    // }
+
+    fn window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
+        let project = if cx.is_window_active() {
+            Some(self.project.clone())
+        } else {
+            None
+        };
+        ActiveCall::global(cx)
+            .update(cx, |call, cx| call.set_location(project.as_ref(), cx))
+            .detach_and_log_err(cx);
+    }
+
+    fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
+        cx.notify();
+    }
+
+    // fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
+    //     let active_call = ActiveCall::global(cx);
+    //     let project = self.project.clone();
+    //     active_call
+    //         .update(cx, |call, cx| call.share_project(project, cx))
+    //         .detach_and_log_err(cx);
+    // }
+
+    // fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
+    //     let active_call = ActiveCall::global(cx);
+    //     let project = self.project.clone();
+    //     active_call
+    //         .update(cx, |call, cx| call.unshare_project(project, cx))
+    //         .log_err();
+    // }
+
+    // pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
+    //     self.user_menu.update(cx, |user_menu, cx| {
+    //         let items = if let Some(_) = self.user_store.read(cx).current_user() {
+    //             vec![
+    //                 ContextMenuItem::action("Settings", zed_actions::OpenSettings),
+    //                 ContextMenuItem::action("Theme", theme_selector::Toggle),
+    //                 ContextMenuItem::separator(),
+    //                 ContextMenuItem::action(
+    //                     "Share Feedback",
+    //                     feedback::feedback_editor::GiveFeedback,
+    //                 ),
+    //                 ContextMenuItem::action("Sign Out", SignOut),
+    //             ]
+    //         } else {
+    //             vec![
+    //                 ContextMenuItem::action("Settings", zed_actions::OpenSettings),
+    //                 ContextMenuItem::action("Theme", theme_selector::Toggle),
+    //                 ContextMenuItem::separator(),
+    //                 ContextMenuItem::action(
+    //                     "Share Feedback",
+    //                     feedback::feedback_editor::GiveFeedback,
+    //                 ),
+    //             ]
+    //         };
+    //         user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
+    //     });
+    // }
+
+    // fn render_branches_popover_host<'a>(
+    //     &'a self,
+    //     _theme: &'a theme::Titlebar,
+    //     cx: &'a mut ViewContext<Self>,
+    // ) -> Option<AnyElement<Self>> {
+    //     self.branch_popover.as_ref().map(|child| {
+    //         let theme = theme::current(cx).clone();
+    //         let child = ChildView::new(child, cx);
+    //         let child = MouseEventHandler::new::<BranchList, _>(0, cx, |_, _| {
+    //             child
+    //                 .flex(1., true)
+    //                 .contained()
+    //                 .constrained()
+    //                 .with_width(theme.titlebar.menu.width)
+    //                 .with_height(theme.titlebar.menu.height)
+    //         })
+    //         .on_click(MouseButton::Left, |_, _, _| {})
+    //         .on_down_out(MouseButton::Left, move |_, this, cx| {
+    //             this.branch_popover.take();
+    //             cx.emit(());
+    //             cx.notify();
+    //         })
+    //         .contained()
+    //         .into_any();
+
+    //         Overlay::new(child)
+    //             .with_fit_mode(OverlayFitMode::SwitchAnchor)
+    //             .with_anchor_corner(AnchorCorner::TopLeft)
+    //             .with_z_index(999)
+    //             .aligned()
+    //             .bottom()
+    //             .left()
+    //             .into_any()
+    //     })
+    // }
+
+    // fn render_project_popover_host<'a>(
+    //     &'a self,
+    //     _theme: &'a theme::Titlebar,
+    //     cx: &'a mut ViewContext<Self>,
+    // ) -> Option<AnyElement<Self>> {
+    //     self.project_popover.as_ref().map(|child| {
+    //         let theme = theme::current(cx).clone();
+    //         let child = ChildView::new(child, cx);
+    //         let child = MouseEventHandler::new::<RecentProjects, _>(0, cx, |_, _| {
+    //             child
+    //                 .flex(1., true)
+    //                 .contained()
+    //                 .constrained()
+    //                 .with_width(theme.titlebar.menu.width)
+    //                 .with_height(theme.titlebar.menu.height)
+    //         })
+    //         .on_click(MouseButton::Left, |_, _, _| {})
+    //         .on_down_out(MouseButton::Left, move |_, this, cx| {
+    //             this.project_popover.take();
+    //             cx.emit(());
+    //             cx.notify();
+    //         })
+    //         .into_any();
+
+    //         Overlay::new(child)
+    //             .with_fit_mode(OverlayFitMode::SwitchAnchor)
+    //             .with_anchor_corner(AnchorCorner::TopLeft)
+    //             .with_z_index(999)
+    //             .aligned()
+    //             .bottom()
+    //             .left()
+    //             .into_any()
+    //     })
+    // }
+
+    // pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
+    //     if self.branch_popover.take().is_none() {
+    //         if let Some(workspace) = self.workspace.upgrade(cx) {
+    //             let Some(view) =
+    //                 cx.add_option_view(|cx| build_branch_list(workspace, cx).log_err())
+    //             else {
+    //                 return;
+    //             };
+    //             cx.subscribe(&view, |this, _, event, cx| {
+    //                 match event {
+    //                     PickerEvent::Dismiss => {
+    //                         this.branch_popover = None;
+    //                     }
+    //                 }
+
+    //                 cx.notify();
+    //             })
+    //             .detach();
+    //             self.project_popover.take();
+    //             cx.focus(&view);
+    //             self.branch_popover = Some(view);
+    //         }
+    //     }
+
+    //     cx.notify();
+    // }
+
+    // pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext<Self>) {
+    //     let workspace = self.workspace.clone();
+    //     if self.project_popover.take().is_none() {
+    //         cx.spawn(|this, mut cx| async move {
+    //             let workspaces = WORKSPACE_DB
+    //                 .recent_workspaces_on_disk()
+    //                 .await
+    //                 .unwrap_or_default()
+    //                 .into_iter()
+    //                 .map(|(_, location)| location)
+    //                 .collect();
+
+    //             let workspace = workspace.clone();
+    //             this.update(&mut cx, move |this, cx| {
+    //                 let view = cx.add_view(|cx| build_recent_projects(workspace, workspaces, cx));
+
+    //                 cx.subscribe(&view, |this, _, event, cx| {
+    //                     match event {
+    //                         PickerEvent::Dismiss => {
+    //                             this.project_popover = None;
+    //                         }
+    //                     }
+
+    //                     cx.notify();
+    //                 })
+    //                 .detach();
+    //                 cx.focus(&view);
+    //                 this.branch_popover.take();
+    //                 this.project_popover = Some(view);
+    //                 cx.notify();
+    //             })
+    //             .log_err();
+    //         })
+    //         .detach();
+    //     }
+    //     cx.notify();
+    // }
+
+    // fn render_toggle_screen_sharing_button(
+    //     &self,
+    //     theme: &Theme,
+    //     room: &ModelHandle<Room>,
+    //     cx: &mut ViewContext<Self>,
+    // ) -> AnyElement<Self> {
+    //     let icon;
+    //     let tooltip;
+    //     if room.read(cx).is_screen_sharing() {
+    //         icon = "icons/desktop.svg";
+    //         tooltip = "Stop Sharing Screen"
+    //     } else {
+    //         icon = "icons/desktop.svg";
+    //         tooltip = "Share Screen";
+    //     }
+
+    //     let active = room.read(cx).is_screen_sharing();
+    //     let titlebar = &theme.titlebar;
+    //     MouseEventHandler::new::<ToggleScreenSharing, _>(0, cx, |state, _| {
+    //         let style = titlebar
+    //             .screen_share_button
+    //             .in_state(active)
+    //             .style_for(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)
+    //             .contained()
+    //             .with_style(style.container)
+    //     })
+    //     .with_cursor_style(CursorStyle::PointingHand)
+    //     .on_click(MouseButton::Left, move |_, _, cx| {
+    //         toggle_screen_sharing(&Default::default(), cx)
+    //     })
+    //     .with_tooltip::<ToggleScreenSharing>(
+    //         0,
+    //         tooltip,
+    //         Some(Box::new(ToggleScreenSharing)),
+    //         theme.tooltip.clone(),
+    //         cx,
+    //     )
+    //     .aligned()
+    //     .into_any()
+    // }
+    // fn render_toggle_mute(
+    //     &self,
+    //     theme: &Theme,
+    //     room: &ModelHandle<Room>,
+    //     cx: &mut ViewContext<Self>,
+    // ) -> AnyElement<Self> {
+    //     let icon;
+    //     let tooltip;
+    //     let is_muted = room.read(cx).is_muted(cx);
+    //     if is_muted {
+    //         icon = "icons/mic-mute.svg";
+    //         tooltip = "Unmute microphone";
+    //     } else {
+    //         icon = "icons/mic.svg";
+    //         tooltip = "Mute microphone";
+    //     }
+
+    //     let titlebar = &theme.titlebar;
+    //     MouseEventHandler::new::<ToggleMute, _>(0, cx, |state, _| {
+    //         let style = titlebar
+    //             .toggle_microphone_button
+    //             .in_state(is_muted)
+    //             .style_for(state);
+    //         let image = 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)
+    //             .contained()
+    //             .with_style(style.container);
+    //         if let Some(color) = style.container.background_color {
+    //             image.with_background_color(color)
+    //         } else {
+    //             image
+    //         }
+    //     })
+    //     .with_cursor_style(CursorStyle::PointingHand)
+    //     .on_click(MouseButton::Left, move |_, _, cx| {
+    //         toggle_mute(&Default::default(), cx)
+    //     })
+    //     .with_tooltip::<ToggleMute>(
+    //         0,
+    //         tooltip,
+    //         Some(Box::new(ToggleMute)),
+    //         theme.tooltip.clone(),
+    //         cx,
+    //     )
+    //     .aligned()
+    //     .into_any()
+    // }
+    // fn render_toggle_deafen(
+    //     &self,
+    //     theme: &Theme,
+    //     room: &ModelHandle<Room>,
+    //     cx: &mut ViewContext<Self>,
+    // ) -> AnyElement<Self> {
+    //     let icon;
+    //     let tooltip;
+    //     let is_deafened = room.read(cx).is_deafened().unwrap_or(false);
+    //     if is_deafened {
+    //         icon = "icons/speaker-off.svg";
+    //         tooltip = "Unmute speakers";
+    //     } else {
+    //         icon = "icons/speaker-loud.svg";
+    //         tooltip = "Mute speakers";
+    //     }
+
+    //     let titlebar = &theme.titlebar;
+    //     MouseEventHandler::new::<ToggleDeafen, _>(0, cx, |state, _| {
+    //         let style = titlebar
+    //             .toggle_speakers_button
+    //             .in_state(is_deafened)
+    //             .style_for(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)
+    //             .contained()
+    //             .with_style(style.container)
+    //     })
+    //     .with_cursor_style(CursorStyle::PointingHand)
+    //     .on_click(MouseButton::Left, move |_, _, cx| {
+    //         toggle_deafen(&Default::default(), cx)
+    //     })
+    //     .with_tooltip::<ToggleDeafen>(
+    //         0,
+    //         tooltip,
+    //         Some(Box::new(ToggleDeafen)),
+    //         theme.tooltip.clone(),
+    //         cx,
+    //     )
+    //     .aligned()
+    //     .into_any()
+    // }
+    // fn render_leave_call(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+    //     let icon = "icons/exit.svg";
+    //     let tooltip = "Leave call";
+
+    //     let titlebar = &theme.titlebar;
+    //     MouseEventHandler::new::<LeaveCall, _>(0, cx, |state, _| {
+    //         let style = titlebar.leave_call_button.style_for(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)
+    //             .contained()
+    //             .with_style(style.container)
+    //     })
+    //     .with_cursor_style(CursorStyle::PointingHand)
+    //     .on_click(MouseButton::Left, move |_, _, cx| {
+    //         ActiveCall::global(cx)
+    //             .update(cx, |call, cx| call.hang_up(cx))
+    //             .detach_and_log_err(cx);
+    //     })
+    //     .with_tooltip::<LeaveCall>(
+    //         0,
+    //         tooltip,
+    //         Some(Box::new(LeaveCall)),
+    //         theme.tooltip.clone(),
+    //         cx,
+    //     )
+    //     .aligned()
+    //     .into_any()
+    // }
+    // fn render_in_call_share_unshare_button(
+    //     &self,
+    //     workspace: &ViewHandle<Workspace>,
+    //     theme: &Theme,
+    //     cx: &mut ViewContext<Self>,
+    // ) -> Option<AnyElement<Self>> {
+    //     let project = workspace.read(cx).project();
+    //     if project.read(cx).is_remote() {
+    //         return None;
+    //     }
+
+    //     let is_shared = project.read(cx).is_shared();
+    //     let label = if is_shared { "Stop Sharing" } else { "Share" };
+    //     let tooltip = if is_shared {
+    //         "Stop sharing project with call participants"
+    //     } else {
+    //         "Share project with call participants"
+    //     };
+
+    //     let titlebar = &theme.titlebar;
+
+    //     enum ShareUnshare {}
+    //     Some(
+    //         Stack::new()
+    //             .with_child(
+    //                 MouseEventHandler::new::<ShareUnshare, _>(0, cx, |state, _| {
+    //                     //TODO: Ensure this button has consistent width for both text variations
+    //                     let style = titlebar.share_button.inactive_state().style_for(state);
+    //                     Label::new(label, style.text.clone())
+    //                         .contained()
+    //                         .with_style(style.container)
+    //                 })
+    //                 .with_cursor_style(CursorStyle::PointingHand)
+    //                 .on_click(MouseButton::Left, move |_, this, cx| {
+    //                     if is_shared {
+    //                         this.unshare_project(&Default::default(), cx);
+    //                     } else {
+    //                         this.share_project(&Default::default(), cx);
+    //                     }
+    //                 })
+    //                 .with_tooltip::<ShareUnshare>(
+    //                     0,
+    //                     tooltip.to_owned(),
+    //                     None,
+    //                     theme.tooltip.clone(),
+    //                     cx,
+    //                 ),
+    //             )
+    //             .aligned()
+    //             .contained()
+    //             .with_margin_left(theme.titlebar.item_spacing)
+    //             .into_any(),
+    //     )
+    // }
+
+    // fn render_user_menu_button(
+    //     &self,
+    //     theme: &Theme,
+    //     avatar: Option<Arc<ImageData>>,
+    //     cx: &mut ViewContext<Self>,
+    // ) -> AnyElement<Self> {
+    //     let tooltip = theme.tooltip.clone();
+    //     let user_menu_button_style = if avatar.is_some() {
+    //         &theme.titlebar.user_menu.user_menu_button_online
+    //     } else {
+    //         &theme.titlebar.user_menu.user_menu_button_offline
+    //     };
+
+    //     let avatar_style = &user_menu_button_style.avatar;
+    //     Stack::new()
+    //         .with_child(
+    //             MouseEventHandler::new::<ToggleUserMenu, _>(0, cx, |state, _| {
+    //                 let style = user_menu_button_style
+    //                     .user_menu
+    //                     .inactive_state()
+    //                     .style_for(state);
+
+    //                 let mut dropdown = Flex::row().align_children_center();
+
+    //                 if let Some(avatar_img) = avatar {
+    //                     dropdown = dropdown.with_child(Self::render_face(
+    //                         avatar_img,
+    //                         *avatar_style,
+    //                         Color::transparent_black(),
+    //                         None,
+    //                     ));
+    //                 };
+
+    //                 dropdown
+    //                     .with_child(
+    //                         Svg::new("icons/caret_down.svg")
+    //                             .with_color(user_menu_button_style.icon.color)
+    //                             .constrained()
+    //                             .with_width(user_menu_button_style.icon.width)
+    //                             .contained()
+    //                             .into_any(),
+    //                     )
+    //                     .aligned()
+    //                     .constrained()
+    //                     .with_height(style.width)
+    //                     .contained()
+    //                     .with_style(style.container)
+    //                     .into_any()
+    //             })
+    //             .with_cursor_style(CursorStyle::PointingHand)
+    //             .on_down(MouseButton::Left, move |_, this, cx| {
+    //                 this.user_menu.update(cx, |menu, _| menu.delay_cancel());
+    //             })
+    //             .on_click(MouseButton::Left, move |_, this, cx| {
+    //                 this.toggle_user_menu(&Default::default(), cx)
+    //             })
+    //             .with_tooltip::<ToggleUserMenu>(
+    //                 0,
+    //                 "Toggle User Menu".to_owned(),
+    //                 Some(Box::new(ToggleUserMenu)),
+    //                 tooltip,
+    //                 cx,
+    //             )
+    //             .contained(),
+    //         )
+    //         .with_child(
+    //             ChildView::new(&self.user_menu, cx)
+    //                 .aligned()
+    //                 .bottom()
+    //                 .right(),
+    //         )
+    //         .into_any()
+    // }
+
+    // fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+    //     let titlebar = &theme.titlebar;
+    //     MouseEventHandler::new::<SignIn, _>(0, cx, |state, _| {
+    //         let style = titlebar.sign_in_button.inactive_state().style_for(state);
+    //         Label::new("Sign In", style.text.clone())
+    //             .contained()
+    //             .with_style(style.container)
+    //     })
+    //     .with_cursor_style(CursorStyle::PointingHand)
+    //     .on_click(MouseButton::Left, move |_, this, cx| {
+    //         let client = this.client.clone();
+    //         cx.app_context()
+    //             .spawn(|cx| async move { client.authenticate_and_connect(true, &cx).await })
+    //             .detach_and_log_err(cx);
+    //     })
+    //     .into_any()
+    // }
+
+    // fn render_collaborators(
+    //     &self,
+    //     workspace: &ViewHandle<Workspace>,
+    //     theme: &Theme,
+    //     room: &ModelHandle<Room>,
+    //     cx: &mut ViewContext<Self>,
+    // ) -> Vec<Container<Self>> {
+    //     let mut participants = room
+    //         .read(cx)
+    //         .remote_participants()
+    //         .values()
+    //         .cloned()
+    //         .collect::<Vec<_>>();
+    //     participants.sort_by_cached_key(|p| p.user.github_login.clone());
+
+    //     participants
+    //         .into_iter()
+    //         .filter_map(|participant| {
+    //             let project = workspace.read(cx).project().read(cx);
+    //             let replica_id = project
+    //                 .collaborators()
+    //                 .get(&participant.peer_id)
+    //                 .map(|collaborator| collaborator.replica_id);
+    //             let user = participant.user.clone();
+    //             Some(
+    //                 Container::new(self.render_face_pile(
+    //                     &user,
+    //                     replica_id,
+    //                     participant.peer_id,
+    //                     Some(participant.location),
+    //                     participant.muted,
+    //                     participant.speaking,
+    //                     workspace,
+    //                     theme,
+    //                     cx,
+    //                 ))
+    //                 .with_margin_right(theme.titlebar.face_pile_spacing),
+    //             )
+    //         })
+    //         .collect()
+    // }
+
+    // fn render_current_user(
+    //     &self,
+    //     workspace: &ViewHandle<Workspace>,
+    //     theme: &Theme,
+    //     user: &Arc<User>,
+    //     peer_id: PeerId,
+    //     muted: bool,
+    //     speaking: bool,
+    //     cx: &mut ViewContext<Self>,
+    // ) -> AnyElement<Self> {
+    //     let replica_id = workspace.read(cx).project().read(cx).replica_id();
+
+    //     Container::new(self.render_face_pile(
+    //         user,
+    //         Some(replica_id),
+    //         peer_id,
+    //         None,
+    //         muted,
+    //         speaking,
+    //         workspace,
+    //         theme,
+    //         cx,
+    //     ))
+    //     .with_margin_right(theme.titlebar.item_spacing)
+    //     .into_any()
+    // }
+
+    // fn render_face_pile(
+    //     &self,
+    //     user: &User,
+    //     _replica_id: Option<ReplicaId>,
+    //     peer_id: PeerId,
+    //     location: Option<ParticipantLocation>,
+    //     muted: bool,
+    //     speaking: bool,
+    //     workspace: &ViewHandle<Workspace>,
+    //     theme: &Theme,
+    //     cx: &mut ViewContext<Self>,
+    // ) -> AnyElement<Self> {
+    //     let user_id = user.id;
+    //     let project_id = workspace.read(cx).project().read(cx).remote_id();
+    //     let room = ActiveCall::global(cx).read(cx).room().cloned();
+    //     let self_peer_id = workspace.read(cx).client().peer_id();
+    //     let self_following = workspace.read(cx).is_being_followed(peer_id);
+    //     let self_following_initialized = self_following
+    //         && room.as_ref().map_or(false, |room| match project_id {
+    //             None => true,
+    //             Some(project_id) => room
+    //                 .read(cx)
+    //                 .followers_for(peer_id, project_id)
+    //                 .iter()
+    //                 .any(|&follower| Some(follower) == self_peer_id),
+    //         });
+
+    //     let leader_style = theme.titlebar.leader_avatar;
+    //     let follower_style = theme.titlebar.follower_avatar;
+
+    //     let microphone_state = if muted {
+    //         Some(theme.titlebar.muted)
+    //     } else if speaking {
+    //         Some(theme.titlebar.speaking)
+    //     } else {
+    //         None
+    //     };
+
+    //     let mut background_color = theme
+    //         .titlebar
+    //         .container
+    //         .background_color
+    //         .unwrap_or_default();
+
+    //     let participant_index = self
+    //         .user_store
+    //         .read(cx)
+    //         .participant_indices()
+    //         .get(&user_id)
+    //         .copied();
+    //     if let Some(participant_index) = participant_index {
+    //         if self_following_initialized {
+    //             let selection = theme
+    //                 .editor
+    //                 .selection_style_for_room_participant(participant_index.0)
+    //                 .selection;
+    //             background_color = Color::blend(selection, background_color);
+    //             background_color.a = 255;
+    //         }
+    //     }
+
+    //     enum TitlebarParticipant {}
+
+    //     let content = MouseEventHandler::new::<TitlebarParticipant, _>(
+    //         peer_id.as_u64() as usize,
+    //         cx,
+    //         move |_, cx| {
+    //             Stack::new()
+    //                 .with_children(user.avatar.as_ref().map(|avatar| {
+    //                     let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap)
+    //                         .with_child(Self::render_face(
+    //                             avatar.clone(),
+    //                             Self::location_style(workspace, location, leader_style, cx),
+    //                             background_color,
+    //                             microphone_state,
+    //                         ))
+    //                         .with_children(
+    //                             (|| {
+    //                                 let project_id = project_id?;
+    //                                 let room = room?.read(cx);
+    //                                 let followers = room.followers_for(peer_id, project_id);
+    //                                 Some(followers.into_iter().filter_map(|&follower| {
+    //                                     if Some(follower) == self_peer_id {
+    //                                         return None;
+    //                                     }
+    //                                     let participant =
+    //                                         room.remote_participant_for_peer_id(follower)?;
+    //                                     Some(Self::render_face(
+    //                                         participant.user.avatar.clone()?,
+    //                                         follower_style,
+    //                                         background_color,
+    //                                         None,
+    //                                     ))
+    //                                 }))
+    //                             })()
+    //                             .into_iter()
+    //                             .flatten(),
+    //                         )
+    //                         .with_children(
+    //                             self_following_initialized
+    //                                 .then(|| self.user_store.read(cx).current_user())
+    //                                 .and_then(|user| {
+    //                                     Some(Self::render_face(
+    //                                         user?.avatar.clone()?,
+    //                                         follower_style,
+    //                                         background_color,
+    //                                         None,
+    //                                     ))
+    //                                 }),
+    //                         );
+
+    //                     let mut container = face_pile
+    //                         .contained()
+    //                         .with_style(theme.titlebar.leader_selection);
+
+    //                     if let Some(participant_index) = participant_index {
+    //                         if self_following_initialized {
+    //                             let color = theme
+    //                                 .editor
+    //                                 .selection_style_for_room_participant(participant_index.0)
+    //                                 .selection;
+    //                             container = container.with_background_color(color);
+    //                         }
+    //                     }
+
+    //                     container
+    //                 }))
+    //                 .with_children((|| {
+    //                     let participant_index = participant_index?;
+    //                     let color = theme
+    //                         .editor
+    //                         .selection_style_for_room_participant(participant_index.0)
+    //                         .cursor;
+    //                     Some(
+    //                         AvatarRibbon::new(color)
+    //                             .constrained()
+    //                             .with_width(theme.titlebar.avatar_ribbon.width)
+    //                             .with_height(theme.titlebar.avatar_ribbon.height)
+    //                             .aligned()
+    //                             .bottom(),
+    //                     )
+    //                 })())
+    //         },
+    //     );
+
+    //     if Some(peer_id) == self_peer_id {
+    //         return content.into_any();
+    //     }
+
+    //     content
+    //         .with_cursor_style(CursorStyle::PointingHand)
+    //         .on_click(MouseButton::Left, move |_, this, cx| {
+    //             let Some(workspace) = this.workspace.upgrade(cx) else {
+    //                 return;
+    //             };
+    //             if let Some(task) =
+    //                 workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
+    //             {
+    //                 task.detach_and_log_err(cx);
+    //             }
+    //         })
+    //         .with_tooltip::<TitlebarParticipant>(
+    //             peer_id.as_u64() as usize,
+    //             format!("Follow {}", user.github_login),
+    //             Some(Box::new(FollowNextCollaborator)),
+    //             theme.tooltip.clone(),
+    //             cx,
+    //         )
+    //         .into_any()
+    // }
+
+    // fn location_style(
+    //     workspace: &ViewHandle<Workspace>,
+    //     location: Option<ParticipantLocation>,
+    //     mut style: AvatarStyle,
+    //     cx: &ViewContext<Self>,
+    // ) -> AvatarStyle {
+    //     if let Some(location) = location {
+    //         if let ParticipantLocation::SharedProject { project_id } = location {
+    //             if Some(project_id) != workspace.read(cx).project().read(cx).remote_id() {
+    //                 style.image.grayscale = true;
+    //             }
+    //         } else {
+    //             style.image.grayscale = true;
+    //         }
+    //     }
+
+    //     style
+    // }
+
+    // fn render_face<V: 'static>(
+    //     avatar: Arc<ImageData>,
+    //     avatar_style: AvatarStyle,
+    //     background_color: Color,
+    //     microphone_state: Option<Color>,
+    // ) -> AnyElement<V> {
+    //     Image::from_data(avatar)
+    //         .with_style(avatar_style.image)
+    //         .aligned()
+    //         .contained()
+    //         .with_background_color(microphone_state.unwrap_or(background_color))
+    //         .with_corner_radius(avatar_style.outer_corner_radius)
+    //         .constrained()
+    //         .with_width(avatar_style.outer_width)
+    //         .with_height(avatar_style.outer_width)
+    //         .aligned()
+    //         .into_any()
+    // }
+
+    // fn render_connection_status(
+    //     &self,
+    //     status: &client::Status,
+    //     cx: &mut ViewContext<Self>,
+    // ) -> Option<AnyElement<Self>> {
+    //     enum ConnectionStatusButton {}
+
+    //     let theme = &theme::current(cx).clone();
+    //     match status {
+    //         client::Status::ConnectionError
+    //         | client::Status::ConnectionLost
+    //         | client::Status::Reauthenticating { .. }
+    //         | client::Status::Reconnecting { .. }
+    //         | client::Status::ReconnectionError { .. } => Some(
+    //             Svg::new("icons/disconnected.svg")
+    //                 .with_color(theme.titlebar.offline_icon.color)
+    //                 .constrained()
+    //                 .with_width(theme.titlebar.offline_icon.width)
+    //                 .aligned()
+    //                 .contained()
+    //                 .with_style(theme.titlebar.offline_icon.container)
+    //                 .into_any(),
+    //         ),
+    //         client::Status::UpgradeRequired => {
+    //             let auto_updater = auto_update::AutoUpdater::get(cx);
+    //             let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
+    //                 Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate",
+    //                 Some(AutoUpdateStatus::Installing)
+    //                 | Some(AutoUpdateStatus::Downloading)
+    //                 | Some(AutoUpdateStatus::Checking) => "Updating...",
+    //                 Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
+    //                     "Please update Zed to Collaborate"
+    //                 }
+    //             };
+
+    //             Some(
+    //                 MouseEventHandler::new::<ConnectionStatusButton, _>(0, cx, |_, _| {
+    //                     Label::new(label, theme.titlebar.outdated_warning.text.clone())
+    //                         .contained()
+    //                         .with_style(theme.titlebar.outdated_warning.container)
+    //                         .aligned()
+    //                 })
+    //                 .with_cursor_style(CursorStyle::PointingHand)
+    //                 .on_click(MouseButton::Left, |_, _, cx| {
+    //                     if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
+    //                         if auto_updater.read(cx).status() == AutoUpdateStatus::Updated {
+    //                             workspace::restart(&Default::default(), cx);
+    //                             return;
+    //                         }
+    //                     }
+    //                     auto_update::check(&Default::default(), cx);
+    //                 })
+    //                 .into_any(),
+    //             )
+    //         }
+    //         _ => None,
+    //     }
+    // }
+}
+
+// pub struct AvatarRibbon {
+//     color: Color,
+// }
+
+// impl AvatarRibbon {
+//     pub fn new(color: Color) -> AvatarRibbon {
+//         AvatarRibbon { color }
+//     }
+// }
+
+// impl Element<CollabTitlebarItem> for AvatarRibbon {
+//     type LayoutState = ();
+
+//     type PaintState = ();
+
+//     fn layout(
+//         &mut self,
+//         constraint: gpui::SizeConstraint,
+//         _: &mut CollabTitlebarItem,
+//         _: &mut ViewContext<CollabTitlebarItem>,
+//     ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
+//         (constraint.max, ())
+//     }
+
+//     fn paint(
+//         &mut self,
+//         bounds: RectF,
+//         _: RectF,
+//         _: &mut Self::LayoutState,
+//         _: &mut CollabTitlebarItem,
+//         cx: &mut ViewContext<CollabTitlebarItem>,
+//     ) -> Self::PaintState {
+//         let mut path = PathBuilder::new();
+//         path.reset(bounds.lower_left());
+//         path.curve_to(
+//             bounds.origin() + vec2f(bounds.height(), 0.),
+//             bounds.origin(),
+//         );
+//         path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
+//         path.curve_to(bounds.lower_right(), bounds.upper_right());
+//         path.line_to(bounds.lower_left());
+//         cx.scene().push_path(path.build(self.color, None));
+//     }
+
+//     fn rect_for_text_range(
+//         &self,
+//         _: Range<usize>,
+//         _: RectF,
+//         _: RectF,
+//         _: &Self::LayoutState,
+//         _: &Self::PaintState,
+//         _: &CollabTitlebarItem,
+//         _: &ViewContext<CollabTitlebarItem>,
+//     ) -> Option<RectF> {
+//         None
+//     }
+
+//     fn debug(
+//         &self,
+//         bounds: RectF,
+//         _: &Self::LayoutState,
+//         _: &Self::PaintState,
+//         _: &CollabTitlebarItem,
+//         _: &ViewContext<CollabTitlebarItem>,
+//     ) -> gpui::json::Value {
+//         json::json!({
+//             "type": "AvatarRibbon",
+//             "bounds": bounds.to_json(),
+//             "color": self.color.to_json(),
+//         })
+//     }
+// }

crates/collab_ui2/src/collab_ui.rs 🔗

@@ -0,0 +1,154 @@
+pub mod channel_view;
+pub mod chat_panel;
+pub mod collab_panel;
+mod collab_titlebar_item;
+mod face_pile;
+pub mod notification_panel;
+pub mod notifications;
+mod panel_settings;
+
+use std::sync::Arc;
+
+pub use collab_panel::CollabPanel;
+pub use collab_titlebar_item::CollabTitlebarItem;
+use gpui::AppContext;
+pub use panel_settings::{
+    ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
+};
+use settings::Settings;
+use workspace::AppState;
+
+// actions!(
+//     collab,
+//     [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
+// );
+
+pub fn init(_app_state: &Arc<AppState>, cx: &mut AppContext) {
+    CollaborationPanelSettings::register(cx);
+    ChatPanelSettings::register(cx);
+    NotificationPanelSettings::register(cx);
+
+    // vcs_menu::init(cx);
+    collab_titlebar_item::init(cx);
+    collab_panel::init(cx);
+    // chat_panel::init(cx);
+    // notifications::init(&app_state, cx);
+
+    // cx.add_global_action(toggle_screen_sharing);
+    // cx.add_global_action(toggle_mute);
+    // cx.add_global_action(toggle_deafen);
+}
+
+// pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
+//     let call = ActiveCall::global(cx).read(cx);
+//     if let Some(room) = call.room().cloned() {
+//         let client = call.client();
+//         let toggle_screen_sharing = room.update(cx, |room, cx| {
+//             if room.is_screen_sharing() {
+//                 report_call_event_for_room(
+//                     "disable screen share",
+//                     room.id(),
+//                     room.channel_id(),
+//                     &client,
+//                     cx,
+//                 );
+//                 Task::ready(room.unshare_screen(cx))
+//             } else {
+//                 report_call_event_for_room(
+//                     "enable screen share",
+//                     room.id(),
+//                     room.channel_id(),
+//                     &client,
+//                     cx,
+//                 );
+//                 room.share_screen(cx)
+//             }
+//         });
+//         toggle_screen_sharing.detach_and_log_err(cx);
+//     }
+// }
+
+// pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
+//     let call = ActiveCall::global(cx).read(cx);
+//     if let Some(room) = call.room().cloned() {
+//         let client = call.client();
+//         room.update(cx, |room, cx| {
+//             let operation = if room.is_muted(cx) {
+//                 "enable microphone"
+//             } else {
+//                 "disable microphone"
+//             };
+//             report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
+
+//             room.toggle_mute(cx)
+//         })
+//         .map(|task| task.detach_and_log_err(cx))
+//         .log_err();
+//     }
+// }
+
+// pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
+//     if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
+//         room.update(cx, Room::toggle_deafen)
+//             .map(|task| task.detach_and_log_err(cx))
+//             .log_err();
+//     }
+// }
+
+// fn notification_window_options(
+//     screen: Rc<dyn Screen>,
+//     window_size: Vector2F,
+// ) -> WindowOptions<'static> {
+//     const NOTIFICATION_PADDING: f32 = 16.;
+
+//     let screen_bounds = screen.content_bounds();
+//     WindowOptions {
+//         bounds: WindowBounds::Fixed(RectF::new(
+//             screen_bounds.upper_right()
+//                 + vec2f(
+//                     -NOTIFICATION_PADDING - window_size.x(),
+//                     NOTIFICATION_PADDING,
+//                 ),
+//             window_size,
+//         )),
+//         titlebar: None,
+//         center: false,
+//         focus: false,
+//         show: true,
+//         kind: WindowKind::PopUp,
+//         is_movable: false,
+//         screen: Some(screen),
+//     }
+// }
+
+// fn render_avatar<T: 'static>(
+//     avatar: Option<Arc<ImageData>>,
+//     avatar_style: &AvatarStyle,
+//     container: ContainerStyle,
+// ) -> AnyElement<T> {
+//     avatar
+//         .map(|avatar| {
+//             Image::from_data(avatar)
+//                 .with_style(avatar_style.image)
+//                 .aligned()
+//                 .contained()
+//                 .with_corner_radius(avatar_style.outer_corner_radius)
+//                 .constrained()
+//                 .with_width(avatar_style.outer_width)
+//                 .with_height(avatar_style.outer_width)
+//                 .into_any()
+//         })
+//         .unwrap_or_else(|| {
+//             Empty::new()
+//                 .constrained()
+//                 .with_width(avatar_style.outer_width)
+//                 .into_any()
+//         })
+//         .contained()
+//         .with_style(container)
+//         .into_any()
+// }
+
+// fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
+//     cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
+// }

crates/collab_ui2/src/face_pile.rs 🔗

@@ -0,0 +1,113 @@
+// use std::ops::Range;
+
+// use gpui::{
+//     geometry::{
+//         rect::RectF,
+//         vector::{vec2f, Vector2F},
+//     },
+//     json::ToJson,
+//     serde_json::{self, json},
+//     AnyElement, Axis, Element, View, ViewContext,
+// };
+
+// pub(crate) struct FacePile<V: View> {
+//     overlap: f32,
+//     faces: Vec<AnyElement<V>>,
+// }
+
+// impl<V: View> FacePile<V> {
+//     pub fn new(overlap: f32) -> Self {
+//         Self {
+//             overlap,
+//             faces: Vec::new(),
+//         }
+//     }
+// }
+
+// impl<V: View> Element<V> for FacePile<V> {
+//     type LayoutState = ();
+//     type PaintState = ();
+
+//     fn layout(
+//         &mut self,
+//         constraint: gpui::SizeConstraint,
+//         view: &mut V,
+//         cx: &mut ViewContext<V>,
+//     ) -> (Vector2F, Self::LayoutState) {
+//         debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
+
+//         let mut width = 0.;
+//         let mut max_height = 0.;
+//         for face in &mut self.faces {
+//             let layout = face.layout(constraint, view, cx);
+//             width += layout.x();
+//             max_height = f32::max(max_height, layout.y());
+//         }
+//         width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
+
+//         (
+//             Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
+//             (),
+//         )
+//     }
+
+//     fn paint(
+//         &mut self,
+//         bounds: RectF,
+//         visible_bounds: RectF,
+//         _layout: &mut Self::LayoutState,
+//         view: &mut V,
+//         cx: &mut ViewContext<V>,
+//     ) -> Self::PaintState {
+//         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
+
+//         let origin_y = bounds.upper_right().y();
+//         let mut origin_x = bounds.upper_right().x();
+
+//         for face in self.faces.iter_mut().rev() {
+//             let size = face.size();
+//             origin_x -= size.x();
+//             let origin_y = origin_y + (bounds.height() - size.y()) / 2.0;
+
+//             cx.scene().push_layer(None);
+//             face.paint(vec2f(origin_x, origin_y), visible_bounds, view, cx);
+//             cx.scene().pop_layer();
+//             origin_x += self.overlap;
+//         }
+
+//         ()
+//     }
+
+//     fn rect_for_text_range(
+//         &self,
+//         _: Range<usize>,
+//         _: RectF,
+//         _: RectF,
+//         _: &Self::LayoutState,
+//         _: &Self::PaintState,
+//         _: &V,
+//         _: &ViewContext<V>,
+//     ) -> Option<RectF> {
+//         None
+//     }
+
+//     fn debug(
+//         &self,
+//         bounds: RectF,
+//         _: &Self::LayoutState,
+//         _: &Self::PaintState,
+//         _: &V,
+//         _: &ViewContext<V>,
+//     ) -> serde_json::Value {
+//         json!({
+//             "type": "FacePile",
+//             "bounds": bounds.to_json()
+//         })
+//     }
+// }
+
+// impl<V: View> Extend<AnyElement<V>> for FacePile<V> {
+//     fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
+//         self.faces.extend(children);
+//     }
+// }

crates/collab_ui2/src/notification_panel.rs 🔗

@@ -0,0 +1,884 @@
+// use crate::{chat_panel::ChatPanel, render_avatar, NotificationPanelSettings};
+// use anyhow::Result;
+// use channel::ChannelStore;
+// use client::{Client, Notification, User, UserStore};
+// use collections::HashMap;
+// use db::kvp::KEY_VALUE_STORE;
+// use futures::StreamExt;
+// use gpui::{
+//     actions,
+//     elements::*,
+//     platform::{CursorStyle, MouseButton},
+//     serde_json, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Task, View,
+//     ViewContext, ViewHandle, WeakViewHandle, WindowContext,
+// };
+// use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
+// use project::Fs;
+// use rpc::proto;
+// use serde::{Deserialize, Serialize};
+// use settings::SettingsStore;
+// use std::{sync::Arc, time::Duration};
+// use theme::{ui, Theme};
+// use time::{OffsetDateTime, UtcOffset};
+// use util::{ResultExt, TryFutureExt};
+// use workspace::{
+//     dock::{DockPosition, Panel},
+//     Workspace,
+// };
+
+// const LOADING_THRESHOLD: usize = 30;
+// const MARK_AS_READ_DELAY: Duration = Duration::from_secs(1);
+// const TOAST_DURATION: Duration = Duration::from_secs(5);
+// const NOTIFICATION_PANEL_KEY: &'static str = "NotificationPanel";
+
+// pub struct NotificationPanel {
+//     client: Arc<Client>,
+//     user_store: ModelHandle<UserStore>,
+//     channel_store: ModelHandle<ChannelStore>,
+//     notification_store: ModelHandle<NotificationStore>,
+//     fs: Arc<dyn Fs>,
+//     width: Option<f32>,
+//     active: bool,
+//     notification_list: ListState<Self>,
+//     pending_serialization: Task<Option<()>>,
+//     subscriptions: Vec<gpui::Subscription>,
+//     workspace: WeakViewHandle<Workspace>,
+//     current_notification_toast: Option<(u64, Task<()>)>,
+//     local_timezone: UtcOffset,
+//     has_focus: bool,
+//     mark_as_read_tasks: HashMap<u64, Task<Result<()>>>,
+// }
+
+// #[derive(Serialize, Deserialize)]
+// struct SerializedNotificationPanel {
+//     width: Option<f32>,
+// }
+
+// #[derive(Debug)]
+// pub enum Event {
+//     DockPositionChanged,
+//     Focus,
+//     Dismissed,
+// }
+
+// pub struct NotificationPresenter {
+//     pub actor: Option<Arc<client::User>>,
+//     pub text: String,
+//     pub icon: &'static str,
+//     pub needs_response: bool,
+//     pub can_navigate: bool,
+// }
+
+// actions!(notification_panel, [ToggleFocus]);
+
+// pub fn init(_cx: &mut AppContext) {}
+
+// impl NotificationPanel {
+//     pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
+//         let fs = workspace.app_state().fs.clone();
+//         let client = workspace.app_state().client.clone();
+//         let user_store = workspace.app_state().user_store.clone();
+//         let workspace_handle = workspace.weak_handle();
+
+//         cx.add_view(|cx| {
+//             let mut status = client.status();
+//             cx.spawn(|this, mut cx| async move {
+//                 while let Some(_) = status.next().await {
+//                     if this
+//                         .update(&mut cx, |_, cx| {
+//                             cx.notify();
+//                         })
+//                         .is_err()
+//                     {
+//                         break;
+//                     }
+//                 }
+//             })
+//             .detach();
+
+//             let mut notification_list =
+//                 ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
+//                     this.render_notification(ix, cx)
+//                         .unwrap_or_else(|| Empty::new().into_any())
+//                 });
+//             notification_list.set_scroll_handler(|visible_range, count, this, cx| {
+//                 if count.saturating_sub(visible_range.end) < LOADING_THRESHOLD {
+//                     if let Some(task) = this
+//                         .notification_store
+//                         .update(cx, |store, cx| store.load_more_notifications(false, cx))
+//                     {
+//                         task.detach();
+//                     }
+//                 }
+//             });
+
+//             let mut this = Self {
+//                 fs,
+//                 client,
+//                 user_store,
+//                 local_timezone: cx.platform().local_timezone(),
+//                 channel_store: ChannelStore::global(cx),
+//                 notification_store: NotificationStore::global(cx),
+//                 notification_list,
+//                 pending_serialization: Task::ready(None),
+//                 workspace: workspace_handle,
+//                 has_focus: false,
+//                 current_notification_toast: None,
+//                 subscriptions: Vec::new(),
+//                 active: false,
+//                 mark_as_read_tasks: HashMap::default(),
+//                 width: None,
+//             };
+
+//             let mut old_dock_position = this.position(cx);
+//             this.subscriptions.extend([
+//                 cx.observe(&this.notification_store, |_, _, cx| cx.notify()),
+//                 cx.subscribe(&this.notification_store, Self::on_notification_event),
+//                 cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
+//                     let new_dock_position = this.position(cx);
+//                     if new_dock_position != old_dock_position {
+//                         old_dock_position = new_dock_position;
+//                         cx.emit(Event::DockPositionChanged);
+//                     }
+//                     cx.notify();
+//                 }),
+//             ]);
+//             this
+//         })
+//     }
+
+//     pub fn load(
+//         workspace: WeakViewHandle<Workspace>,
+//         cx: AsyncAppContext,
+//     ) -> Task<Result<ViewHandle<Self>>> {
+//         cx.spawn(|mut cx| async move {
+//             let serialized_panel = if let Some(panel) = cx
+//                 .background()
+//                 .spawn(async move { KEY_VALUE_STORE.read_kvp(NOTIFICATION_PANEL_KEY) })
+//                 .await
+//                 .log_err()
+//                 .flatten()
+//             {
+//                 Some(serde_json::from_str::<SerializedNotificationPanel>(&panel)?)
+//             } else {
+//                 None
+//             };
+
+//             workspace.update(&mut cx, |workspace, cx| {
+//                 let panel = Self::new(workspace, cx);
+//                 if let Some(serialized_panel) = serialized_panel {
+//                     panel.update(cx, |panel, cx| {
+//                         panel.width = serialized_panel.width;
+//                         cx.notify();
+//                     });
+//                 }
+//                 panel
+//             })
+//         })
+//     }
+
+//     fn serialize(&mut self, cx: &mut ViewContext<Self>) {
+//         let width = self.width;
+//         self.pending_serialization = cx.background().spawn(
+//             async move {
+//                 KEY_VALUE_STORE
+//                     .write_kvp(
+//                         NOTIFICATION_PANEL_KEY.into(),
+//                         serde_json::to_string(&SerializedNotificationPanel { width })?,
+//                     )
+//                     .await?;
+//                 anyhow::Ok(())
+//             }
+//             .log_err(),
+//         );
+//     }
+
+//     fn render_notification(
+//         &mut self,
+//         ix: usize,
+//         cx: &mut ViewContext<Self>,
+//     ) -> Option<AnyElement<Self>> {
+//         let entry = self.notification_store.read(cx).notification_at(ix)?;
+//         let notification_id = entry.id;
+//         let now = OffsetDateTime::now_utc();
+//         let timestamp = entry.timestamp;
+//         let NotificationPresenter {
+//             actor,
+//             text,
+//             needs_response,
+//             can_navigate,
+//             ..
+//         } = self.present_notification(entry, cx)?;
+
+//         let theme = theme::current(cx);
+//         let style = &theme.notification_panel;
+//         let response = entry.response;
+//         let notification = entry.notification.clone();
+
+//         let message_style = if entry.is_read {
+//             style.read_text.clone()
+//         } else {
+//             style.unread_text.clone()
+//         };
+
+//         if self.active && !entry.is_read {
+//             self.did_render_notification(notification_id, &notification, cx);
+//         }
+
+//         enum Decline {}
+//         enum Accept {}
+
+//         Some(
+//             MouseEventHandler::new::<NotificationEntry, _>(ix, cx, |_, cx| {
+//                 let container = message_style.container;
+
+//                 Flex::row()
+//                     .with_children(actor.map(|actor| {
+//                         render_avatar(actor.avatar.clone(), &style.avatar, style.avatar_container)
+//                     }))
+//                     .with_child(
+//                         Flex::column()
+//                             .with_child(Text::new(text, message_style.text.clone()))
+//                             .with_child(
+//                                 Flex::row()
+//                                     .with_child(
+//                                         Label::new(
+//                                             format_timestamp(timestamp, now, self.local_timezone),
+//                                             style.timestamp.text.clone(),
+//                                         )
+//                                         .contained()
+//                                         .with_style(style.timestamp.container),
+//                                     )
+//                                     .with_children(if let Some(is_accepted) = response {
+//                                         Some(
+//                                             Label::new(
+//                                                 if is_accepted {
+//                                                     "You accepted"
+//                                                 } else {
+//                                                     "You declined"
+//                                                 },
+//                                                 style.read_text.text.clone(),
+//                                             )
+//                                             .flex_float()
+//                                             .into_any(),
+//                                         )
+//                                     } else if needs_response {
+//                                         Some(
+//                                             Flex::row()
+//                                                 .with_children([
+//                                                     MouseEventHandler::new::<Decline, _>(
+//                                                         ix,
+//                                                         cx,
+//                                                         |state, _| {
+//                                                             let button =
+//                                                                 style.button.style_for(state);
+//                                                             Label::new(
+//                                                                 "Decline",
+//                                                                 button.text.clone(),
+//                                                             )
+//                                                             .contained()
+//                                                             .with_style(button.container)
+//                                                         },
+//                                                     )
+//                                                     .with_cursor_style(CursorStyle::PointingHand)
+//                                                     .on_click(MouseButton::Left, {
+//                                                         let notification = notification.clone();
+//                                                         move |_, view, cx| {
+//                                                             view.respond_to_notification(
+//                                                                 notification.clone(),
+//                                                                 false,
+//                                                                 cx,
+//                                                             );
+//                                                         }
+//                                                     }),
+//                                                     MouseEventHandler::new::<Accept, _>(
+//                                                         ix,
+//                                                         cx,
+//                                                         |state, _| {
+//                                                             let button =
+//                                                                 style.button.style_for(state);
+//                                                             Label::new(
+//                                                                 "Accept",
+//                                                                 button.text.clone(),
+//                                                             )
+//                                                             .contained()
+//                                                             .with_style(button.container)
+//                                                         },
+//                                                     )
+//                                                     .with_cursor_style(CursorStyle::PointingHand)
+//                                                     .on_click(MouseButton::Left, {
+//                                                         let notification = notification.clone();
+//                                                         move |_, view, cx| {
+//                                                             view.respond_to_notification(
+//                                                                 notification.clone(),
+//                                                                 true,
+//                                                                 cx,
+//                                                             );
+//                                                         }
+//                                                     }),
+//                                                 ])
+//                                                 .flex_float()
+//                                                 .into_any(),
+//                                         )
+//                                     } else {
+//                                         None
+//                                     }),
+//                             )
+//                             .flex(1.0, true),
+//                     )
+//                     .contained()
+//                     .with_style(container)
+//                     .into_any()
+//             })
+//             .with_cursor_style(if can_navigate {
+//                 CursorStyle::PointingHand
+//             } else {
+//                 CursorStyle::default()
+//             })
+//             .on_click(MouseButton::Left, {
+//                 let notification = notification.clone();
+//                 move |_, this, cx| this.did_click_notification(&notification, cx)
+//             })
+//             .into_any(),
+//         )
+//     }
+
+//     fn present_notification(
+//         &self,
+//         entry: &NotificationEntry,
+//         cx: &AppContext,
+//     ) -> Option<NotificationPresenter> {
+//         let user_store = self.user_store.read(cx);
+//         let channel_store = self.channel_store.read(cx);
+//         match entry.notification {
+//             Notification::ContactRequest { sender_id } => {
+//                 let requester = user_store.get_cached_user(sender_id)?;
+//                 Some(NotificationPresenter {
+//                     icon: "icons/plus.svg",
+//                     text: format!("{} wants to add you as a contact", requester.github_login),
+//                     needs_response: user_store.has_incoming_contact_request(requester.id),
+//                     actor: Some(requester),
+//                     can_navigate: false,
+//                 })
+//             }
+//             Notification::ContactRequestAccepted { responder_id } => {
+//                 let responder = user_store.get_cached_user(responder_id)?;
+//                 Some(NotificationPresenter {
+//                     icon: "icons/plus.svg",
+//                     text: format!("{} accepted your contact invite", responder.github_login),
+//                     needs_response: false,
+//                     actor: Some(responder),
+//                     can_navigate: false,
+//                 })
+//             }
+//             Notification::ChannelInvitation {
+//                 ref channel_name,
+//                 channel_id,
+//                 inviter_id,
+//             } => {
+//                 let inviter = user_store.get_cached_user(inviter_id)?;
+//                 Some(NotificationPresenter {
+//                     icon: "icons/hash.svg",
+//                     text: format!(
+//                         "{} invited you to join the #{channel_name} channel",
+//                         inviter.github_login
+//                     ),
+//                     needs_response: channel_store.has_channel_invitation(channel_id),
+//                     actor: Some(inviter),
+//                     can_navigate: false,
+//                 })
+//             }
+//             Notification::ChannelMessageMention {
+//                 sender_id,
+//                 channel_id,
+//                 message_id,
+//             } => {
+//                 let sender = user_store.get_cached_user(sender_id)?;
+//                 let channel = channel_store.channel_for_id(channel_id)?;
+//                 let message = self
+//                     .notification_store
+//                     .read(cx)
+//                     .channel_message_for_id(message_id)?;
+//                 Some(NotificationPresenter {
+//                     icon: "icons/conversations.svg",
+//                     text: format!(
+//                         "{} mentioned you in #{}:\n{}",
+//                         sender.github_login, channel.name, message.body,
+//                     ),
+//                     needs_response: false,
+//                     actor: Some(sender),
+//                     can_navigate: true,
+//                 })
+//             }
+//         }
+//     }
+
+//     fn did_render_notification(
+//         &mut self,
+//         notification_id: u64,
+//         notification: &Notification,
+//         cx: &mut ViewContext<Self>,
+//     ) {
+//         let should_mark_as_read = match notification {
+//             Notification::ContactRequestAccepted { .. } => true,
+//             Notification::ContactRequest { .. }
+//             | Notification::ChannelInvitation { .. }
+//             | Notification::ChannelMessageMention { .. } => false,
+//         };
+
+//         if should_mark_as_read {
+//             self.mark_as_read_tasks
+//                 .entry(notification_id)
+//                 .or_insert_with(|| {
+//                     let client = self.client.clone();
+//                     cx.spawn(|this, mut cx| async move {
+//                         cx.background().timer(MARK_AS_READ_DELAY).await;
+//                         client
+//                             .request(proto::MarkNotificationRead { notification_id })
+//                             .await?;
+//                         this.update(&mut cx, |this, _| {
+//                             this.mark_as_read_tasks.remove(&notification_id);
+//                         })?;
+//                         Ok(())
+//                     })
+//                 });
+//         }
+//     }
+
+//     fn did_click_notification(&mut self, notification: &Notification, cx: &mut ViewContext<Self>) {
+//         if let Notification::ChannelMessageMention {
+//             message_id,
+//             channel_id,
+//             ..
+//         } = notification.clone()
+//         {
+//             if let Some(workspace) = self.workspace.upgrade(cx) {
+//                 cx.app_context().defer(move |cx| {
+//                     workspace.update(cx, |workspace, cx| {
+//                         if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
+//                             panel.update(cx, |panel, cx| {
+//                                 panel
+//                                     .select_channel(channel_id, Some(message_id), cx)
+//                                     .detach_and_log_err(cx);
+//                             });
+//                         }
+//                     });
+//                 });
+//             }
+//         }
+//     }
+
+//     fn is_showing_notification(&self, notification: &Notification, cx: &AppContext) -> bool {
+//         if let Notification::ChannelMessageMention { channel_id, .. } = &notification {
+//             if let Some(workspace) = self.workspace.upgrade(cx) {
+//                 return workspace
+//                     .read_with(cx, |workspace, cx| {
+//                         if let Some(panel) = workspace.panel::<ChatPanel>(cx) {
+//                             return panel.read_with(cx, |panel, cx| {
+//                                 panel.is_scrolled_to_bottom()
+//                                     && panel.active_chat().map_or(false, |chat| {
+//                                         chat.read(cx).channel_id == *channel_id
+//                                     })
+//                             });
+//                         }
+//                         false
+//                     })
+//                     .unwrap_or_default();
+//             }
+//         }
+
+//         false
+//     }
+
+//     fn render_sign_in_prompt(
+//         &self,
+//         theme: &Arc<Theme>,
+//         cx: &mut ViewContext<Self>,
+//     ) -> AnyElement<Self> {
+//         enum SignInPromptLabel {}
+
+//         MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
+//             Label::new(
+//                 "Sign in to view your notifications".to_string(),
+//                 theme
+//                     .chat_panel
+//                     .sign_in_prompt
+//                     .style_for(mouse_state)
+//                     .clone(),
+//             )
+//         })
+//         .with_cursor_style(CursorStyle::PointingHand)
+//         .on_click(MouseButton::Left, move |_, this, cx| {
+//             let client = this.client.clone();
+//             cx.spawn(|_, cx| async move {
+//                 client.authenticate_and_connect(true, &cx).log_err().await;
+//             })
+//             .detach();
+//         })
+//         .aligned()
+//         .into_any()
+//     }
+
+//     fn render_empty_state(
+//         &self,
+//         theme: &Arc<Theme>,
+//         _cx: &mut ViewContext<Self>,
+//     ) -> AnyElement<Self> {
+//         Label::new(
+//             "You have no notifications".to_string(),
+//             theme.chat_panel.sign_in_prompt.default.clone(),
+//         )
+//         .aligned()
+//         .into_any()
+//     }
+
+//     fn on_notification_event(
+//         &mut self,
+//         _: ModelHandle<NotificationStore>,
+//         event: &NotificationEvent,
+//         cx: &mut ViewContext<Self>,
+//     ) {
+//         match event {
+//             NotificationEvent::NewNotification { entry } => self.add_toast(entry, cx),
+//             NotificationEvent::NotificationRemoved { entry }
+//             | NotificationEvent::NotificationRead { entry } => self.remove_toast(entry.id, cx),
+//             NotificationEvent::NotificationsUpdated {
+//                 old_range,
+//                 new_count,
+//             } => {
+//                 self.notification_list.splice(old_range.clone(), *new_count);
+//                 cx.notify();
+//             }
+//         }
+//     }
+
+//     fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext<Self>) {
+//         if self.is_showing_notification(&entry.notification, cx) {
+//             return;
+//         }
+
+//         let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx)
+//         else {
+//             return;
+//         };
+
+//         let notification_id = entry.id;
+//         self.current_notification_toast = Some((
+//             notification_id,
+//             cx.spawn(|this, mut cx| async move {
+//                 cx.background().timer(TOAST_DURATION).await;
+//                 this.update(&mut cx, |this, cx| this.remove_toast(notification_id, cx))
+//                     .ok();
+//             }),
+//         ));
+
+//         self.workspace
+//             .update(cx, |workspace, cx| {
+//                 workspace.dismiss_notification::<NotificationToast>(0, cx);
+//                 workspace.show_notification(0, cx, |cx| {
+//                     let workspace = cx.weak_handle();
+//                     cx.add_view(|_| NotificationToast {
+//                         notification_id,
+//                         actor,
+//                         text,
+//                         workspace,
+//                     })
+//                 })
+//             })
+//             .ok();
+//     }
+
+//     fn remove_toast(&mut self, notification_id: u64, cx: &mut ViewContext<Self>) {
+//         if let Some((current_id, _)) = &self.current_notification_toast {
+//             if *current_id == notification_id {
+//                 self.current_notification_toast.take();
+//                 self.workspace
+//                     .update(cx, |workspace, cx| {
+//                         workspace.dismiss_notification::<NotificationToast>(0, cx)
+//                     })
+//                     .ok();
+//             }
+//         }
+//     }
+
+//     fn respond_to_notification(
+//         &mut self,
+//         notification: Notification,
+//         response: bool,
+//         cx: &mut ViewContext<Self>,
+//     ) {
+//         self.notification_store.update(cx, |store, cx| {
+//             store.respond_to_notification(notification, response, cx);
+//         });
+//     }
+// }
+
+// impl Entity for NotificationPanel {
+//     type Event = Event;
+// }
+
+// impl View for NotificationPanel {
+//     fn ui_name() -> &'static str {
+//         "NotificationPanel"
+//     }
+
+//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+//         let theme = theme::current(cx);
+//         let style = &theme.notification_panel;
+//         let element = if self.client.user_id().is_none() {
+//             self.render_sign_in_prompt(&theme, cx)
+//         } else if self.notification_list.item_count() == 0 {
+//             self.render_empty_state(&theme, cx)
+//         } else {
+//             Flex::column()
+//                 .with_child(
+//                     Flex::row()
+//                         .with_child(Label::new("Notifications", style.title.text.clone()))
+//                         .with_child(ui::svg(&style.title_icon).flex_float())
+//                         .align_children_center()
+//                         .contained()
+//                         .with_style(style.title.container)
+//                         .constrained()
+//                         .with_height(style.title_height),
+//                 )
+//                 .with_child(
+//                     List::new(self.notification_list.clone())
+//                         .contained()
+//                         .with_style(style.list)
+//                         .flex(1., true),
+//                 )
+//                 .into_any()
+//         };
+//         element
+//             .contained()
+//             .with_style(style.container)
+//             .constrained()
+//             .with_min_width(150.)
+//             .into_any()
+//     }
+
+//     fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
+//         self.has_focus = true;
+//     }
+
+//     fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
+//         self.has_focus = false;
+//     }
+// }
+
+// impl Panel for NotificationPanel {
+//     fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
+//         settings::get::<NotificationPanelSettings>(cx).dock
+//     }
+
+//     fn position_is_valid(&self, position: DockPosition) -> bool {
+//         matches!(position, DockPosition::Left | DockPosition::Right)
+//     }
+
+//     fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
+//         settings::update_settings_file::<NotificationPanelSettings>(
+//             self.fs.clone(),
+//             cx,
+//             move |settings| settings.dock = Some(position),
+//         );
+//     }
+
+//     fn size(&self, cx: &gpui::WindowContext) -> f32 {
+//         self.width
+//             .unwrap_or_else(|| settings::get::<NotificationPanelSettings>(cx).default_width)
+//     }
+
+//     fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
+//         self.width = size;
+//         self.serialize(cx);
+//         cx.notify();
+//     }
+
+//     fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
+//         self.active = active;
+//         if self.notification_store.read(cx).notification_count() == 0 {
+//             cx.emit(Event::Dismissed);
+//         }
+//     }
+
+//     fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
+//         (settings::get::<NotificationPanelSettings>(cx).button
+//             && self.notification_store.read(cx).notification_count() > 0)
+//             .then(|| "icons/bell.svg")
+//     }
+
+//     fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
+//         (
+//             "Notification Panel".to_string(),
+//             Some(Box::new(ToggleFocus)),
+//         )
+//     }
+
+//     fn icon_label(&self, cx: &WindowContext) -> Option<String> {
+//         let count = self.notification_store.read(cx).unread_notification_count();
+//         if count == 0 {
+//             None
+//         } else {
+//             Some(count.to_string())
+//         }
+//     }
+
+//     fn should_change_position_on_event(event: &Self::Event) -> bool {
+//         matches!(event, Event::DockPositionChanged)
+//     }
+
+//     fn should_close_on_event(event: &Self::Event) -> bool {
+//         matches!(event, Event::Dismissed)
+//     }
+
+//     fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
+//         self.has_focus
+//     }
+
+//     fn is_focus_event(event: &Self::Event) -> bool {
+//         matches!(event, Event::Focus)
+//     }
+// }
+
+// pub struct NotificationToast {
+//     notification_id: u64,
+//     actor: Option<Arc<User>>,
+//     text: String,
+//     workspace: WeakViewHandle<Workspace>,
+// }
+
+// pub enum ToastEvent {
+//     Dismiss,
+// }
+
+// impl NotificationToast {
+//     fn focus_notification_panel(&self, cx: &mut AppContext) {
+//         let workspace = self.workspace.clone();
+//         let notification_id = self.notification_id;
+//         cx.defer(move |cx| {
+//             workspace
+//                 .update(cx, |workspace, cx| {
+//                     if let Some(panel) = workspace.focus_panel::<NotificationPanel>(cx) {
+//                         panel.update(cx, |panel, cx| {
+//                             let store = panel.notification_store.read(cx);
+//                             if let Some(entry) = store.notification_for_id(notification_id) {
+//                                 panel.did_click_notification(&entry.clone().notification, cx);
+//                             }
+//                         });
+//                     }
+//                 })
+//                 .ok();
+//         })
+//     }
+// }
+
+// impl Entity for NotificationToast {
+//     type Event = ToastEvent;
+// }
+
+// impl View for NotificationToast {
+//     fn ui_name() -> &'static str {
+//         "ContactNotification"
+//     }
+
+//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+//         let user = self.actor.clone();
+//         let theme = theme::current(cx).clone();
+//         let theme = &theme.contact_notification;
+
+//         MouseEventHandler::new::<Self, _>(0, cx, |_, cx| {
+//             Flex::row()
+//                 .with_children(user.and_then(|user| {
+//                     Some(
+//                         Image::from_data(user.avatar.clone()?)
+//                             .with_style(theme.header_avatar)
+//                             .aligned()
+//                             .constrained()
+//                             .with_height(
+//                                 cx.font_cache()
+//                                     .line_height(theme.header_message.text.font_size),
+//                             )
+//                             .aligned()
+//                             .top(),
+//                     )
+//                 }))
+//                 .with_child(
+//                     Text::new(self.text.clone(), theme.header_message.text.clone())
+//                         .contained()
+//                         .with_style(theme.header_message.container)
+//                         .aligned()
+//                         .top()
+//                         .left()
+//                         .flex(1., true),
+//                 )
+//                 .with_child(
+//                     MouseEventHandler::new::<ToastEvent, _>(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_cursor_style(CursorStyle::PointingHand)
+//                     .with_padding(Padding::uniform(5.))
+//                     .on_click(MouseButton::Left, move |_, _, cx| {
+//                         cx.emit(ToastEvent::Dismiss)
+//                     })
+//                     .aligned()
+//                     .constrained()
+//                     .with_height(
+//                         cx.font_cache()
+//                             .line_height(theme.header_message.text.font_size),
+//                     )
+//                     .aligned()
+//                     .top()
+//                     .flex_float(),
+//                 )
+//                 .contained()
+//         })
+//         .with_cursor_style(CursorStyle::PointingHand)
+//         .on_click(MouseButton::Left, move |_, this, cx| {
+//             this.focus_notification_panel(cx);
+//             cx.emit(ToastEvent::Dismiss);
+//         })
+//         .into_any()
+//     }
+// }
+
+// impl workspace::notifications::Notification for NotificationToast {
+//     fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
+//         matches!(event, ToastEvent::Dismiss)
+//     }
+// }
+
+// fn format_timestamp(
+//     mut timestamp: OffsetDateTime,
+//     mut now: OffsetDateTime,
+//     local_timezone: UtcOffset,
+// ) -> String {
+//     timestamp = timestamp.to_offset(local_timezone);
+//     now = now.to_offset(local_timezone);
+
+//     let today = now.date();
+//     let date = timestamp.date();
+//     if date == today {
+//         let difference = now - timestamp;
+//         if difference >= Duration::from_secs(3600) {
+//             format!("{}h", difference.whole_seconds() / 3600)
+//         } else if difference >= Duration::from_secs(60) {
+//             format!("{}m", difference.whole_seconds() / 60)
+//         } else {
+//             "just now".to_string()
+//         }
+//     } else if date.next_day() == Some(today) {
+//         format!("yesterday")
+//     } else {
+//         format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
+//     }
+// }

crates/collab_ui2/src/notifications.rs 🔗

@@ -0,0 +1,11 @@
+// use gpui::AppContext;
+// use std::sync::Arc;
+// use workspace::AppState;
+
+// pub mod incoming_call_notification;
+// pub mod project_shared_notification;
+
+// pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
+//     incoming_call_notification::init(app_state, cx);
+//     project_shared_notification::init(app_state, cx);
+// }

crates/collab_ui2/src/notifications/incoming_call_notification.rs 🔗

@@ -0,0 +1,213 @@
+use crate::notification_window_options;
+use call::{ActiveCall, IncomingCall};
+use client::proto;
+use futures::StreamExt;
+use gpui::{
+    elements::*,
+    geometry::vector::vec2f,
+    platform::{CursorStyle, MouseButton},
+    AnyElement, AppContext, Entity, View, ViewContext, WindowHandle,
+};
+use std::sync::{Arc, Weak};
+use util::ResultExt;
+use workspace::AppState;
+
+pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
+    let app_state = Arc::downgrade(app_state);
+    let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
+    cx.spawn(|mut cx| async move {
+        let mut notification_windows: Vec<WindowHandle<IncomingCallNotification>> = Vec::new();
+        while let Some(incoming_call) = incoming_call.next().await {
+            for window in notification_windows.drain(..) {
+                window.remove(&mut cx);
+            }
+
+            if let Some(incoming_call) = incoming_call {
+                let window_size = cx.read(|cx| {
+                    let theme = &theme::current(cx).incoming_call_notification;
+                    vec2f(theme.window_width, theme.window_height)
+                });
+
+                for screen in cx.platform().screens() {
+                    let window = cx
+                        .add_window(notification_window_options(screen, window_size), |_| {
+                            IncomingCallNotification::new(incoming_call.clone(), app_state.clone())
+                        });
+
+                    notification_windows.push(window);
+                }
+            }
+        }
+    })
+    .detach();
+}
+
+#[derive(Clone, PartialEq)]
+struct RespondToCall {
+    accept: bool,
+}
+
+pub struct IncomingCallNotification {
+    call: IncomingCall,
+    app_state: Weak<AppState>,
+}
+
+impl IncomingCallNotification {
+    pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
+        Self { call, app_state }
+    }
+
+    fn respond(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
+        let active_call = ActiveCall::global(cx);
+        if accept {
+            let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
+            let caller_user_id = self.call.calling_user.id;
+            let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
+            let app_state = self.app_state.clone();
+            cx.app_context()
+                .spawn(|mut cx| async move {
+                    join.await?;
+                    if let Some(project_id) = initial_project_id {
+                        cx.update(|cx| {
+                            if let Some(app_state) = app_state.upgrade() {
+                                workspace::join_remote_project(
+                                    project_id,
+                                    caller_user_id,
+                                    app_state,
+                                    cx,
+                                )
+                                .detach_and_log_err(cx);
+                            }
+                        });
+                    }
+                    anyhow::Ok(())
+                })
+                .detach_and_log_err(cx);
+        } else {
+            active_call.update(cx, |active_call, cx| {
+                active_call.decline_incoming(cx).log_err();
+            });
+        }
+    }
+
+    fn render_caller(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        let theme = &theme::current(cx).incoming_call_notification;
+        let default_project = proto::ParticipantProject::default();
+        let initial_project = self
+            .call
+            .initial_project
+            .as_ref()
+            .unwrap_or(&default_project);
+        Flex::row()
+            .with_children(self.call.calling_user.avatar.clone().map(|avatar| {
+                Image::from_data(avatar)
+                    .with_style(theme.caller_avatar)
+                    .aligned()
+            }))
+            .with_child(
+                Flex::column()
+                    .with_child(
+                        Label::new(
+                            self.call.calling_user.github_login.clone(),
+                            theme.caller_username.text.clone(),
+                        )
+                        .contained()
+                        .with_style(theme.caller_username.container),
+                    )
+                    .with_child(
+                        Label::new(
+                            format!(
+                                "is sharing a project in Zed{}",
+                                if initial_project.worktree_root_names.is_empty() {
+                                    ""
+                                } else {
+                                    ":"
+                                }
+                            ),
+                            theme.caller_message.text.clone(),
+                        )
+                        .contained()
+                        .with_style(theme.caller_message.container),
+                    )
+                    .with_children(if initial_project.worktree_root_names.is_empty() {
+                        None
+                    } else {
+                        Some(
+                            Label::new(
+                                initial_project.worktree_root_names.join(", "),
+                                theme.worktree_roots.text.clone(),
+                            )
+                            .contained()
+                            .with_style(theme.worktree_roots.container),
+                        )
+                    })
+                    .contained()
+                    .with_style(theme.caller_metadata)
+                    .aligned(),
+            )
+            .contained()
+            .with_style(theme.caller_container)
+            .flex(1., true)
+            .into_any()
+    }
+
+    fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        enum Accept {}
+        enum Decline {}
+
+        let theme = theme::current(cx);
+        Flex::column()
+            .with_child(
+                MouseEventHandler::new::<Accept, _>(0, cx, |_, _| {
+                    let theme = &theme.incoming_call_notification;
+                    Label::new("Accept", theme.accept_button.text.clone())
+                        .aligned()
+                        .contained()
+                        .with_style(theme.accept_button.container)
+                })
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_click(MouseButton::Left, |_, this, cx| {
+                    this.respond(true, cx);
+                })
+                .flex(1., true),
+            )
+            .with_child(
+                MouseEventHandler::new::<Decline, _>(0, cx, |_, _| {
+                    let theme = &theme.incoming_call_notification;
+                    Label::new("Decline", theme.decline_button.text.clone())
+                        .aligned()
+                        .contained()
+                        .with_style(theme.decline_button.container)
+                })
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_click(MouseButton::Left, |_, this, cx| {
+                    this.respond(false, cx);
+                })
+                .flex(1., true),
+            )
+            .constrained()
+            .with_width(theme.incoming_call_notification.button_width)
+            .into_any()
+    }
+}
+
+impl Entity for IncomingCallNotification {
+    type Event = ();
+}
+
+impl View for IncomingCallNotification {
+    fn ui_name() -> &'static str {
+        "IncomingCallNotification"
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        let background = theme::current(cx).incoming_call_notification.background;
+        Flex::row()
+            .with_child(self.render_caller(cx))
+            .with_child(self.render_buttons(cx))
+            .contained()
+            .with_background_color(background)
+            .expanded()
+            .into_any()
+    }
+}

crates/collab_ui2/src/notifications/project_shared_notification.rs 🔗

@@ -0,0 +1,217 @@
+use crate::notification_window_options;
+use call::{room, ActiveCall};
+use client::User;
+use collections::HashMap;
+use gpui::{
+    elements::*,
+    geometry::vector::vec2f,
+    platform::{CursorStyle, MouseButton},
+    AppContext, Entity, View, ViewContext,
+};
+use std::sync::{Arc, Weak};
+use workspace::AppState;
+
+pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
+    let app_state = Arc::downgrade(app_state);
+    let active_call = ActiveCall::global(cx);
+    let mut notification_windows = HashMap::default();
+    cx.subscribe(&active_call, move |_, event, cx| match event {
+        room::Event::RemoteProjectShared {
+            owner,
+            project_id,
+            worktree_root_names,
+        } => {
+            let theme = &theme::current(cx).project_shared_notification;
+            let window_size = vec2f(theme.window_width, theme.window_height);
+
+            for screen in cx.platform().screens() {
+                let window =
+                    cx.add_window(notification_window_options(screen, window_size), |_| {
+                        ProjectSharedNotification::new(
+                            owner.clone(),
+                            *project_id,
+                            worktree_root_names.clone(),
+                            app_state.clone(),
+                        )
+                    });
+                notification_windows
+                    .entry(*project_id)
+                    .or_insert(Vec::new())
+                    .push(window);
+            }
+        }
+        room::Event::RemoteProjectUnshared { project_id }
+        | room::Event::RemoteProjectJoined { project_id }
+        | room::Event::RemoteProjectInvitationDiscarded { project_id } => {
+            if let Some(windows) = notification_windows.remove(&project_id) {
+                for window in windows {
+                    window.remove(cx);
+                }
+            }
+        }
+        room::Event::Left => {
+            for (_, windows) in notification_windows.drain() {
+                for window in windows {
+                    window.remove(cx);
+                }
+            }
+        }
+        _ => {}
+    })
+    .detach();
+}
+
+pub struct ProjectSharedNotification {
+    project_id: u64,
+    worktree_root_names: Vec<String>,
+    owner: Arc<User>,
+    app_state: Weak<AppState>,
+}
+
+impl ProjectSharedNotification {
+    fn new(
+        owner: Arc<User>,
+        project_id: u64,
+        worktree_root_names: Vec<String>,
+        app_state: Weak<AppState>,
+    ) -> Self {
+        Self {
+            project_id,
+            worktree_root_names,
+            owner,
+            app_state,
+        }
+    }
+
+    fn join(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(app_state) = self.app_state.upgrade() {
+            workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx)
+                .detach_and_log_err(cx);
+        }
+    }
+
+    fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(active_room) =
+            ActiveCall::global(cx).read_with(cx, |call, _| call.room().cloned())
+        {
+            active_room.update(cx, |_, cx| {
+                cx.emit(room::Event::RemoteProjectInvitationDiscarded {
+                    project_id: self.project_id,
+                });
+            });
+        }
+    }
+
+    fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        let theme = &theme::current(cx).project_shared_notification;
+        Flex::row()
+            .with_children(self.owner.avatar.clone().map(|avatar| {
+                Image::from_data(avatar)
+                    .with_style(theme.owner_avatar)
+                    .aligned()
+            }))
+            .with_child(
+                Flex::column()
+                    .with_child(
+                        Label::new(
+                            self.owner.github_login.clone(),
+                            theme.owner_username.text.clone(),
+                        )
+                        .contained()
+                        .with_style(theme.owner_username.container),
+                    )
+                    .with_child(
+                        Label::new(
+                            format!(
+                                "is sharing a project in Zed{}",
+                                if self.worktree_root_names.is_empty() {
+                                    ""
+                                } else {
+                                    ":"
+                                }
+                            ),
+                            theme.message.text.clone(),
+                        )
+                        .contained()
+                        .with_style(theme.message.container),
+                    )
+                    .with_children(if self.worktree_root_names.is_empty() {
+                        None
+                    } else {
+                        Some(
+                            Label::new(
+                                self.worktree_root_names.join(", "),
+                                theme.worktree_roots.text.clone(),
+                            )
+                            .contained()
+                            .with_style(theme.worktree_roots.container),
+                        )
+                    })
+                    .contained()
+                    .with_style(theme.owner_metadata)
+                    .aligned(),
+            )
+            .contained()
+            .with_style(theme.owner_container)
+            .flex(1., true)
+            .into_any()
+    }
+
+    fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        enum Open {}
+        enum Dismiss {}
+
+        let theme = theme::current(cx);
+        Flex::column()
+            .with_child(
+                MouseEventHandler::new::<Open, _>(0, cx, |_, _| {
+                    let theme = &theme.project_shared_notification;
+                    Label::new("Open", theme.open_button.text.clone())
+                        .aligned()
+                        .contained()
+                        .with_style(theme.open_button.container)
+                })
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_click(MouseButton::Left, move |_, this, cx| this.join(cx))
+                .flex(1., true),
+            )
+            .with_child(
+                MouseEventHandler::new::<Dismiss, _>(0, cx, |_, _| {
+                    let theme = &theme.project_shared_notification;
+                    Label::new("Dismiss", theme.dismiss_button.text.clone())
+                        .aligned()
+                        .contained()
+                        .with_style(theme.dismiss_button.container)
+                })
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_click(MouseButton::Left, |_, this, cx| {
+                    this.dismiss(cx);
+                })
+                .flex(1., true),
+            )
+            .constrained()
+            .with_width(theme.project_shared_notification.button_width)
+            .into_any()
+    }
+}
+
+impl Entity for ProjectSharedNotification {
+    type Event = ();
+}
+
+impl View for ProjectSharedNotification {
+    fn ui_name() -> &'static str {
+        "ProjectSharedNotification"
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
+        let background = theme::current(cx).project_shared_notification.background;
+        Flex::row()
+            .with_child(self.render_owner(cx))
+            .with_child(self.render_buttons(cx))
+            .contained()
+            .with_background_color(background)
+            .expanded()
+            .into_any()
+    }
+}

crates/collab_ui2/src/panel_settings.rs 🔗

@@ -0,0 +1,69 @@
+use anyhow;
+use schemars::JsonSchema;
+use serde_derive::{Deserialize, Serialize};
+use settings::Settings;
+use workspace::dock::DockPosition;
+
+#[derive(Deserialize, Debug)]
+pub struct CollaborationPanelSettings {
+    pub button: bool,
+    pub dock: DockPosition,
+    pub default_width: f32,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct ChatPanelSettings {
+    pub button: bool,
+    pub dock: DockPosition,
+    pub default_width: f32,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct NotificationPanelSettings {
+    pub button: bool,
+    pub dock: DockPosition,
+    pub default_width: f32,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+pub struct PanelSettingsContent {
+    pub button: Option<bool>,
+    pub dock: Option<DockPosition>,
+    pub default_width: Option<f32>,
+}
+
+impl Settings for CollaborationPanelSettings {
+    const KEY: Option<&'static str> = Some("collaboration_panel");
+    type FileContent = PanelSettingsContent;
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &mut gpui::AppContext,
+    ) -> anyhow::Result<Self> {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}
+
+impl Settings for ChatPanelSettings {
+    const KEY: Option<&'static str> = Some("chat_panel");
+    type FileContent = PanelSettingsContent;
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &mut gpui::AppContext,
+    ) -> anyhow::Result<Self> {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}
+
+impl Settings for NotificationPanelSettings {
+    const KEY: Option<&'static str> = Some("notification_panel");
+    type FileContent = PanelSettingsContent;
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &mut gpui::AppContext,
+    ) -> anyhow::Result<Self> {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}

crates/command_palette2/src/command_palette.rs 🔗

@@ -1,9 +1,8 @@
 use collections::{CommandPaletteFilter, HashMap};
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    actions, div, prelude::*, Action, AppContext, Component, Div, EventEmitter, FocusHandle,
-    Keystroke, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView,
-    WindowContext,
+    actions, div, prelude::*, Action, AppContext, Component, Dismiss, Div, FocusHandle, Keystroke,
+    ManagedView, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView,
 };
 use picker::{Picker, PickerDelegate};
 use std::{
@@ -16,7 +15,7 @@ use util::{
     channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
     ResultExt,
 };
-use workspace::{Modal, ModalEvent, Workspace};
+use workspace::Workspace;
 use zed_actions::OpenZedURL;
 
 actions!(Toggle);
@@ -47,7 +46,7 @@ impl CommandPalette {
             .available_actions()
             .into_iter()
             .filter_map(|action| {
-                let name = action.name();
+                let name = gpui::remove_the_2(action.name());
                 let namespace = name.split("::").next().unwrap_or("malformed action name");
                 if filter.is_some_and(|f| f.filtered_namespaces.contains(namespace)) {
                     return None;
@@ -69,10 +68,9 @@ impl CommandPalette {
     }
 }
 
-impl EventEmitter<ModalEvent> for CommandPalette {}
-impl Modal for CommandPalette {
-    fn focus(&self, cx: &mut WindowContext) {
-        self.picker.update(cx, |picker, cx| picker.focus(cx));
+impl ManagedView for CommandPalette {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.picker.focus_handle(cx)
     }
 }
 
@@ -268,7 +266,7 @@ impl PickerDelegate for CommandPaletteDelegate {
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
         self.command_palette
-            .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed))
+            .update(cx, |_, cx| cx.emit(Dismiss))
             .log_err();
     }
 
@@ -457,7 +455,7 @@ mod tests {
     fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
         cx.update(|cx| {
             let app_state = AppState::test(cx);
-            theme::init(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             language::init(cx);
             editor::init(cx);
             workspace::init(app_state.clone(), cx);

crates/copilot/src/copilot.rs 🔗

@@ -1051,17 +1051,15 @@ mod tests {
         );
 
         // Ensure updates to the file are reflected in the LSP.
-        buffer_1
-            .update(cx, |buffer, cx| {
-                buffer.file_updated(
-                    Arc::new(File {
-                        abs_path: "/root/child/buffer-1".into(),
-                        path: Path::new("child/buffer-1").into(),
-                    }),
-                    cx,
-                )
-            })
-            .await;
+        buffer_1.update(cx, |buffer, cx| {
+            buffer.file_updated(
+                Arc::new(File {
+                    abs_path: "/root/child/buffer-1".into(),
+                    path: Path::new("child/buffer-1").into(),
+                }),
+                cx,
+            )
+        });
         assert_eq!(
             lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
                 .await,

crates/diagnostics2/src/diagnostics.rs 🔗

@@ -14,8 +14,8 @@ use editor::{
 use futures::future::try_join_all;
 use gpui::{
     actions, div, AnyElement, AnyView, AppContext, Component, Context, Div, EventEmitter,
-    FocusEvent, FocusHandle, Focusable, FocusableComponent, InteractiveComponent, Model,
-    ParentComponent, Render, SharedString, Styled, Subscription, Task, View, ViewContext,
+    FocusEvent, FocusHandle, Focusable, FocusableComponent, InteractiveComponent, ManagedView,
+    Model, ParentComponent, Render, SharedString, Styled, Subscription, Task, View, ViewContext,
     VisualContext, WeakView,
 };
 use language::{
@@ -641,11 +641,13 @@ impl ProjectDiagnosticsEditor {
     }
 }
 
-impl Item for ProjectDiagnosticsEditor {
-    fn focus_handle(&self) -> FocusHandle {
+impl ManagedView for ProjectDiagnosticsEditor {
+    fn focus_handle(&self, _: &AppContext) -> FocusHandle {
         self.focus_handle.clone()
     }
+}
 
+impl Item for ProjectDiagnosticsEditor {
     fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
         self.editor.update(cx, |editor, cx| editor.deactivated(cx));
     }
@@ -1583,7 +1585,7 @@ mod tests {
         cx.update(|cx| {
             let settings = SettingsStore::test(cx);
             cx.set_global(settings);
-            theme::init(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             language::init(cx);
             client::init_settings(cx);
             workspace::init_settings(cx);

crates/diagnostics2/src/toolbar_controls.rs 🔗

@@ -1,9 +1,9 @@
 use crate::{ProjectDiagnosticsEditor, ToggleWarnings};
 use gpui::{
     div, Action, CursorStyle, Div, Entity, EventEmitter, MouseButton, ParentComponent, Render,
-    View, ViewContext, WeakView,
+    View, ViewContext, VisualContext, WeakView,
 };
-use ui::{Icon, IconButton, StyledExt};
+use ui::{Icon, IconButton, Label, StyledExt, Tooltip};
 use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
 
 pub struct ToolbarControls {
@@ -29,7 +29,7 @@ impl Render for ToolbarControls {
 
         div().child(
             IconButton::new("toggle-warnings", Icon::ExclamationTriangle)
-                .tooltip(tooltip)
+                .tooltip(move |_, cx| Tooltip::text(tooltip, cx))
                 .on_click(|this: &mut Self, cx| {
                     if let Some(editor) = this.editor.as_ref().and_then(|editor| editor.upgrade()) {
                         editor.update(cx, |editor, cx| {

crates/editor2/src/display_map.rs 🔗

@@ -13,7 +13,8 @@ pub use block_map::{BlockMap, BlockPoint};
 use collections::{BTreeMap, HashMap, HashSet};
 use fold_map::FoldMap;
 use gpui::{
-    Font, FontId, HighlightStyle, Hsla, Line, Model, ModelContext, Pixels, TextRun, UnderlineStyle,
+    Font, FontId, HighlightStyle, Hsla, LineLayout, Model, ModelContext, Pixels, ShapedLine,
+    TextRun, UnderlineStyle, WrappedLine,
 };
 use inlay_map::InlayMap;
 use language::{
@@ -561,7 +562,7 @@ impl DisplaySnapshot {
         })
     }
 
-    pub fn lay_out_line_for_row(
+    pub fn layout_row(
         &self,
         display_row: u32,
         TextLayoutDetails {
@@ -569,7 +570,7 @@ impl DisplaySnapshot {
             editor_style,
             rem_size,
         }: &TextLayoutDetails,
-    ) -> Line {
+    ) -> Arc<LineLayout> {
         let mut runs = Vec::new();
         let mut line = String::new();
 
@@ -598,29 +599,27 @@ impl DisplaySnapshot {
 
         let font_size = editor_style.text.font_size.to_pixels(*rem_size);
         text_system
-            .layout_text(&line, font_size, &runs, None)
-            .unwrap()
-            .pop()
-            .unwrap()
+            .layout_line(&line, font_size, &runs)
+            .expect("we expect the font to be loaded because it's rendered by the editor")
     }
 
-    pub fn x_for_point(
+    pub fn x_for_display_point(
         &self,
         display_point: DisplayPoint,
         text_layout_details: &TextLayoutDetails,
     ) -> Pixels {
-        let layout_line = self.lay_out_line_for_row(display_point.row(), text_layout_details);
-        layout_line.x_for_index(display_point.column() as usize)
+        let line = self.layout_row(display_point.row(), text_layout_details);
+        line.x_for_index(display_point.column() as usize)
     }
 
-    pub fn column_for_x(
+    pub fn display_column_for_x(
         &self,
         display_row: u32,
-        x_coordinate: Pixels,
-        text_layout_details: &TextLayoutDetails,
+        x: Pixels,
+        details: &TextLayoutDetails,
     ) -> u32 {
-        let layout_line = self.lay_out_line_for_row(display_row, text_layout_details);
-        layout_line.closest_index_for_x(x_coordinate) as u32
+        let layout_line = self.layout_row(display_row, details);
+        layout_line.closest_index_for_x(x) as u32
     }
 
     pub fn chars_at(

crates/editor2/src/display_map/inlay_map.rs 🔗

@@ -1891,6 +1891,6 @@ mod tests {
     fn init_test(cx: &mut AppContext) {
         let store = SettingsStore::test(cx);
         cx.set_global(store);
-        theme::init(cx);
+        theme::init(theme::LoadThemes::JustBase, cx);
     }
 }

crates/editor2/src/editor.rs 🔗

@@ -39,10 +39,10 @@ use futures::FutureExt;
 use fuzzy::{StringMatch, StringMatchCandidate};
 use git::diff_hunk_to_display;
 use gpui::{
-    action, actions, div, point, prelude::*, px, relative, rems, size, uniform_list, AnyElement,
+    actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement,
     AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context,
-    EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight, HighlightStyle, Hsla,
-    InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels, Render, Styled,
+    EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle,
+    Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels, Render, Styled,
     Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext,
     WeakView, WindowContext,
 };
@@ -97,7 +97,7 @@ use text::{OffsetUtf16, Rope};
 use theme::{
     ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings,
 };
-use ui::{v_stack, HighlightedLabel, IconButton, StyledExt, TextTooltip};
+use ui::{v_stack, HighlightedLabel, IconButton, StyledExt, Tooltip};
 use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
 use workspace::{
     item::{ItemEvent, ItemHandle},
@@ -180,78 +180,78 @@ pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
 //     //     .with_soft_wrap(true)
 // }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct SelectNext {
     #[serde(default)]
     pub replace_newest: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct SelectPrevious {
     #[serde(default)]
     pub replace_newest: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct SelectAllMatches {
     #[serde(default)]
     pub replace_newest: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct SelectToBeginningOfLine {
     #[serde(default)]
     stop_at_soft_wraps: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct MovePageUp {
     #[serde(default)]
     center_cursor: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct MovePageDown {
     #[serde(default)]
     center_cursor: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct SelectToEndOfLine {
     #[serde(default)]
     stop_at_soft_wraps: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct ToggleCodeActions {
     #[serde(default)]
     pub deployed_from_indicator: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct ConfirmCompletion {
     #[serde(default)]
     pub item_ix: Option<usize>,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct ConfirmCodeAction {
     #[serde(default)]
     pub item_ix: Option<usize>,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct ToggleComments {
     #[serde(default)]
     pub advance_downwards: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct FoldAt {
     pub buffer_row: u32,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct UnfoldAt {
     pub buffer_row: u32,
 }
@@ -5445,7 +5445,9 @@ impl Editor {
                     *head.column_mut() += 1;
                     head = display_map.clip_point(head, Bias::Right);
                     let goal = SelectionGoal::HorizontalPosition(
-                        display_map.x_for_point(head, &text_layout_details).into(),
+                        display_map
+                            .x_for_display_point(head, &text_layout_details)
+                            .into(),
                     );
                     selection.collapse_to(head, goal);
 
@@ -6391,8 +6393,8 @@ impl Editor {
             let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone();
             let range = oldest_selection.display_range(&display_map).sorted();
 
-            let start_x = display_map.x_for_point(range.start, &text_layout_details);
-            let end_x = display_map.x_for_point(range.end, &text_layout_details);
+            let start_x = display_map.x_for_display_point(range.start, &text_layout_details);
+            let end_x = display_map.x_for_display_point(range.end, &text_layout_details);
             let positions = start_x.min(end_x)..start_x.max(end_x);
 
             selections.clear();
@@ -6431,15 +6433,16 @@ impl Editor {
                     let range = selection.display_range(&display_map).sorted();
                     debug_assert_eq!(range.start.row(), range.end.row());
                     let mut row = range.start.row();
-                    let positions = if let SelectionGoal::HorizontalRange { start, end } =
-                        selection.goal
-                    {
-                        px(start)..px(end)
-                    } else {
-                        let start_x = display_map.x_for_point(range.start, &text_layout_details);
-                        let end_x = display_map.x_for_point(range.end, &text_layout_details);
-                        start_x.min(end_x)..start_x.max(end_x)
-                    };
+                    let positions =
+                        if let SelectionGoal::HorizontalRange { start, end } = selection.goal {
+                            px(start)..px(end)
+                        } else {
+                            let start_x =
+                                display_map.x_for_display_point(range.start, &text_layout_details);
+                            let end_x =
+                                display_map.x_for_display_point(range.end, &text_layout_details);
+                            start_x.min(end_x)..start_x.max(end_x)
+                        };
 
                     while row != end_row {
                         if above {
@@ -6992,7 +6995,7 @@ impl Editor {
                         let display_point = point.to_display_point(display_snapshot);
                         let goal = SelectionGoal::HorizontalPosition(
                             display_snapshot
-                                .x_for_point(display_point, &text_layout_details)
+                                .x_for_display_point(display_point, &text_layout_details)
                                 .into(),
                         );
                         (display_point, goal)
@@ -9372,24 +9375,28 @@ pub struct EditorReleased(pub WeakView<Editor>);
 //
 impl EventEmitter<EditorEvent> for Editor {}
 
+impl FocusableView for Editor {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
 impl Render for Editor {
     type Element = EditorElement;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
         let settings = ThemeSettings::get_global(cx);
         let text_style = match self.mode {
-            EditorMode::SingleLine => {
-                TextStyle {
-                    color: cx.theme().colors().text,
-                    font_family: settings.ui_font.family.clone(), // todo!()
-                    font_features: settings.ui_font.features,
-                    font_size: rems(0.875).into(),
-                    font_weight: FontWeight::NORMAL,
-                    font_style: FontStyle::Normal,
-                    line_height: relative(1.3).into(), // TODO relative(settings.buffer_line_height.value()),
-                    underline: None,
-                }
-            }
+            EditorMode::SingleLine => TextStyle {
+                color: cx.theme().colors().text,
+                font_family: settings.ui_font.family.clone(),
+                font_features: settings.ui_font.features,
+                font_size: rems(0.875).into(),
+                font_weight: FontWeight::NORMAL,
+                font_style: FontStyle::Normal,
+                line_height: relative(1.).into(),
+                underline: None,
+            },
 
             EditorMode::AutoHeight { max_lines } => todo!(),
 
@@ -9760,7 +9767,8 @@ impl InputHandler for Editor {
         let scroll_left = scroll_position.x * em_width;
 
         let start = OffsetUtf16(range_utf16.start).to_display_point(&snapshot);
-        let x = snapshot.x_for_point(start, &text_layout_details) - scroll_left + self.gutter_width;
+        let x = snapshot.x_for_display_point(start, &text_layout_details) - scroll_left
+            + self.gutter_width;
         let y = line_height * (start.row() as f32 - scroll_position.y);
 
         Some(Bounds {
@@ -9990,7 +9998,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
             .on_click(move |_, _, cx| {
                 cx.write_to_clipboard(ClipboardItem::new(message.clone()));
             })
-            .tooltip(|_, cx| cx.build_view(|cx| TextTooltip::new("Copy diagnostic message")))
+            .tooltip(|_, cx| Tooltip::text("Copy diagnostic message", cx))
             .render()
     })
 }

crates/editor2/src/editor_tests.rs 🔗

@@ -8277,7 +8277,7 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC
     cx.update(|cx| {
         let store = SettingsStore::test(cx);
         cx.set_global(store);
-        theme::init(cx);
+        theme::init(theme::LoadThemes::JustBase, cx);
         client::init_settings(cx);
         language::init(cx);
         Project::init_settings(cx);

crates/editor2/src/element.rs 🔗

@@ -12,18 +12,18 @@ use crate::{
     },
     scroll::scroll_amount::ScrollAmount,
     CursorShape, DisplayPoint, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle,
-    HalfPageDown, HalfPageUp, LineDown, LineUp, MoveDown, PageDown, PageUp, Point, SelectPhase,
-    Selection, SoftWrap, ToPoint, MAX_LINE_LEN,
+    HalfPageDown, HalfPageUp, LineDown, LineUp, MoveDown, OpenExcerpts, PageDown, PageUp, Point,
+    SelectPhase, Selection, SoftWrap, ToPoint, MAX_LINE_LEN,
 };
 use anyhow::Result;
 use collections::{BTreeMap, HashMap};
 use gpui::{
     div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace,
     BorrowWindow, Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element,
-    ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveComponent, Line,
+    ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveComponent, LineLayout,
     MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels,
-    ScrollWheelEvent, Size, StatefulInteractiveComponent, Style, Styled, TextRun, TextStyle, View,
-    ViewContext, WindowContext,
+    ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveComponent, Style, Styled,
+    TextRun, TextStyle, View, ViewContext, WindowContext, WrappedLine,
 };
 use itertools::Itertools;
 use language::language_settings::ShowWhitespaceSetting;
@@ -45,7 +45,7 @@ use std::{
 };
 use sum_tree::Bias;
 use theme::{ActiveTheme, PlayerColor};
-use ui::{h_stack, IconButton};
+use ui::{h_stack, IconButton, Tooltip};
 use util::ResultExt;
 use workspace::item::Item;
 
@@ -476,7 +476,7 @@ impl EditorElement {
             Self::paint_diff_hunks(bounds, layout, cx);
         }
 
-        for (ix, line) in layout.line_number_layouts.iter().enumerate() {
+        for (ix, line) in layout.line_numbers.iter().enumerate() {
             if let Some(line) = line {
                 let line_origin = bounds.origin
                     + point(
@@ -775,21 +775,21 @@ impl EditorElement {
                                         .chars_at(cursor_position)
                                         .next()
                                         .and_then(|(character, _)| {
-                                            let text = character.to_string();
+                                            let text = SharedString::from(character.to_string());
+                                            let len = text.len();
                                             cx.text_system()
-                                                .layout_text(
-                                                    &text,
+                                                .shape_line(
+                                                    text,
                                                     cursor_row_layout.font_size,
                                                     &[TextRun {
-                                                        len: text.len(),
+                                                        len,
                                                         font: self.style.text.font(),
                                                         color: self.style.background,
+                                                        background_color: None,
                                                         underline: None,
                                                     }],
-                                                    None,
                                                 )
-                                                .unwrap()
-                                                .pop()
+                                                .log_err()
                                         })
                                 } else {
                                     None
@@ -1244,20 +1244,20 @@ impl EditorElement {
         let font_size = style.text.font_size.to_pixels(cx.rem_size());
         let layout = cx
             .text_system()
-            .layout_text(
-                " ".repeat(column).as_str(),
+            .shape_line(
+                SharedString::from(" ".repeat(column)),
                 font_size,
                 &[TextRun {
                     len: column,
                     font: style.text.font(),
                     color: Hsla::default(),
+                    background_color: None,
                     underline: None,
                 }],
-                None,
             )
             .unwrap();
 
-        layout[0].width
+        layout.width
     }
 
     fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext<Editor>) -> Pixels {
@@ -1338,7 +1338,7 @@ impl EditorElement {
         relative_rows
     }
 
-    fn layout_line_numbers(
+    fn shape_line_numbers(
         &self,
         rows: Range<u32>,
         active_rows: &BTreeMap<u32, bool>,
@@ -1347,12 +1347,12 @@ impl EditorElement {
         snapshot: &EditorSnapshot,
         cx: &ViewContext<Editor>,
     ) -> (
-        Vec<Option<gpui::Line>>,
+        Vec<Option<ShapedLine>>,
         Vec<Option<(FoldStatus, BufferRow, bool)>>,
     ) {
         let font_size = self.style.text.font_size.to_pixels(cx.rem_size());
         let include_line_numbers = snapshot.mode == EditorMode::Full;
-        let mut line_number_layouts = Vec::with_capacity(rows.len());
+        let mut shaped_line_numbers = Vec::with_capacity(rows.len());
         let mut fold_statuses = Vec::with_capacity(rows.len());
         let mut line_number = String::new();
         let is_relative = EditorSettings::get_global(cx).relative_line_numbers;
@@ -1387,15 +1387,14 @@ impl EditorElement {
                         len: line_number.len(),
                         font: self.style.text.font(),
                         color,
+                        background_color: None,
                         underline: None,
                     };
-                    let layout = cx
+                    let shaped_line = cx
                         .text_system()
-                        .layout_text(&line_number, font_size, &[run], None)
-                        .unwrap()
-                        .pop()
+                        .shape_line(line_number.clone().into(), font_size, &[run])
                         .unwrap();
-                    line_number_layouts.push(Some(layout));
+                    shaped_line_numbers.push(Some(shaped_line));
                     fold_statuses.push(
                         is_singleton
                             .then(|| {
@@ -1408,17 +1407,17 @@ impl EditorElement {
                 }
             } else {
                 fold_statuses.push(None);
-                line_number_layouts.push(None);
+                shaped_line_numbers.push(None);
             }
         }
 
-        (line_number_layouts, fold_statuses)
+        (shaped_line_numbers, fold_statuses)
     }
 
     fn layout_lines(
         &mut self,
         rows: Range<u32>,
-        line_number_layouts: &[Option<Line>],
+        line_number_layouts: &[Option<ShapedLine>],
         snapshot: &EditorSnapshot,
         cx: &ViewContext<Editor>,
     ) -> Vec<LineWithInvisibles> {
@@ -1439,18 +1438,17 @@ impl EditorElement {
                 .chain(iter::repeat(""))
                 .take(rows.len());
             placeholder_lines
-                .map(|line| {
+                .filter_map(move |line| {
                     let run = TextRun {
                         len: line.len(),
                         font: self.style.text.font(),
                         color: placeholder_color,
+                        background_color: None,
                         underline: Default::default(),
                     };
                     cx.text_system()
-                        .layout_text(line, font_size, &[run], None)
-                        .unwrap()
-                        .pop()
-                        .unwrap()
+                        .shape_line(line.to_string().into(), font_size, &[run])
+                        .log_err()
                 })
                 .map(|line| LineWithInvisibles {
                     line,
@@ -1726,7 +1724,7 @@ impl EditorElement {
             .head
         });
 
-        let (line_number_layouts, fold_statuses) = self.layout_line_numbers(
+        let (line_numbers, fold_statuses) = self.shape_line_numbers(
             start_row..end_row,
             &active_rows,
             head_for_relative,
@@ -1740,8 +1738,7 @@ impl EditorElement {
         let scrollbar_row_range = scroll_position.y..(scroll_position.y + height_in_lines);
 
         let mut max_visible_line_width = Pixels::ZERO;
-        let line_layouts =
-            self.layout_lines(start_row..end_row, &line_number_layouts, &snapshot, cx);
+        let line_layouts = self.layout_lines(start_row..end_row, &line_numbers, &snapshot, cx);
         for line_with_invisibles in &line_layouts {
             if line_with_invisibles.line.width > max_visible_line_width {
                 max_visible_line_width = line_with_invisibles.line.width;
@@ -1879,35 +1876,31 @@ impl EditorElement {
         let invisible_symbol_font_size = font_size / 2.;
         let tab_invisible = cx
             .text_system()
-            .layout_text(
-                "→",
+            .shape_line(
+                "→".into(),
                 invisible_symbol_font_size,
                 &[TextRun {
                     len: "→".len(),
                     font: self.style.text.font(),
                     color: cx.theme().colors().editor_invisible,
+                    background_color: None,
                     underline: None,
                 }],
-                None,
             )
-            .unwrap()
-            .pop()
             .unwrap();
         let space_invisible = cx
             .text_system()
-            .layout_text(
-                "•",
+            .shape_line(
+                "•".into(),
                 invisible_symbol_font_size,
                 &[TextRun {
                     len: "•".len(),
                     font: self.style.text.font(),
                     color: cx.theme().colors().editor_invisible,
+                    background_color: None,
                     underline: None,
                 }],
-                None,
             )
-            .unwrap()
-            .pop()
             .unwrap();
 
         LayoutState {
@@ -1939,7 +1932,7 @@ impl EditorElement {
             active_rows,
             highlighted_rows,
             highlighted_ranges,
-            line_number_layouts,
+            line_numbers,
             display_hunks,
             blocks,
             selections,
@@ -2038,7 +2031,9 @@ impl EditorElement {
                             .on_click(move |editor: &mut Editor, cx| {
                                 editor.jump(jump_path.clone(), jump_position, jump_anchor, cx);
                             })
-                            .tooltip("Jump to Buffer") // todo!(pass an action as well to show key binding)
+                            .tooltip(move |_, cx| {
+                                Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx)
+                            })
                     });
 
                     let element = if *starts_new_buffer {
@@ -2203,7 +2198,7 @@ impl EditorElement {
 
 #[derive(Debug)]
 pub struct LineWithInvisibles {
-    pub line: Line,
+    pub line: ShapedLine,
     invisibles: Vec<Invisible>,
 }
 
@@ -2213,7 +2208,7 @@ impl LineWithInvisibles {
         text_style: &TextStyle,
         max_line_len: usize,
         max_line_count: usize,
-        line_number_layouts: &[Option<Line>],
+        line_number_layouts: &[Option<ShapedLine>],
         editor_mode: EditorMode,
         cx: &WindowContext,
     ) -> Vec<Self> {
@@ -2233,11 +2228,12 @@ impl LineWithInvisibles {
         }]) {
             for (ix, mut line_chunk) in highlighted_chunk.chunk.split('\n').enumerate() {
                 if ix > 0 {
-                    let layout = cx
+                    let shaped_line = cx
                         .text_system()
-                        .layout_text(&line, font_size, &styles, None);
+                        .shape_line(line.clone().into(), font_size, &styles)
+                        .unwrap();
                     layouts.push(Self {
-                        line: layout.unwrap().pop().unwrap(),
+                        line: shaped_line,
                         invisibles: invisibles.drain(..).collect(),
                     });
 
@@ -2271,6 +2267,7 @@ impl LineWithInvisibles {
                         len: line_chunk.len(),
                         font: text_style.font(),
                         color: text_style.color,
+                        background_color: None,
                         underline: text_style.underline,
                     });
 
@@ -2402,21 +2399,14 @@ impl Element<Editor> for EditorElement {
         Some(self.editor_id.into())
     }
 
-    fn initialize(
+    fn layout(
         &mut self,
         editor: &mut Editor,
         element_state: Option<Self::ElementState>,
         cx: &mut gpui::ViewContext<Editor>,
-    ) -> Self::ElementState {
+    ) -> (gpui::LayoutId, Self::ElementState) {
         editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this.
-    }
 
-    fn layout(
-        &mut self,
-        editor: &mut Editor,
-        element_state: &mut Self::ElementState,
-        cx: &mut gpui::ViewContext<Editor>,
-    ) -> gpui::LayoutId {
         let rem_size = cx.rem_size();
         let mut style = Style::default();
         style.size.width = relative(1.).into();
@@ -2425,7 +2415,8 @@ impl Element<Editor> for EditorElement {
             EditorMode::AutoHeight { .. } => todo!(),
             EditorMode::Full => relative(1.).into(),
         };
-        cx.request_layout(&style, None)
+        let layout_id = cx.request_layout(&style, None);
+        (layout_id, ())
     }
 
     fn paint(
@@ -3097,7 +3088,7 @@ pub struct LayoutState {
     visible_display_row_range: Range<u32>,
     active_rows: BTreeMap<u32, bool>,
     highlighted_rows: Option<Range<u32>>,
-    line_number_layouts: Vec<Option<gpui::Line>>,
+    line_numbers: Vec<Option<ShapedLine>>,
     display_hunks: Vec<DisplayDiffHunk>,
     blocks: Vec<BlockLayout>,
     highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
@@ -3110,8 +3101,8 @@ pub struct LayoutState {
     code_actions_indicator: Option<CodeActionsIndicator>,
     // hover_popovers: Option<(DisplayPoint, Vec<AnyElement<Editor>>)>,
     fold_indicators: Vec<Option<AnyElement<Editor>>>,
-    tab_invisible: Line,
-    space_invisible: Line,
+    tab_invisible: ShapedLine,
+    space_invisible: ShapedLine,
 }
 
 struct CodeActionsIndicator {
@@ -3211,7 +3202,7 @@ fn layout_line(
     snapshot: &EditorSnapshot,
     style: &EditorStyle,
     cx: &WindowContext,
-) -> Result<Line> {
+) -> Result<ShapedLine> {
     let mut line = snapshot.line(row);
 
     if line.len() > MAX_LINE_LEN {
@@ -3223,21 +3214,17 @@ fn layout_line(
         line.truncate(len);
     }
 
-    Ok(cx
-        .text_system()
-        .layout_text(
-            &line,
-            style.text.font_size.to_pixels(cx.rem_size()),
-            &[TextRun {
-                len: snapshot.line_len(row) as usize,
-                font: style.text.font(),
-                color: Hsla::default(),
-                underline: None,
-            }],
-            None,
-        )?
-        .pop()
-        .unwrap())
+    cx.text_system().shape_line(
+        line.into(),
+        style.text.font_size.to_pixels(cx.rem_size()),
+        &[TextRun {
+            len: snapshot.line_len(row) as usize,
+            font: style.text.font(),
+            color: Hsla::default(),
+            background_color: None,
+            underline: None,
+        }],
+    )
 }
 
 #[derive(Debug)]
@@ -3247,7 +3234,7 @@ pub struct Cursor {
     line_height: Pixels,
     color: Hsla,
     shape: CursorShape,
-    block_text: Option<Line>,
+    block_text: Option<ShapedLine>,
 }
 
 impl Cursor {
@@ -3257,7 +3244,7 @@ impl Cursor {
         line_height: Pixels,
         color: Hsla,
         shape: CursorShape,
-        block_text: Option<Line>,
+        block_text: Option<ShapedLine>,
     ) -> Cursor {
         Cursor {
             origin,

crates/editor2/src/inlay_hint_cache.rs 🔗

@@ -3179,7 +3179,7 @@ all hints should be invalidated and requeried for all of its visible excerpts"
         cx.update(|cx| {
             let settings_store = SettingsStore::test(cx);
             cx.set_global(settings_store);
-            theme::init(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             client::init_settings(cx);
             language::init(cx);
             Project::init_settings(cx);

crates/editor2/src/items.rs 🔗

@@ -528,10 +528,6 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor)
 }
 
 impl Item for Editor {
-    fn focus_handle(&self) -> FocusHandle {
-        self.focus_handle.clone()
-    }
-
     fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
         todo!();
         // if let Ok(data) = data.downcast::<NavigationData>() {
@@ -802,7 +798,7 @@ impl Item for Editor {
 
     fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
         let workspace_id = workspace.database_id();
-        let item_id = cx.view().entity_id().as_u64() as ItemId;
+        let item_id = cx.view().item_id().as_u64() as ItemId;
         self.workspace = Some((workspace.weak_handle(), workspace.database_id()));
 
         fn serialize(
@@ -833,7 +829,7 @@ impl Item for Editor {
                         serialize(
                             buffer,
                             *workspace_id,
-                            cx.view().entity_id().as_u64() as ItemId,
+                            cx.view().item_id().as_u64() as ItemId,
                             cx,
                         );
                     }

crates/editor2/src/movement.rs 🔗

@@ -98,7 +98,7 @@ pub fn up_by_rows(
         SelectionGoal::HorizontalPosition(x) => x.into(), // todo!("Can the fields in SelectionGoal by Pixels? We should extract a geometry crate and depend on that.")
         SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
         SelectionGoal::HorizontalRange { end, .. } => end.into(),
-        _ => map.x_for_point(start, text_layout_details),
+        _ => map.x_for_display_point(start, text_layout_details),
     };
 
     let prev_row = start.row().saturating_sub(row_count);
@@ -107,7 +107,7 @@ pub fn up_by_rows(
         Bias::Left,
     );
     if point.row() < start.row() {
-        *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
+        *point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details)
     } else if preserve_column_at_start {
         return (start, goal);
     } else {
@@ -137,18 +137,18 @@ pub fn down_by_rows(
         SelectionGoal::HorizontalPosition(x) => x.into(),
         SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
         SelectionGoal::HorizontalRange { end, .. } => end.into(),
-        _ => map.x_for_point(start, text_layout_details),
+        _ => map.x_for_display_point(start, text_layout_details),
     };
 
     let new_row = start.row() + row_count;
     let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
     if point.row() > start.row() {
-        *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
+        *point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details)
     } else if preserve_column_at_end {
         return (start, goal);
     } else {
         point = map.max_point();
-        goal_x = map.x_for_point(point, text_layout_details)
+        goal_x = map.x_for_display_point(point, text_layout_details)
     }
 
     let mut clipped_point = map.clip_point(point, Bias::Right);

crates/editor2/src/scroll.rs 🔗

@@ -426,7 +426,7 @@ impl Editor {
 
     pub fn read_scroll_position_from_db(
         &mut self,
-        item_id: usize,
+        item_id: u64,
         workspace_id: WorkspaceId,
         cx: &mut ViewContext<Editor>,
     ) {

crates/editor2/src/selections_collection.rs 🔗

@@ -313,14 +313,14 @@ impl SelectionsCollection {
         let is_empty = positions.start == positions.end;
         let line_len = display_map.line_len(row);
 
-        let layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details);
+        let line = display_map.layout_row(row, &text_layout_details);
 
         dbg!("****START COL****");
-        let start_col = layed_out_line.closest_index_for_x(positions.start) as u32;
-        if start_col < line_len || (is_empty && positions.start == layed_out_line.width) {
+        let start_col = line.closest_index_for_x(positions.start) as u32;
+        if start_col < line_len || (is_empty && positions.start == line.width) {
             let start = DisplayPoint::new(row, start_col);
             dbg!("****END COL****");
-            let end_col = layed_out_line.closest_index_for_x(positions.end) as u32;
+            let end_col = line.closest_index_for_x(positions.end) as u32;
             let end = DisplayPoint::new(row, end_col);
             dbg!(start_col, end_col);
 

crates/file_finder2/src/file_finder.rs 🔗

@@ -2,9 +2,9 @@ use collections::HashMap;
 use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
 use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
 use gpui::{
-    actions, div, AppContext, Component, Div, EventEmitter, InteractiveComponent, Model,
-    ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
-    WindowContext,
+    actions, div, AppContext, Component, Dismiss, Div, FocusHandle, InteractiveComponent,
+    ManagedView, Model, ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext,
+    WeakView,
 };
 use picker::{Picker, PickerDelegate};
 use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
@@ -19,7 +19,7 @@ use text::Point;
 use theme::ActiveTheme;
 use ui::{v_stack, HighlightedLabel, StyledExt};
 use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
-use workspace::{Modal, ModalEvent, Workspace};
+use workspace::Workspace;
 
 actions!(Toggle);
 
@@ -111,10 +111,9 @@ impl FileFinder {
     }
 }
 
-impl EventEmitter<ModalEvent> for FileFinder {}
-impl Modal for FileFinder {
-    fn focus(&self, cx: &mut WindowContext) {
-        self.picker.update(cx, |picker, cx| picker.focus(cx))
+impl ManagedView for FileFinder {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.picker.focus_handle(cx)
     }
 }
 impl Render for FileFinder {
@@ -689,9 +688,7 @@ impl PickerDelegate for FileFinderDelegate {
                                 .log_err();
                         }
                     }
-                    finder
-                        .update(&mut cx, |_, cx| cx.emit(ModalEvent::Dismissed))
-                        .ok()?;
+                    finder.update(&mut cx, |_, cx| cx.emit(Dismiss)).ok()?;
 
                     Some(())
                 })
@@ -702,7 +699,7 @@ impl PickerDelegate for FileFinderDelegate {
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
         self.file_finder
-            .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed))
+            .update(cx, |_, cx| cx.emit(Dismiss))
             .log_err();
     }
 
@@ -1763,7 +1760,7 @@ mod tests {
     fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
         cx.update(|cx| {
             let state = AppState::test(cx);
-            theme::init(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             language::init(cx);
             super::init(cx);
             editor::init(cx);

crates/go_to_line2/src/go_to_line.rs 🔗

@@ -1,13 +1,13 @@
 use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
 use gpui::{
-    actions, div, prelude::*, AppContext, Div, EventEmitter, ParentComponent, Render, SharedString,
-    Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
+    actions, div, prelude::*, AppContext, Dismiss, Div, FocusHandle, ManagedView, ParentComponent,
+    Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
 };
 use text::{Bias, Point};
 use theme::ActiveTheme;
 use ui::{h_stack, v_stack, Label, StyledExt, TextColor};
 use util::paths::FILE_ROW_COLUMN_DELIMITER;
-use workspace::{Modal, ModalEvent, Workspace};
+use workspace::Workspace;
 
 actions!(Toggle);
 
@@ -23,10 +23,9 @@ pub struct GoToLine {
     _subscriptions: Vec<Subscription>,
 }
 
-impl EventEmitter<ModalEvent> for GoToLine {}
-impl Modal for GoToLine {
-    fn focus(&self, cx: &mut WindowContext) {
-        self.line_editor.update(cx, |editor, cx| editor.focus(cx))
+impl ManagedView for GoToLine {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.line_editor.focus_handle(cx)
     }
 }
 
@@ -88,7 +87,7 @@ impl GoToLine {
     ) {
         match event {
             // todo!() this isn't working...
-            editor::EditorEvent::Blurred => cx.emit(ModalEvent::Dismissed),
+            editor::EditorEvent::Blurred => cx.emit(Dismiss),
             editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
             _ => {}
         }
@@ -123,7 +122,7 @@ impl GoToLine {
     }
 
     fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
-        cx.emit(ModalEvent::Dismissed);
+        cx.emit(Dismiss);
     }
 
     fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
@@ -140,7 +139,7 @@ impl GoToLine {
             self.prev_scroll_position.take();
         }
 
-        cx.emit(ModalEvent::Dismissed);
+        cx.emit(Dismiss);
     }
 }
 

crates/gpui2/Cargo.toml 🔗

@@ -22,6 +22,7 @@ sqlez = { path = "../sqlez" }
 async-task = "4.0.3"
 backtrace = { version = "0.3", optional = true }
 ctor.workspace = true
+linkme = "0.3"
 derive_more.workspace = true
 dhat = { version = "0.3", optional = true }
 env_logger = { version = "0.9", optional = true }

crates/gpui2/src/action.rs 🔗

@@ -1,10 +1,12 @@
 use crate::SharedString;
 use anyhow::{anyhow, Context, Result};
 use collections::HashMap;
-use lazy_static::lazy_static;
-use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard};
-use serde::Deserialize;
-use std::any::{type_name, Any, TypeId};
+pub use no_action::NoAction;
+use serde_json::json;
+use std::{
+    any::{Any, TypeId},
+    ops::Deref,
+};
 
 /// Actions are used to implement keyboard-driven UI.
 /// When you declare an action, you can bind keys to the action in the keymap and
@@ -15,24 +17,16 @@ use std::any::{type_name, Any, TypeId};
 /// ```rust
 /// actions!(MoveUp, MoveDown, MoveLeft, MoveRight, Newline);
 /// ```
-/// More complex data types can also be actions. If you annotate your type with the `#[action]` proc macro,
-/// it will automatically
+/// More complex data types can also be actions. If you annotate your type with the action derive macro
+/// it will be implemented and registered automatically.
 /// ```
-/// #[action]
+/// #[derive(Clone, PartialEq, serde_derive::Deserialize, Action)]
 /// pub struct SelectNext {
 ///     pub replace_newest: bool,
 /// }
 ///
-/// Any type A that satisfies the following bounds is automatically an action:
-///
-/// ```
-/// A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + std::fmt::Debug + 'static,
-/// ```
-///
-/// The `#[action]` annotation will derive these implementations for your struct automatically. If you
-/// want to control them manually, you can use the lower-level `#[register_action]` macro, which only
-/// generates the code needed to register your action before `main`. Then you'll need to implement all
-/// the traits manually.
+/// If you want to control the behavior of the action trait manually, you can use the lower-level `#[register_action]`
+/// macro, which only generates the code needed to register your action before `main`.
 ///
 /// ```
 /// #[gpui::register_action]
@@ -41,77 +35,29 @@ use std::any::{type_name, Any, TypeId};
 ///     pub content: SharedString,
 /// }
 ///
-/// impl std::default::Default for Paste {
-///     fn default() -> Self {
-///         Self {
-///             content: SharedString::from("🍝"),
-///         }
-///     }
+/// impl gpui::Action for Paste {
+///      ///...
 /// }
 /// ```
-pub trait Action: std::fmt::Debug + 'static {
-    fn qualified_name() -> SharedString
-    where
-        Self: Sized;
-    fn build(value: Option<serde_json::Value>) -> Result<Box<dyn Action>>
+pub trait Action: 'static {
+    fn boxed_clone(&self) -> Box<dyn Action>;
+    fn as_any(&self) -> &dyn Any;
+    fn partial_eq(&self, action: &dyn Action) -> bool;
+    fn name(&self) -> &str;
+
+    fn debug_name() -> &'static str
     where
         Self: Sized;
-    fn is_registered() -> bool
+    fn build(value: serde_json::Value) -> Result<Box<dyn Action>>
     where
         Self: Sized;
-
-    fn partial_eq(&self, action: &dyn Action) -> bool;
-    fn boxed_clone(&self) -> Box<dyn Action>;
-    fn as_any(&self) -> &dyn Any;
 }
 
-// Types become actions by satisfying a list of trait bounds.
-impl<A> Action for A
-where
-    A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + std::fmt::Debug + 'static,
-{
-    fn qualified_name() -> SharedString {
-        let name = type_name::<A>();
-        let mut separator_matches = name.rmatch_indices("::");
-        separator_matches.next().unwrap();
-        let name_start_ix = separator_matches.next().map_or(0, |(ix, _)| ix + 2);
-        // todo!() remove the 2 replacement when migration is done
-        name[name_start_ix..].replace("2::", "::").into()
-    }
-
-    fn build(params: Option<serde_json::Value>) -> Result<Box<dyn Action>>
-    where
-        Self: Sized,
-    {
-        let action = if let Some(params) = params {
-            serde_json::from_value(params).context("failed to deserialize action")?
-        } else {
-            Self::default()
-        };
-        Ok(Box::new(action))
-    }
-
-    fn is_registered() -> bool {
-        ACTION_REGISTRY
-            .read()
-            .names_by_type_id
-            .get(&TypeId::of::<A>())
-            .is_some()
-    }
-
-    fn partial_eq(&self, action: &dyn Action) -> bool {
-        action
-            .as_any()
-            .downcast_ref::<Self>()
-            .map_or(false, |a| self == a)
-    }
-
-    fn boxed_clone(&self) -> Box<dyn Action> {
-        Box::new(self.clone())
-    }
-
-    fn as_any(&self) -> &dyn Any {
-        self
+impl std::fmt::Debug for dyn Action {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("dyn Action")
+            .field("type_name", &self.name())
+            .finish()
     }
 }
 
@@ -119,69 +65,93 @@ impl dyn Action {
     pub fn type_id(&self) -> TypeId {
         self.as_any().type_id()
     }
-
-    pub fn name(&self) -> SharedString {
-        ACTION_REGISTRY
-            .read()
-            .names_by_type_id
-            .get(&self.type_id())
-            .expect("type is not a registered action")
-            .clone()
-    }
 }
 
-type ActionBuilder = fn(json: Option<serde_json::Value>) -> anyhow::Result<Box<dyn Action>>;
-
-lazy_static! {
-    static ref ACTION_REGISTRY: RwLock<ActionRegistry> = RwLock::default();
-}
+type ActionBuilder = fn(json: serde_json::Value) -> anyhow::Result<Box<dyn Action>>;
 
-#[derive(Default)]
-struct ActionRegistry {
+pub(crate) struct ActionRegistry {
     builders_by_name: HashMap<SharedString, ActionBuilder>,
     names_by_type_id: HashMap<TypeId, SharedString>,
     all_names: Vec<SharedString>, // So we can return a static slice.
 }
 
-/// Register an action type to allow it to be referenced in keymaps.
-pub fn register_action<A: Action>() {
-    let name = A::qualified_name();
-    let mut lock = ACTION_REGISTRY.write();
-    lock.builders_by_name.insert(name.clone(), A::build);
-    lock.names_by_type_id
-        .insert(TypeId::of::<A>(), name.clone());
-    lock.all_names.push(name);
+impl Default for ActionRegistry {
+    fn default() -> Self {
+        let mut this = ActionRegistry {
+            builders_by_name: Default::default(),
+            names_by_type_id: Default::default(),
+            all_names: Default::default(),
+        };
+
+        this.load_actions();
+
+        this
+    }
 }
 
-/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
-pub fn build_action_from_type(type_id: &TypeId) -> Result<Box<dyn Action>> {
-    let lock = ACTION_REGISTRY.read();
-    let name = lock
-        .names_by_type_id
-        .get(type_id)
-        .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))?
-        .clone();
-    drop(lock);
-
-    build_action(&name, None)
+/// This type must be public so that our macros can build it in other crates.
+/// But this is an implementation detail and should not be used directly.
+#[doc(hidden)]
+pub type MacroActionBuilder = fn() -> ActionData;
+
+/// This type must be public so that our macros can build it in other crates.
+/// But this is an implementation detail and should not be used directly.
+#[doc(hidden)]
+pub struct ActionData {
+    pub name: &'static str,
+    pub type_id: TypeId,
+    pub build: ActionBuilder,
 }
 
-/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
-pub fn build_action(name: &str, params: Option<serde_json::Value>) -> Result<Box<dyn Action>> {
-    let lock = ACTION_REGISTRY.read();
+/// This constant must be public to be accessible from other crates.
+/// But it's existence is an implementation detail and should not be used directly.
+#[doc(hidden)]
+#[linkme::distributed_slice]
+pub static __GPUI_ACTIONS: [MacroActionBuilder];
+
+impl ActionRegistry {
+    /// Load all registered actions into the registry.
+    pub(crate) fn load_actions(&mut self) {
+        for builder in __GPUI_ACTIONS {
+            let action = builder();
+            //todo(remove)
+            let name: SharedString = remove_the_2(action.name).into();
+            self.builders_by_name.insert(name.clone(), action.build);
+            self.names_by_type_id.insert(action.type_id, name.clone());
+            self.all_names.push(name);
+        }
+    }
+
+    /// Construct an action based on its name and optional JSON parameters sourced from the keymap.
+    pub fn build_action_type(&self, type_id: &TypeId) -> Result<Box<dyn Action>> {
+        let name = self
+            .names_by_type_id
+            .get(type_id)
+            .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))?
+            .clone();
 
-    let build_action = lock
-        .builders_by_name
-        .get(name)
-        .ok_or_else(|| anyhow!("no action type registered for {}", name))?;
-    (build_action)(params)
-}
+        self.build_action(&name, None)
+    }
+
+    /// Construct an action based on its name and optional JSON parameters sourced from the keymap.
+    pub fn build_action(
+        &self,
+        name: &str,
+        params: Option<serde_json::Value>,
+    ) -> Result<Box<dyn Action>> {
+        //todo(remove)
+        let name = remove_the_2(name);
+        let build_action = self
+            .builders_by_name
+            .get(name.deref())
+            .ok_or_else(|| anyhow!("no action type registered for {}", name))?;
+        (build_action)(params.unwrap_or_else(|| json!({})))
+            .with_context(|| format!("Attempting to build action {}", name))
+    }
 
-pub fn all_action_names() -> MappedRwLockReadGuard<'static, [SharedString]> {
-    let lock = ACTION_REGISTRY.read();
-    RwLockReadGuard::map(lock, |registry: &ActionRegistry| {
-        registry.all_names.as_slice()
-    })
+    pub fn all_action_names(&self) -> &[SharedString] {
+        self.all_names.as_slice()
+    }
 }
 
 /// Defines unit structs that can be used as actions.
@@ -191,7 +161,7 @@ macro_rules! actions {
     () => {};
 
     ( $name:ident ) => {
-        #[gpui::action]
+        #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize, gpui::Action)]
         pub struct $name;
     };
 
@@ -200,3 +170,20 @@ macro_rules! actions {
         actions!($($rest)*);
     };
 }
+
+//todo!(remove)
+pub fn remove_the_2(action_name: &str) -> String {
+    let mut separator_matches = action_name.rmatch_indices("::");
+    separator_matches.next().unwrap();
+    let name_start_ix = separator_matches.next().map_or(0, |(ix, _)| ix + 2);
+    // todo!() remove the 2 replacement when migration is done
+    action_name[name_start_ix..]
+        .replace("2::", "::")
+        .to_string()
+}
+
+mod no_action {
+    use crate as gpui;
+
+    actions!(NoAction);
+}

crates/gpui2/src/app.rs 🔗

@@ -14,12 +14,13 @@ use smallvec::SmallVec;
 pub use test_context::*;
 
 use crate::{
-    current_platform, image_cache::ImageCache, Action, AnyBox, AnyView, AnyWindowHandle,
-    AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context, DispatchPhase, DisplayId,
-    Entity, EventEmitter, FocusEvent, FocusHandle, FocusId, ForegroundExecutor, KeyBinding, Keymap,
-    LayoutId, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point, Render, SubscriberSet,
-    Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, ViewContext,
-    Window, WindowContext, WindowHandle, WindowId,
+    current_platform, image_cache::ImageCache, Action, ActionRegistry, AnyBox, AnyView,
+    AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
+    DispatchPhase, DisplayId, Entity, EventEmitter, FocusEvent, FocusHandle, FocusId,
+    ForegroundExecutor, KeyBinding, Keymap, LayoutId, PathPromptOptions, Pixels, Platform,
+    PlatformDisplay, Point, Render, SharedString, SubscriberSet, Subscription, SvgRenderer, Task,
+    TextStyle, TextStyleRefinement, TextSystem, View, ViewContext, Window, WindowContext,
+    WindowHandle, WindowId,
 };
 use anyhow::{anyhow, Result};
 use collections::{HashMap, HashSet, VecDeque};
@@ -182,6 +183,7 @@ pub struct AppContext {
     text_system: Arc<TextSystem>,
     flushing_effects: bool,
     pending_updates: usize,
+    pub(crate) actions: Rc<ActionRegistry>,
     pub(crate) active_drag: Option<AnyDrag>,
     pub(crate) active_tooltip: Option<AnyTooltip>,
     pub(crate) next_frame_callbacks: HashMap<DisplayId, Vec<FrameCallback>>,
@@ -240,6 +242,7 @@ impl AppContext {
                 platform: platform.clone(),
                 app_metadata,
                 text_system,
+                actions: Rc::new(ActionRegistry::default()),
                 flushing_effects: false,
                 pending_updates: 0,
                 active_drag: None,
@@ -964,6 +967,18 @@ impl AppContext {
     pub fn propagate(&mut self) {
         self.propagate_event = true;
     }
+
+    pub fn build_action(
+        &self,
+        name: &str,
+        data: Option<serde_json::Value>,
+    ) -> Result<Box<dyn Action>> {
+        self.actions.build_action(name, data)
+    }
+
+    pub fn all_action_names(&self) -> &[SharedString] {
+        self.actions.all_action_names()
+    }
 }
 
 impl Context for AppContext {

crates/gpui2/src/app/async_context.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
-    AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, ForegroundExecutor,
-    Model, ModelContext, Render, Result, Task, View, ViewContext, VisualContext, WindowContext,
-    WindowHandle,
+    AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, FocusableView,
+    ForegroundExecutor, Model, ModelContext, Render, Result, Task, View, ViewContext,
+    VisualContext, WindowContext, WindowHandle,
 };
 use anyhow::{anyhow, Context as _};
 use derive_more::{Deref, DerefMut};
@@ -182,6 +182,10 @@ pub struct AsyncWindowContext {
 }
 
 impl AsyncWindowContext {
+    pub fn window_handle(&self) -> AnyWindowHandle {
+        self.window
+    }
+
     pub(crate) fn new(app: AsyncAppContext, window: AnyWindowHandle) -> Self {
         Self { app, window }
     }
@@ -307,4 +311,13 @@ impl VisualContext for AsyncWindowContext {
         self.window
             .update(self, |_, cx| cx.replace_root_view(build_view))
     }
+
+    fn focus_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
+    where
+        V: FocusableView,
+    {
+        self.window.update(self, |_, cx| {
+            view.read(cx).focus_handle(cx).clone().focus(cx);
+        })
+    }
 }

crates/gpui2/src/app/entity_map.rs 🔗

@@ -68,6 +68,7 @@ impl EntityMap {
     }
 
     /// Move an entity to the stack.
+    #[track_caller]
     pub fn lease<'a, T>(&mut self, model: &'a Model<T>) -> Lease<'a, T> {
         self.assert_valid_context(model);
         let entity = Some(

crates/gpui2/src/app/test_context.rs 🔗

@@ -370,10 +370,19 @@ impl<T: Send> Model<T> {
             })
         });
 
-        cx.executor().run_until_parked();
-        rx.try_next()
-            .expect("no event received")
-            .expect("model was dropped")
+        // Run other tasks until the event is emitted.
+        loop {
+            match rx.try_next() {
+                Ok(Some(event)) => return event,
+                Ok(None) => panic!("model was dropped"),
+                Err(_) => {
+                    if !cx.executor().tick() {
+                        break;
+                    }
+                }
+            }
+        }
+        panic!("no event received")
     }
 }
 
@@ -588,6 +597,14 @@ impl<'a> VisualContext for VisualTestContext<'a> {
             .update(self.cx, |_, cx| cx.replace_root_view(build_view))
             .unwrap()
     }
+
+    fn focus_view<V: crate::FocusableView>(&mut self, view: &View<V>) -> Self::Result<()> {
+        self.window
+            .update(self.cx, |_, cx| {
+                view.read(cx).focus_handle(cx).clone().focus(cx)
+            })
+            .unwrap()
+    }
 }
 
 impl AnyWindowHandle {

crates/gpui2/src/element.rs 🔗

@@ -10,21 +10,12 @@ pub trait Element<V: 'static> {
 
     fn element_id(&self) -> Option<ElementId>;
 
-    /// Called to initialize this element for the current frame. If this
-    /// element had state in a previous frame, it will be passed in for the 3rd argument.
-    fn initialize(
-        &mut self,
-        view_state: &mut V,
-        element_state: Option<Self::ElementState>,
-        cx: &mut ViewContext<V>,
-    ) -> Self::ElementState;
-
     fn layout(
         &mut self,
         view_state: &mut V,
-        element_state: &mut Self::ElementState,
+        element_state: Option<Self::ElementState>,
         cx: &mut ViewContext<V>,
-    ) -> LayoutId;
+    ) -> (LayoutId, Self::ElementState);
 
     fn paint(
         &mut self,
@@ -97,7 +88,6 @@ pub trait ParentComponent<V: 'static> {
 
 trait ElementObject<V> {
     fn element_id(&self) -> Option<ElementId>;
-    fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>);
     fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId;
     fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>);
     fn measure(
@@ -124,9 +114,6 @@ struct RenderedElement<V: 'static, E: Element<V>> {
 enum ElementRenderPhase<V> {
     #[default]
     Start,
-    Initialized {
-        frame_state: Option<V>,
-    },
     LayoutRequested {
         layout_id: LayoutId,
         frame_state: Option<V>,
@@ -162,42 +149,19 @@ where
         self.element.element_id()
     }
 
-    fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
-        let frame_state = if let Some(id) = self.element.element_id() {
-            cx.with_element_state(id, |element_state, cx| {
-                let element_state = self.element.initialize(view_state, element_state, cx);
-                ((), element_state)
-            });
-            None
-        } else {
-            let frame_state = self.element.initialize(view_state, None, cx);
-            Some(frame_state)
-        };
-
-        self.phase = ElementRenderPhase::Initialized { frame_state };
-    }
-
     fn layout(&mut self, state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
-        let layout_id;
-        let mut frame_state;
-        match mem::take(&mut self.phase) {
-            ElementRenderPhase::Initialized {
-                frame_state: initial_frame_state,
-            } => {
-                frame_state = initial_frame_state;
+        let (layout_id, frame_state) = match mem::take(&mut self.phase) {
+            ElementRenderPhase::Start => {
                 if let Some(id) = self.element.element_id() {
-                    layout_id = cx.with_element_state(id, |element_state, cx| {
-                        let mut element_state = element_state.unwrap();
-                        let layout_id = self.element.layout(state, &mut element_state, cx);
-                        (layout_id, element_state)
+                    let layout_id = cx.with_element_state(id, |element_state, cx| {
+                        self.element.layout(state, element_state, cx)
                     });
+                    (layout_id, None)
                 } else {
-                    layout_id = self
-                        .element
-                        .layout(state, frame_state.as_mut().unwrap(), cx);
+                    let (layout_id, frame_state) = self.element.layout(state, None, cx);
+                    (layout_id, Some(frame_state))
                 }
             }
-            ElementRenderPhase::Start => panic!("must call initialize before layout"),
             ElementRenderPhase::LayoutRequested { .. }
             | ElementRenderPhase::LayoutComputed { .. }
             | ElementRenderPhase::Painted { .. } => {
@@ -249,10 +213,6 @@ where
         cx: &mut ViewContext<V>,
     ) -> Size<Pixels> {
         if matches!(&self.phase, ElementRenderPhase::Start) {
-            self.initialize(view_state, cx);
-        }
-
-        if matches!(&self.phase, ElementRenderPhase::Initialized { .. }) {
             self.layout(view_state, cx);
         }
 
@@ -289,16 +249,13 @@ where
 
     fn draw(
         &mut self,
-        mut origin: Point<Pixels>,
+        origin: Point<Pixels>,
         available_space: Size<AvailableSpace>,
         view_state: &mut V,
         cx: &mut ViewContext<V>,
     ) {
         self.measure(available_space, view_state, cx);
-        // Ignore the element offset when drawing this element, as the origin is already specified
-        // in absolute terms.
-        origin -= cx.element_offset();
-        cx.with_element_offset(origin, |cx| self.paint(view_state, cx))
+        cx.with_absolute_element_offset(origin, |cx| self.paint(view_state, cx))
     }
 }
 
@@ -318,10 +275,6 @@ impl<V> AnyElement<V> {
         self.0.element_id()
     }
 
-    pub fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
-        self.0.initialize(view_state, cx);
-    }
-
     pub fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
         self.0.layout(view_state, cx)
     }
@@ -402,25 +355,16 @@ where
         None
     }
 
-    fn initialize(
+    fn layout(
         &mut self,
         view_state: &mut V,
-        _rendered_element: Option<Self::ElementState>,
+        _: Option<Self::ElementState>,
         cx: &mut ViewContext<V>,
-    ) -> Self::ElementState {
+    ) -> (LayoutId, Self::ElementState) {
         let render = self.take().unwrap();
         let mut rendered_element = (render)(view_state, cx).render();
-        rendered_element.initialize(view_state, cx);
-        rendered_element
-    }
-
-    fn layout(
-        &mut self,
-        view_state: &mut V,
-        rendered_element: &mut Self::ElementState,
-        cx: &mut ViewContext<V>,
-    ) -> LayoutId {
-        rendered_element.layout(view_state, cx)
+        let layout_id = rendered_element.layout(view_state, cx);
+        (layout_id, rendered_element)
     }
 
     fn paint(

crates/gpui2/src/elements/div.rs 🔗

@@ -22,7 +22,6 @@ use util::ResultExt;
 
 const DRAG_THRESHOLD: f64 = 2.;
 const TOOLTIP_DELAY: Duration = Duration::from_millis(500);
-const TOOLTIP_OFFSET: Point<Pixels> = Point::new(px(10.0), px(8.0));
 
 pub struct GroupStyle {
     pub group: SharedString,
@@ -238,11 +237,11 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
         //
         // if we are relying on this side-effect still, removing the debug_assert!
         // likely breaks the command_palette tests.
-        debug_assert!(
-            A::is_registered(),
-            "{:?} is not registered as an action",
-            A::qualified_name()
-        );
+        // debug_assert!(
+        //     A::is_registered(),
+        //     "{:?} is not registered as an action",
+        //     A::qualified_name()
+        // );
         self.interactivity().action_listeners.push((
             TypeId::of::<A>(),
             Box::new(move |view, action, phase, cx| {
@@ -408,21 +407,19 @@ pub trait StatefulInteractiveComponent<V: 'static, E: Element<V>>: InteractiveCo
         self
     }
 
-    fn tooltip<W>(
+    fn tooltip(
         mut self,
-        build_tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> View<W> + 'static,
+        build_tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static,
     ) -> Self
     where
         Self: Sized,
-        W: 'static + Render,
     {
         debug_assert!(
             self.interactivity().tooltip_builder.is_none(),
             "calling tooltip more than once on the same element is not supported"
         );
-        self.interactivity().tooltip_builder = Some(Rc::new(move |view_state, cx| {
-            build_tooltip(view_state, cx).into()
-        }));
+        self.interactivity().tooltip_builder =
+            Some(Rc::new(move |view_state, cx| build_tooltip(view_state, cx)));
 
         self
     }
@@ -437,14 +434,6 @@ pub trait FocusableComponent<V: 'static>: InteractiveComponent<V> {
         self
     }
 
-    fn focus_in(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
-    where
-        Self: Sized,
-    {
-        self.interactivity().focus_in_style = f(StyleRefinement::default());
-        self
-    }
-
     fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
     where
         Self: Sized,
@@ -617,46 +606,36 @@ impl<V: 'static> Element<V> for Div<V> {
         self.interactivity.element_id.clone()
     }
 
-    fn initialize(
-        &mut self,
-        view_state: &mut V,
-        element_state: Option<Self::ElementState>,
-        cx: &mut ViewContext<V>,
-    ) -> Self::ElementState {
-        let interactive_state = self
-            .interactivity
-            .initialize(element_state.map(|s| s.interactive_state), cx);
-
-        for child in &mut self.children {
-            child.initialize(view_state, cx);
-        }
-
-        DivState {
-            interactive_state,
-            child_layout_ids: SmallVec::new(),
-        }
-    }
-
     fn layout(
         &mut self,
         view_state: &mut V,
-        element_state: &mut Self::ElementState,
+        element_state: Option<Self::ElementState>,
         cx: &mut ViewContext<V>,
-    ) -> crate::LayoutId {
+    ) -> (LayoutId, Self::ElementState) {
+        let mut child_layout_ids = SmallVec::new();
         let mut interactivity = mem::take(&mut self.interactivity);
-        let layout_id =
-            interactivity.layout(&mut element_state.interactive_state, cx, |style, cx| {
+        let (layout_id, interactive_state) = interactivity.layout(
+            element_state.map(|s| s.interactive_state),
+            cx,
+            |style, cx| {
                 cx.with_text_style(style.text_style().cloned(), |cx| {
-                    element_state.child_layout_ids = self
+                    child_layout_ids = self
                         .children
                         .iter_mut()
                         .map(|child| child.layout(view_state, cx))
                         .collect::<SmallVec<_>>();
-                    cx.request_layout(&style, element_state.child_layout_ids.iter().copied())
+                    cx.request_layout(&style, child_layout_ids.iter().copied())
                 })
-            });
+            },
+        );
         self.interactivity = interactivity;
-        layout_id
+        (
+            layout_id,
+            DivState {
+                interactive_state,
+                child_layout_ids,
+            },
+        )
     }
 
     fn paint(
@@ -740,7 +719,6 @@ pub struct Interactivity<V> {
     pub group: Option<SharedString>,
     pub base_style: StyleRefinement,
     pub focus_style: StyleRefinement,
-    pub focus_in_style: StyleRefinement,
     pub in_focus_style: StyleRefinement,
     pub hover_style: StyleRefinement,
     pub group_hover_style: Option<GroupStyle>,
@@ -766,11 +744,12 @@ impl<V> Interactivity<V>
 where
     V: 'static,
 {
-    pub fn initialize(
+    pub fn layout(
         &mut self,
         element_state: Option<InteractiveElementState>,
         cx: &mut ViewContext<V>,
-    ) -> InteractiveElementState {
+        f: impl FnOnce(Style, &mut ViewContext<V>) -> LayoutId,
+    ) -> (LayoutId, InteractiveElementState) {
         let mut element_state = element_state.unwrap_or_default();
 
         // Ensure we store a focus handle in our element state if we're focusable.
@@ -785,17 +764,9 @@ where
             });
         }
 
-        element_state
-    }
-
-    pub fn layout(
-        &mut self,
-        element_state: &mut InteractiveElementState,
-        cx: &mut ViewContext<V>,
-        f: impl FnOnce(Style, &mut ViewContext<V>) -> LayoutId,
-    ) -> LayoutId {
-        let style = self.compute_style(None, element_state, cx);
-        f(style, cx)
+        let style = self.compute_style(None, &mut element_state, cx);
+        let layout_id = f(style, cx);
+        (layout_id, element_state)
     }
 
     pub fn paint(
@@ -989,11 +960,11 @@ where
                             cx.background_executor().timer(TOOLTIP_DELAY).await;
                             view.update(&mut cx, move |view_state, cx| {
                                 active_tooltip.borrow_mut().replace(ActiveTooltip {
-                                    waiting: None,
                                     tooltip: Some(AnyTooltip {
                                         view: tooltip_builder(view_state, cx),
-                                        cursor_offset: cx.mouse_position() + TOOLTIP_OFFSET,
+                                        cursor_offset: cx.mouse_position(),
                                     }),
+                                    _task: None,
                                 });
                                 cx.notify();
                             })
@@ -1001,12 +972,17 @@ where
                         }
                     });
                     active_tooltip.borrow_mut().replace(ActiveTooltip {
-                        waiting: Some(task),
                         tooltip: None,
+                        _task: Some(task),
                     });
                 }
             });
 
+            let active_tooltip = element_state.active_tooltip.clone();
+            cx.on_mouse_event(move |_, _: &MouseDownEvent, _, _| {
+                active_tooltip.borrow_mut().take();
+            });
+
             if let Some(active_tooltip) = element_state.active_tooltip.borrow().as_ref() {
                 if active_tooltip.tooltip.is_some() {
                     cx.active_tooltip = active_tooltip.tooltip.clone()
@@ -1130,10 +1106,6 @@ where
         style.refine(&self.base_style);
 
         if let Some(focus_handle) = self.tracked_focus_handle.as_ref() {
-            if focus_handle.contains_focused(cx) {
-                style.refine(&self.focus_in_style);
-            }
-
             if focus_handle.within_focused(cx) {
                 style.refine(&self.in_focus_style);
             }
@@ -1206,7 +1178,6 @@ impl<V: 'static> Default for Interactivity<V> {
             group: None,
             base_style: StyleRefinement::default(),
             focus_style: StyleRefinement::default(),
-            focus_in_style: StyleRefinement::default(),
             in_focus_style: StyleRefinement::default(),
             hover_style: StyleRefinement::default(),
             group_hover_style: None,
@@ -1241,9 +1212,8 @@ pub struct InteractiveElementState {
 }
 
 pub struct ActiveTooltip {
-    #[allow(unused)] // used to drop the task
-    waiting: Option<Task<()>>,
     tooltip: Option<AnyTooltip>,
+    _task: Option<Task<()>>,
 }
 
 /// Whether or not the element or a group that contains it is clicked by the mouse.
@@ -1327,21 +1297,12 @@ where
         self.element.element_id()
     }
 
-    fn initialize(
-        &mut self,
-        view_state: &mut V,
-        element_state: Option<Self::ElementState>,
-        cx: &mut ViewContext<V>,
-    ) -> Self::ElementState {
-        self.element.initialize(view_state, element_state, cx)
-    }
-
     fn layout(
         &mut self,
         view_state: &mut V,
-        element_state: &mut Self::ElementState,
+        element_state: Option<Self::ElementState>,
         cx: &mut ViewContext<V>,
-    ) -> LayoutId {
+    ) -> (LayoutId, Self::ElementState) {
         self.element.layout(view_state, element_state, cx)
     }
 
@@ -1422,21 +1383,12 @@ where
         self.element.element_id()
     }
 
-    fn initialize(
-        &mut self,
-        view_state: &mut V,
-        element_state: Option<Self::ElementState>,
-        cx: &mut ViewContext<V>,
-    ) -> Self::ElementState {
-        self.element.initialize(view_state, element_state, cx)
-    }
-
     fn layout(
         &mut self,
         view_state: &mut V,
-        element_state: &mut Self::ElementState,
+        element_state: Option<Self::ElementState>,
         cx: &mut ViewContext<V>,
-    ) -> LayoutId {
+    ) -> (LayoutId, Self::ElementState) {
         self.element.layout(view_state, element_state, cx)
     }
 

crates/gpui2/src/elements/img.rs 🔗

@@ -48,21 +48,12 @@ impl<V> Element<V> for Img<V> {
         self.interactivity.element_id.clone()
     }
 
-    fn initialize(
-        &mut self,
-        _view_state: &mut V,
-        element_state: Option<Self::ElementState>,
-        cx: &mut ViewContext<V>,
-    ) -> Self::ElementState {
-        self.interactivity.initialize(element_state, cx)
-    }
-
     fn layout(
         &mut self,
         _view_state: &mut V,
-        element_state: &mut Self::ElementState,
+        element_state: Option<Self::ElementState>,
         cx: &mut ViewContext<V>,
-    ) -> LayoutId {
+    ) -> (LayoutId, Self::ElementState) {
         self.interactivity.layout(element_state, cx, |style, cx| {
             cx.request_layout(&style, None)
         })

crates/gpui2/src/elements/mod.rs 🔗

@@ -1,11 +1,13 @@
 mod div;
 mod img;
+mod overlay;
 mod svg;
 mod text;
 mod uniform_list;
 
 pub use div::*;
 pub use img::*;
+pub use overlay::*;
 pub use svg::*;
 pub use text::*;
 pub use uniform_list::*;

crates/gpui2/src/elements/overlay.rs 🔗

@@ -0,0 +1,232 @@
+use smallvec::SmallVec;
+use taffy::style::{Display, Position};
+
+use crate::{
+    point, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, ParentComponent, Pixels,
+    Point, Size, Style,
+};
+
+pub struct OverlayState {
+    child_layout_ids: SmallVec<[LayoutId; 4]>,
+}
+
+pub struct Overlay<V> {
+    children: SmallVec<[AnyElement<V>; 2]>,
+    anchor_corner: AnchorCorner,
+    fit_mode: OverlayFitMode,
+    // todo!();
+    anchor_position: Option<Point<Pixels>>,
+    // position_mode: OverlayPositionMode,
+}
+
+/// overlay gives you a floating element that will avoid overflowing the window bounds.
+/// Its children should have no margin to avoid measurement issues.
+pub fn overlay<V: 'static>() -> Overlay<V> {
+    Overlay {
+        children: SmallVec::new(),
+        anchor_corner: AnchorCorner::TopLeft,
+        fit_mode: OverlayFitMode::SwitchAnchor,
+        anchor_position: None,
+    }
+}
+
+impl<V> Overlay<V> {
+    /// Sets which corner of the overlay should be anchored to the current position.
+    pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
+        self.anchor_corner = anchor;
+        self
+    }
+
+    /// Sets the position in window co-ordinates
+    /// (otherwise the location the overlay is rendered is used)
+    pub fn position(mut self, anchor: Point<Pixels>) -> Self {
+        self.anchor_position = Some(anchor);
+        self
+    }
+
+    /// Snap to window edge instead of switching anchor corner when an overflow would occur.
+    pub fn snap_to_window(mut self) -> Self {
+        self.fit_mode = OverlayFitMode::SnapToWindow;
+        self
+    }
+}
+
+impl<V: 'static> ParentComponent<V> for Overlay<V> {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
+        &mut self.children
+    }
+}
+
+impl<V: 'static> Component<V> for Overlay<V> {
+    fn render(self) -> AnyElement<V> {
+        AnyElement::new(self)
+    }
+}
+
+impl<V: 'static> Element<V> for Overlay<V> {
+    type ElementState = OverlayState;
+
+    fn element_id(&self) -> Option<crate::ElementId> {
+        None
+    }
+
+    fn layout(
+        &mut self,
+        view_state: &mut V,
+        _: Option<Self::ElementState>,
+        cx: &mut crate::ViewContext<V>,
+    ) -> (crate::LayoutId, Self::ElementState) {
+        let child_layout_ids = self
+            .children
+            .iter_mut()
+            .map(|child| child.layout(view_state, cx))
+            .collect::<SmallVec<_>>();
+
+        let mut overlay_style = Style::default();
+        overlay_style.position = Position::Absolute;
+        overlay_style.display = Display::Flex;
+
+        let layout_id = cx.request_layout(&overlay_style, child_layout_ids.iter().copied());
+
+        (layout_id, OverlayState { child_layout_ids })
+    }
+
+    fn paint(
+        &mut self,
+        bounds: crate::Bounds<crate::Pixels>,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut crate::ViewContext<V>,
+    ) {
+        if element_state.child_layout_ids.is_empty() {
+            return;
+        }
+
+        let mut child_min = point(Pixels::MAX, Pixels::MAX);
+        let mut child_max = Point::default();
+        for child_layout_id in &element_state.child_layout_ids {
+            let child_bounds = cx.layout_bounds(*child_layout_id);
+            child_min = child_min.min(&child_bounds.origin);
+            child_max = child_max.max(&child_bounds.lower_right());
+        }
+        let size: Size<Pixels> = (child_max - child_min).into();
+        let origin = self.anchor_position.unwrap_or(bounds.origin);
+
+        let mut desired = self.anchor_corner.get_bounds(origin, size);
+        let limits = Bounds {
+            origin: Point::zero(),
+            size: cx.viewport_size(),
+        };
+
+        match self.fit_mode {
+            OverlayFitMode::SnapToWindow => {
+                // Snap the horizontal edges of the overlay to the horizontal edges of the window if
+                // its horizontal bounds overflow
+                if desired.right() > limits.right() {
+                    desired.origin.x -= desired.right() - limits.right();
+                } else if desired.left() < limits.left() {
+                    desired.origin.x = limits.origin.x;
+                }
+
+                // Snap the vertical edges of the overlay to the vertical edges of the window if
+                // its vertical bounds overflow.
+                if desired.bottom() > limits.bottom() {
+                    desired.origin.y -= desired.bottom() - limits.bottom();
+                } else if desired.top() < limits.top() {
+                    desired.origin.y = limits.origin.y;
+                }
+            }
+            OverlayFitMode::SwitchAnchor => {
+                let mut anchor_corner = self.anchor_corner;
+
+                if desired.left() < limits.left() || desired.right() > limits.right() {
+                    anchor_corner = anchor_corner.switch_axis(Axis::Horizontal);
+                }
+
+                if bounds.top() < limits.top() || bounds.bottom() > limits.bottom() {
+                    anchor_corner = anchor_corner.switch_axis(Axis::Vertical);
+                }
+
+                // Update bounds if needed
+                if anchor_corner != self.anchor_corner {
+                    desired = anchor_corner.get_bounds(origin, size)
+                }
+            }
+            OverlayFitMode::None => {}
+        }
+
+        cx.with_element_offset(desired.origin - bounds.origin, |cx| {
+            for child in &mut self.children {
+                child.paint(view_state, cx);
+            }
+        })
+    }
+}
+
+enum Axis {
+    Horizontal,
+    Vertical,
+}
+
+#[derive(Copy, Clone)]
+pub enum OverlayFitMode {
+    SnapToWindow,
+    SwitchAnchor,
+    None,
+}
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum AnchorCorner {
+    TopLeft,
+    TopRight,
+    BottomLeft,
+    BottomRight,
+}
+
+impl AnchorCorner {
+    fn get_bounds(&self, origin: Point<Pixels>, size: Size<Pixels>) -> Bounds<Pixels> {
+        let origin = match self {
+            Self::TopLeft => origin,
+            Self::TopRight => Point {
+                x: origin.x - size.width,
+                y: origin.y,
+            },
+            Self::BottomLeft => Point {
+                x: origin.x,
+                y: origin.y - size.height,
+            },
+            Self::BottomRight => Point {
+                x: origin.x - size.width,
+                y: origin.y - size.height,
+            },
+        };
+
+        Bounds { origin, size }
+    }
+
+    pub fn corner(&self, bounds: Bounds<Pixels>) -> Point<Pixels> {
+        match self {
+            Self::TopLeft => bounds.origin,
+            Self::TopRight => bounds.upper_right(),
+            Self::BottomLeft => bounds.lower_left(),
+            Self::BottomRight => bounds.lower_right(),
+        }
+    }
+
+    fn switch_axis(self, axis: Axis) -> Self {
+        match axis {
+            Axis::Vertical => match self {
+                AnchorCorner::TopLeft => AnchorCorner::BottomLeft,
+                AnchorCorner::TopRight => AnchorCorner::BottomRight,
+                AnchorCorner::BottomLeft => AnchorCorner::TopLeft,
+                AnchorCorner::BottomRight => AnchorCorner::TopRight,
+            },
+            Axis::Horizontal => match self {
+                AnchorCorner::TopLeft => AnchorCorner::TopRight,
+                AnchorCorner::TopRight => AnchorCorner::TopLeft,
+                AnchorCorner::BottomLeft => AnchorCorner::BottomRight,
+                AnchorCorner::BottomRight => AnchorCorner::BottomLeft,
+            },
+        }
+    }
+}

crates/gpui2/src/elements/svg.rs 🔗

@@ -37,21 +37,12 @@ impl<V> Element<V> for Svg<V> {
         self.interactivity.element_id.clone()
     }
 
-    fn initialize(
-        &mut self,
-        _view_state: &mut V,
-        element_state: Option<Self::ElementState>,
-        cx: &mut ViewContext<V>,
-    ) -> Self::ElementState {
-        self.interactivity.initialize(element_state, cx)
-    }
-
     fn layout(
         &mut self,
         _view_state: &mut V,
-        element_state: &mut Self::ElementState,
+        element_state: Option<Self::ElementState>,
         cx: &mut ViewContext<V>,
-    ) -> LayoutId {
+    ) -> (LayoutId, Self::ElementState) {
         self.interactivity.layout(element_state, cx, |style, cx| {
             cx.request_layout(&style, None)
         })

crates/gpui2/src/elements/text.rs 🔗

@@ -1,96 +1,51 @@
 use crate::{
-    AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, Line, Pixels, SharedString,
-    Size, TextRun, ViewContext,
+    AnyElement, BorrowWindow, Bounds, Component, Element, ElementId, LayoutId, Pixels,
+    SharedString, Size, TextRun, ViewContext, WrappedLine,
 };
-use parking_lot::Mutex;
+use parking_lot::{Mutex, MutexGuard};
 use smallvec::SmallVec;
-use std::{marker::PhantomData, sync::Arc};
+use std::{cell::Cell, rc::Rc, sync::Arc};
 use util::ResultExt;
 
-impl<V: 'static> Component<V> for SharedString {
-    fn render(self) -> AnyElement<V> {
-        Text {
-            text: self,
-            runs: None,
-            state_type: PhantomData,
-        }
-        .render()
-    }
-}
-
-impl<V: 'static> Component<V> for &'static str {
-    fn render(self) -> AnyElement<V> {
-        Text {
-            text: self.into(),
-            runs: None,
-            state_type: PhantomData,
-        }
-        .render()
-    }
-}
-
-// TODO: Figure out how to pass `String` to `child` without this.
-// This impl doesn't exist in the `gpui2` crate.
-impl<V: 'static> Component<V> for String {
-    fn render(self) -> AnyElement<V> {
-        Text {
-            text: self.into(),
-            runs: None,
-            state_type: PhantomData,
-        }
-        .render()
-    }
-}
-
-pub struct Text<V> {
+pub struct Text {
     text: SharedString,
     runs: Option<Vec<TextRun>>,
-    state_type: PhantomData<V>,
 }
 
-impl<V: 'static> Text<V> {
-    /// styled renders text that has different runs of different styles.
-    /// callers are responsible for setting the correct style for each run.
-    ////
-    /// For uniform text you can usually just pass a string as a child, and
-    /// cx.text_style() will be used automatically.
+impl Text {
+    /// Renders text with runs of different styles.
+    ///
+    /// Callers are responsible for setting the correct style for each run.
+    /// For text with a uniform style, you can usually avoid calling this constructor
+    /// and just pass text directly.
     pub fn styled(text: SharedString, runs: Vec<TextRun>) -> Self {
         Text {
             text,
             runs: Some(runs),
-            state_type: Default::default(),
         }
     }
 }
 
-impl<V: 'static> Component<V> for Text<V> {
+impl<V: 'static> Component<V> for Text {
     fn render(self) -> AnyElement<V> {
         AnyElement::new(self)
     }
 }
 
-impl<V: 'static> Element<V> for Text<V> {
-    type ElementState = Arc<Mutex<Option<TextElementState>>>;
+impl<V: 'static> Element<V> for Text {
+    type ElementState = TextState;
 
     fn element_id(&self) -> Option<crate::ElementId> {
         None
     }
 
-    fn initialize(
-        &mut self,
-        _view_state: &mut V,
-        element_state: Option<Self::ElementState>,
-        _cx: &mut ViewContext<V>,
-    ) -> Self::ElementState {
-        element_state.unwrap_or_default()
-    }
-
     fn layout(
         &mut self,
         _view: &mut V,
-        element_state: &mut Self::ElementState,
+        element_state: Option<Self::ElementState>,
         cx: &mut ViewContext<V>,
-    ) -> LayoutId {
+    ) -> (LayoutId, Self::ElementState) {
+        let element_state = element_state.unwrap_or_default();
         let text_system = cx.text_system().clone();
         let text_style = cx.text_style();
         let font_size = text_style.font_size.to_pixels(cx.rem_size());
@@ -111,7 +66,7 @@ impl<V: 'static> Element<V> for Text<V> {
             let element_state = element_state.clone();
             move |known_dimensions, _| {
                 let Some(lines) = text_system
-                    .layout_text(
+                    .shape_text(
                         &text,
                         font_size,
                         &runs[..],
@@ -119,36 +74,29 @@ impl<V: 'static> Element<V> for Text<V> {
                     )
                     .log_err()
                 else {
-                    element_state.lock().replace(TextElementState {
+                    element_state.lock().replace(TextStateInner {
                         lines: Default::default(),
                         line_height,
                     });
                     return Size::default();
                 };
 
-                let line_count = lines
-                    .iter()
-                    .map(|line| line.wrap_count() + 1)
-                    .sum::<usize>();
-                let size = Size {
-                    width: lines
-                        .iter()
-                        .map(|line| line.layout.width)
-                        .max()
-                        .unwrap()
-                        .ceil(),
-                    height: line_height * line_count,
-                };
+                let mut size: Size<Pixels> = Size::default();
+                for line in &lines {
+                    let line_size = line.size(line_height);
+                    size.height += line_size.height;
+                    size.width = size.width.max(line_size.width);
+                }
 
                 element_state
                     .lock()
-                    .replace(TextElementState { lines, line_height });
+                    .replace(TextStateInner { lines, line_height });
 
                 size
             }
         });
 
-        layout_id
+        (layout_id, element_state)
     }
 
     fn paint(
@@ -173,7 +121,104 @@ impl<V: 'static> Element<V> for Text<V> {
     }
 }
 
-pub struct TextElementState {
-    lines: SmallVec<[Line; 1]>,
+#[derive(Default, Clone)]
+pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
+
+impl TextState {
+    fn lock(&self) -> MutexGuard<Option<TextStateInner>> {
+        self.0.lock()
+    }
+}
+
+struct TextStateInner {
+    lines: SmallVec<[WrappedLine; 1]>,
     line_height: Pixels,
 }
+
+struct InteractiveText {
+    id: ElementId,
+    text: Text,
+}
+
+struct InteractiveTextState {
+    text_state: TextState,
+    clicked_range_ixs: Rc<Cell<SmallVec<[usize; 1]>>>,
+}
+
+impl<V: 'static> Element<V> for InteractiveText {
+    type ElementState = InteractiveTextState;
+
+    fn element_id(&self) -> Option<ElementId> {
+        Some(self.id.clone())
+    }
+
+    fn layout(
+        &mut self,
+        view_state: &mut V,
+        element_state: Option<Self::ElementState>,
+        cx: &mut ViewContext<V>,
+    ) -> (LayoutId, Self::ElementState) {
+        if let Some(InteractiveTextState {
+            text_state,
+            clicked_range_ixs,
+        }) = element_state
+        {
+            let (layout_id, text_state) = self.text.layout(view_state, Some(text_state), cx);
+            let element_state = InteractiveTextState {
+                text_state,
+                clicked_range_ixs,
+            };
+            (layout_id, element_state)
+        } else {
+            let (layout_id, text_state) = self.text.layout(view_state, None, cx);
+            let element_state = InteractiveTextState {
+                text_state,
+                clicked_range_ixs: Rc::default(),
+            };
+            (layout_id, element_state)
+        }
+    }
+
+    fn paint(
+        &mut self,
+        bounds: Bounds<Pixels>,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
+    ) {
+        self.text
+            .paint(bounds, view_state, &mut element_state.text_state, cx)
+    }
+}
+
+impl<V: 'static> Component<V> for SharedString {
+    fn render(self) -> AnyElement<V> {
+        Text {
+            text: self,
+            runs: None,
+        }
+        .render()
+    }
+}
+
+impl<V: 'static> Component<V> for &'static str {
+    fn render(self) -> AnyElement<V> {
+        Text {
+            text: self.into(),
+            runs: None,
+        }
+        .render()
+    }
+}
+
+// TODO: Figure out how to pass `String` to `child` without this.
+// This impl doesn't exist in the `gpui2` crate.
+impl<V: 'static> Component<V> for String {
+    fn render(self) -> AnyElement<V> {
+        Text {
+            text: self.into(),
+            runs: None,
+        }
+        .render()
+    }
+}

crates/gpui2/src/elements/uniform_list.rs 🔗

@@ -108,62 +108,54 @@ impl<V: 'static> Element<V> for UniformList<V> {
         Some(self.id.clone())
     }
 
-    fn initialize(
+    fn layout(
         &mut self,
         view_state: &mut V,
         element_state: Option<Self::ElementState>,
         cx: &mut ViewContext<V>,
-    ) -> Self::ElementState {
-        if let Some(mut element_state) = element_state {
-            element_state.interactive = self
-                .interactivity
-                .initialize(Some(element_state.interactive), cx);
-            element_state
-        } else {
-            let item_size = self.measure_item(view_state, None, cx);
-            UniformListState {
-                interactive: self.interactivity.initialize(None, cx),
-                item_size,
-            }
-        }
-    }
-
-    fn layout(
-        &mut self,
-        _view_state: &mut V,
-        element_state: &mut Self::ElementState,
-        cx: &mut ViewContext<V>,
-    ) -> LayoutId {
+    ) -> (LayoutId, Self::ElementState) {
         let max_items = self.item_count;
-        let item_size = element_state.item_size;
         let rem_size = cx.rem_size();
+        let item_size = element_state
+            .as_ref()
+            .map(|s| s.item_size)
+            .unwrap_or_else(|| self.measure_item(view_state, None, cx));
 
-        self.interactivity
-            .layout(&mut element_state.interactive, cx, |style, cx| {
-                cx.request_measured_layout(
-                    style,
-                    rem_size,
-                    move |known_dimensions: Size<Option<Pixels>>,
-                          available_space: Size<AvailableSpace>| {
-                        let desired_height = item_size.height * max_items;
-                        let width = known_dimensions
-                            .width
-                            .unwrap_or(match available_space.width {
-                                AvailableSpace::Definite(x) => x,
+        let (layout_id, interactive) =
+            self.interactivity
+                .layout(element_state.map(|s| s.interactive), cx, |style, cx| {
+                    cx.request_measured_layout(
+                        style,
+                        rem_size,
+                        move |known_dimensions: Size<Option<Pixels>>,
+                              available_space: Size<AvailableSpace>| {
+                            let desired_height = item_size.height * max_items;
+                            let width =
+                                known_dimensions
+                                    .width
+                                    .unwrap_or(match available_space.width {
+                                        AvailableSpace::Definite(x) => x,
+                                        AvailableSpace::MinContent | AvailableSpace::MaxContent => {
+                                            item_size.width
+                                        }
+                                    });
+                            let height = match available_space.height {
+                                AvailableSpace::Definite(x) => desired_height.min(x),
                                 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
-                                    item_size.width
+                                    desired_height
                                 }
-                            });
-                        let height = match available_space.height {
-                            AvailableSpace::Definite(x) => desired_height.min(x),
-                            AvailableSpace::MinContent | AvailableSpace::MaxContent => {
-                                desired_height
-                            }
-                        };
-                        size(width, height)
-                    },
-                )
-            })
+                            };
+                            size(width, height)
+                        },
+                    )
+                });
+
+        let element_state = UniformListState {
+            interactive,
+            item_size,
+        };
+
+        (layout_id, element_state)
     }
 
     fn paint(

crates/gpui2/src/executor.rs 🔗

@@ -5,10 +5,11 @@ use std::{
     fmt::Debug,
     marker::PhantomData,
     mem,
+    num::NonZeroUsize,
     pin::Pin,
     rc::Rc,
     sync::{
-        atomic::{AtomicBool, Ordering::SeqCst},
+        atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
         Arc,
     },
     task::{Context, Poll},
@@ -71,30 +72,57 @@ impl<T> Future for Task<T> {
         }
     }
 }
+
+#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
+pub struct TaskLabel(NonZeroUsize);
+
+impl TaskLabel {
+    pub fn new() -> Self {
+        static NEXT_TASK_LABEL: AtomicUsize = AtomicUsize::new(1);
+        Self(NEXT_TASK_LABEL.fetch_add(1, SeqCst).try_into().unwrap())
+    }
+}
+
 type AnyLocalFuture<R> = Pin<Box<dyn 'static + Future<Output = R>>>;
+
 type AnyFuture<R> = Pin<Box<dyn 'static + Send + Future<Output = R>>>;
+
 impl BackgroundExecutor {
     pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
         Self { dispatcher }
     }
 
-    /// Enqueues the given closure to be run on any thread. The closure returns
-    /// a future which will be run to completion on any available thread.
+    /// Enqueues the given future to be run to completion on a background thread.
     pub fn spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
     where
         R: Send + 'static,
     {
+        self.spawn_internal::<R>(Box::pin(future), None)
+    }
+
+    /// Enqueues the given future to be run to completion on a background thread.
+    /// The given label can be used to control the priority of the task in tests.
+    pub fn spawn_labeled<R>(
+        &self,
+        label: TaskLabel,
+        future: impl Future<Output = R> + Send + 'static,
+    ) -> Task<R>
+    where
+        R: Send + 'static,
+    {
+        self.spawn_internal::<R>(Box::pin(future), Some(label))
+    }
+
+    fn spawn_internal<R: Send + 'static>(
+        &self,
+        future: AnyFuture<R>,
+        label: Option<TaskLabel>,
+    ) -> Task<R> {
         let dispatcher = self.dispatcher.clone();
-        fn inner<R: Send + 'static>(
-            dispatcher: Arc<dyn PlatformDispatcher>,
-            future: AnyFuture<R>,
-        ) -> Task<R> {
-            let (runnable, task) =
-                async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable));
-            runnable.schedule();
-            Task::Spawned(task)
-        }
-        inner::<R>(dispatcher, Box::pin(future))
+        let (runnable, task) =
+            async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable, label));
+        runnable.schedule();
+        Task::Spawned(task)
     }
 
     #[cfg(any(test, feature = "test-support"))]
@@ -130,7 +158,7 @@ impl BackgroundExecutor {
             match future.as_mut().poll(&mut cx) {
                 Poll::Ready(result) => return result,
                 Poll::Pending => {
-                    if !self.dispatcher.poll(background_only) {
+                    if !self.dispatcher.tick(background_only) {
                         if awoken.swap(false, SeqCst) {
                             continue;
                         }
@@ -216,11 +244,21 @@ impl BackgroundExecutor {
         self.dispatcher.as_test().unwrap().simulate_random_delay()
     }
 
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn deprioritize(&self, task_label: TaskLabel) {
+        self.dispatcher.as_test().unwrap().deprioritize(task_label)
+    }
+
     #[cfg(any(test, feature = "test-support"))]
     pub fn advance_clock(&self, duration: Duration) {
         self.dispatcher.as_test().unwrap().advance_clock(duration)
     }
 
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn tick(&self) -> bool {
+        self.dispatcher.as_test().unwrap().tick(false)
+    }
+
     #[cfg(any(test, feature = "test-support"))]
     pub fn run_until_parked(&self) {
         self.dispatcher.as_test().unwrap().run_until_parked()

crates/gpui2/src/geometry.rs 🔗

@@ -335,11 +335,15 @@ where
         };
         Bounds { origin, size }
     }
+
+    pub fn new(origin: Point<T>, size: Size<T>) -> Self {
+        Bounds { origin, size }
+    }
 }
 
 impl<T> Bounds<T>
 where
-    T: Clone + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T> + Default,
+    T: Clone + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T> + Default + Half,
 {
     pub fn intersects(&self, other: &Bounds<T>) -> bool {
         let my_lower_right = self.lower_right();
@@ -358,6 +362,13 @@ where
         self.size.width = self.size.width.clone() + double_amount.clone();
         self.size.height = self.size.height.clone() + double_amount;
     }
+
+    pub fn center(&self) -> Point<T> {
+        Point {
+            x: self.origin.x.clone() + self.size.width.clone().half(),
+            y: self.origin.y.clone() + self.size.height.clone().half(),
+        }
+    }
 }
 
 impl<T: Clone + Default + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T>> Bounds<T> {
@@ -421,6 +432,22 @@ impl<T> Bounds<T>
 where
     T: Add<T, Output = T> + Clone + Default + Debug,
 {
+    pub fn top(&self) -> T {
+        self.origin.y.clone()
+    }
+
+    pub fn bottom(&self) -> T {
+        self.origin.y.clone() + self.size.height.clone()
+    }
+
+    pub fn left(&self) -> T {
+        self.origin.x.clone()
+    }
+
+    pub fn right(&self) -> T {
+        self.origin.x.clone() + self.size.width.clone()
+    }
+
     pub fn upper_right(&self) -> Point<T> {
         Point {
             x: self.origin.x.clone() + self.size.width.clone(),
@@ -1191,6 +1218,46 @@ impl From<()> for Length {
     }
 }
 
+pub trait Half {
+    fn half(&self) -> Self;
+}
+
+impl Half for f32 {
+    fn half(&self) -> Self {
+        self / 2.
+    }
+}
+
+impl Half for DevicePixels {
+    fn half(&self) -> Self {
+        Self(self.0 / 2)
+    }
+}
+
+impl Half for ScaledPixels {
+    fn half(&self) -> Self {
+        Self(self.0 / 2.)
+    }
+}
+
+impl Half for Pixels {
+    fn half(&self) -> Self {
+        Self(self.0 / 2.)
+    }
+}
+
+impl Half for Rems {
+    fn half(&self) -> Self {
+        Self(self.0 / 2.)
+    }
+}
+
+impl Half for GlobalPixels {
+    fn half(&self) -> Self {
+        Self(self.0 / 2.)
+    }
+}
+
 pub trait IsZero {
     fn is_zero(&self) -> bool;
 }

crates/gpui2/src/gpui2.rs 🔗

@@ -49,11 +49,13 @@ pub use input::*;
 pub use interactive::*;
 pub use key_dispatch::*;
 pub use keymap::*;
+pub use linkme;
 pub use platform::*;
 use private::Sealed;
 pub use refineable::*;
 pub use scene::*;
 pub use serde;
+pub use serde_derive;
 pub use serde_json;
 pub use smallvec;
 pub use smol::Timer;
@@ -135,6 +137,10 @@ pub trait VisualContext: Context {
     ) -> Self::Result<View<V>>
     where
         V: Render;
+
+    fn focus_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
+    where
+        V: FocusableView;
 }
 
 pub trait Entity<T>: Sealed {

crates/gpui2/src/key_dispatch.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
-    build_action_from_type, Action, DispatchPhase, FocusId, KeyBinding, KeyContext, KeyMatch,
-    Keymap, Keystroke, KeystrokeMatcher, WindowContext,
+    Action, ActionRegistry, DispatchPhase, FocusId, KeyBinding, KeyContext, KeyMatch, Keymap,
+    Keystroke, KeystrokeMatcher, WindowContext,
 };
 use collections::HashMap;
 use parking_lot::Mutex;
@@ -10,7 +10,6 @@ use std::{
     rc::Rc,
     sync::Arc,
 };
-use util::ResultExt;
 
 #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
 pub struct DispatchNodeId(usize);
@@ -22,6 +21,7 @@ pub(crate) struct DispatchTree {
     focusable_node_ids: HashMap<FocusId, DispatchNodeId>,
     keystroke_matchers: HashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
     keymap: Arc<Mutex<Keymap>>,
+    action_registry: Rc<ActionRegistry>,
 }
 
 #[derive(Default)]
@@ -41,7 +41,7 @@ pub(crate) struct DispatchActionListener {
 }
 
 impl DispatchTree {
-    pub fn new(keymap: Arc<Mutex<Keymap>>) -> Self {
+    pub fn new(keymap: Arc<Mutex<Keymap>>, action_registry: Rc<ActionRegistry>) -> Self {
         Self {
             node_stack: Vec::new(),
             context_stack: Vec::new(),
@@ -49,6 +49,7 @@ impl DispatchTree {
             focusable_node_ids: HashMap::default(),
             keystroke_matchers: HashMap::default(),
             keymap,
+            action_registry,
         }
     }
 
@@ -153,7 +154,9 @@ impl DispatchTree {
             for node_id in self.dispatch_path(*node) {
                 let node = &self.nodes[node_id.0];
                 for DispatchActionListener { action_type, .. } in &node.action_listeners {
-                    actions.extend(build_action_from_type(action_type).log_err());
+                    // Intentionally silence these errors without logging.
+                    // If an action cannot be built by default, it's not available.
+                    actions.extend(self.action_registry.build_action_type(action_type).ok());
                 }
             }
         }

crates/gpui2/src/keymap/keymap.rs 🔗

@@ -1,7 +1,10 @@
-use crate::{KeyBinding, KeyBindingContextPredicate, Keystroke};
+use crate::{KeyBinding, KeyBindingContextPredicate, Keystroke, NoAction};
 use collections::HashSet;
 use smallvec::SmallVec;
-use std::{any::TypeId, collections::HashMap};
+use std::{
+    any::{Any, TypeId},
+    collections::HashMap,
+};
 
 #[derive(Copy, Clone, Eq, PartialEq, Default)]
 pub struct KeymapVersion(usize);
@@ -37,20 +40,19 @@ impl Keymap {
     }
 
     pub fn add_bindings<T: IntoIterator<Item = KeyBinding>>(&mut self, bindings: T) {
-        // todo!("no action")
-        // let no_action_id = (NoAction {}).id();
+        let no_action_id = &(NoAction {}).type_id();
         let mut new_bindings = Vec::new();
-        let has_new_disabled_keystrokes = false;
+        let mut has_new_disabled_keystrokes = false;
         for binding in bindings {
-            // if binding.action().id() == no_action_id {
-            //     has_new_disabled_keystrokes |= self
-            //         .disabled_keystrokes
-            //         .entry(binding.keystrokes)
-            //         .or_default()
-            //         .insert(binding.context_predicate);
-            // } else {
-            new_bindings.push(binding);
-            // }
+            if binding.action.type_id() == *no_action_id {
+                has_new_disabled_keystrokes |= self
+                    .disabled_keystrokes
+                    .entry(binding.keystrokes)
+                    .or_default()
+                    .insert(binding.context_predicate);
+            } else {
+                new_bindings.push(binding);
+            }
         }
 
         if has_new_disabled_keystrokes {

crates/gpui2/src/platform.rs 🔗

@@ -8,7 +8,7 @@ use crate::{
     point, size, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId,
     FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, LineLayout,
     Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, Scene,
-    SharedString, Size,
+    SharedString, Size, TaskLabel,
 };
 use anyhow::{anyhow, bail};
 use async_task::Runnable;
@@ -162,10 +162,10 @@ pub(crate) trait PlatformWindow {
 
 pub trait PlatformDispatcher: Send + Sync {
     fn is_main_thread(&self) -> bool;
-    fn dispatch(&self, runnable: Runnable);
+    fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>);
     fn dispatch_on_main_thread(&self, runnable: Runnable);
     fn dispatch_after(&self, duration: Duration, runnable: Runnable);
-    fn poll(&self, background_only: bool) -> bool;
+    fn tick(&self, background_only: bool) -> bool;
     fn park(&self);
     fn unparker(&self) -> Unparker;
 

crates/gpui2/src/platform/mac/dispatcher.rs 🔗

@@ -2,7 +2,7 @@
 #![allow(non_camel_case_types)]
 #![allow(non_snake_case)]
 
-use crate::PlatformDispatcher;
+use crate::{PlatformDispatcher, TaskLabel};
 use async_task::Runnable;
 use objc::{
     class, msg_send,
@@ -37,7 +37,7 @@ impl PlatformDispatcher for MacDispatcher {
         is_main_thread == YES
     }
 
-    fn dispatch(&self, runnable: Runnable) {
+    fn dispatch(&self, runnable: Runnable, _: Option<TaskLabel>) {
         unsafe {
             dispatch_async_f(
                 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.try_into().unwrap(), 0),
@@ -71,7 +71,7 @@ impl PlatformDispatcher for MacDispatcher {
         }
     }
 
-    fn poll(&self, _background_only: bool) -> bool {
+    fn tick(&self, _background_only: bool) -> bool {
         false
     }
 

crates/gpui2/src/platform/mac/text_system.rs 🔗

@@ -343,10 +343,10 @@ impl MacTextSystemState {
         // Construct the attributed string, converting UTF8 ranges to UTF16 ranges.
         let mut string = CFMutableAttributedString::new();
         {
-            string.replace_str(&CFString::new(text), CFRange::init(0, 0));
+            string.replace_str(&CFString::new(text.as_ref()), CFRange::init(0, 0));
             let utf16_line_len = string.char_len() as usize;
 
-            let mut ix_converter = StringIndexConverter::new(text);
+            let mut ix_converter = StringIndexConverter::new(text.as_ref());
             for run in font_runs {
                 let utf8_end = ix_converter.utf8_ix + run.len;
                 let utf16_start = ix_converter.utf16_ix;
@@ -390,7 +390,7 @@ impl MacTextSystemState {
             };
             let font_id = self.id_for_native_font(font);
 
-            let mut ix_converter = StringIndexConverter::new(text);
+            let mut ix_converter = StringIndexConverter::new(text.as_ref());
             let mut glyphs = SmallVec::new();
             for ((glyph_id, position), glyph_utf16_ix) in run
                 .glyphs()
@@ -413,11 +413,11 @@ impl MacTextSystemState {
 
         let typographic_bounds = line.get_typographic_bounds();
         LineLayout {
+            runs,
+            font_size,
             width: typographic_bounds.width.into(),
             ascent: typographic_bounds.ascent.into(),
             descent: typographic_bounds.descent.into(),
-            runs,
-            font_size,
             len: text.len(),
         }
     }

crates/gpui2/src/platform/mac/window.rs 🔗

@@ -1141,7 +1141,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
     let event = unsafe { InputEvent::from_native(native_event, Some(window_height)) };
 
     if let Some(mut event) = event {
-        let synthesized_second_event = match &mut event {
+        match &mut event {
             InputEvent::MouseDown(
                 event @ MouseDownEvent {
                     button: MouseButton::Left,
@@ -1149,6 +1149,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
                     ..
                 },
             ) => {
+                // On mac, a ctrl-left click should be handled as a right click.
                 *event = MouseDownEvent {
                     button: MouseButton::Right,
                     modifiers: Modifiers {
@@ -1158,26 +1159,30 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
                     click_count: 1,
                     ..*event
                 };
-
-                Some(InputEvent::MouseDown(MouseDownEvent {
-                    button: MouseButton::Right,
-                    ..*event
-                }))
             }
 
             // Because we map a ctrl-left_down to a right_down -> right_up let's ignore
             // the ctrl-left_up to avoid having a mismatch in button down/up events if the
             // user is still holding ctrl when releasing the left mouse button
-            InputEvent::MouseUp(MouseUpEvent {
-                button: MouseButton::Left,
-                modifiers: Modifiers { control: true, .. },
-                ..
-            }) => {
-                lock.synthetic_drag_counter += 1;
-                return;
+            InputEvent::MouseUp(
+                event @ MouseUpEvent {
+                    button: MouseButton::Left,
+                    modifiers: Modifiers { control: true, .. },
+                    ..
+                },
+            ) => {
+                *event = MouseUpEvent {
+                    button: MouseButton::Right,
+                    modifiers: Modifiers {
+                        control: false,
+                        ..event.modifiers
+                    },
+                    click_count: 1,
+                    ..*event
+                };
             }
 
-            _ => None,
+            _ => {}
         };
 
         match &event {
@@ -1227,9 +1232,6 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
         if let Some(mut callback) = lock.event_callback.take() {
             drop(lock);
             callback(event);
-            if let Some(event) = synthesized_second_event {
-                callback(event);
-            }
             window_state.lock().event_callback = Some(callback);
         }
     }

crates/gpui2/src/platform/test/dispatcher.rs 🔗

@@ -1,7 +1,7 @@
-use crate::PlatformDispatcher;
+use crate::{PlatformDispatcher, TaskLabel};
 use async_task::Runnable;
 use backtrace::Backtrace;
-use collections::{HashMap, VecDeque};
+use collections::{HashMap, HashSet, VecDeque};
 use parking::{Parker, Unparker};
 use parking_lot::Mutex;
 use rand::prelude::*;
@@ -28,12 +28,14 @@ struct TestDispatcherState {
     random: StdRng,
     foreground: HashMap<TestDispatcherId, VecDeque<Runnable>>,
     background: Vec<Runnable>,
+    deprioritized_background: Vec<Runnable>,
     delayed: Vec<(Duration, Runnable)>,
     time: Duration,
     is_main_thread: bool,
     next_id: TestDispatcherId,
     allow_parking: bool,
     waiting_backtrace: Option<Backtrace>,
+    deprioritized_task_labels: HashSet<TaskLabel>,
 }
 
 impl TestDispatcher {
@@ -43,12 +45,14 @@ impl TestDispatcher {
             random,
             foreground: HashMap::default(),
             background: Vec::new(),
+            deprioritized_background: Vec::new(),
             delayed: Vec::new(),
             time: Duration::ZERO,
             is_main_thread: true,
             next_id: TestDispatcherId(1),
             allow_parking: false,
             waiting_backtrace: None,
+            deprioritized_task_labels: Default::default(),
         };
 
         TestDispatcher {
@@ -101,8 +105,15 @@ impl TestDispatcher {
         }
     }
 
+    pub fn deprioritize(&self, task_label: TaskLabel) {
+        self.state
+            .lock()
+            .deprioritized_task_labels
+            .insert(task_label);
+    }
+
     pub fn run_until_parked(&self) {
-        while self.poll(false) {}
+        while self.tick(false) {}
     }
 
     pub fn parking_allowed(&self) -> bool {
@@ -150,8 +161,17 @@ impl PlatformDispatcher for TestDispatcher {
         self.state.lock().is_main_thread
     }
 
-    fn dispatch(&self, runnable: Runnable) {
-        self.state.lock().background.push(runnable);
+    fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>) {
+        {
+            let mut state = self.state.lock();
+            if label.map_or(false, |label| {
+                state.deprioritized_task_labels.contains(&label)
+            }) {
+                state.deprioritized_background.push(runnable);
+            } else {
+                state.background.push(runnable);
+            }
+        }
         self.unparker.unpark();
     }
 
@@ -174,7 +194,7 @@ impl PlatformDispatcher for TestDispatcher {
         state.delayed.insert(ix, (next_time, runnable));
     }
 
-    fn poll(&self, background_only: bool) -> bool {
+    fn tick(&self, background_only: bool) -> bool {
         let mut state = self.state.lock();
 
         while let Some((deadline, _)) = state.delayed.first() {
@@ -196,34 +216,41 @@ impl PlatformDispatcher for TestDispatcher {
         };
         let background_len = state.background.len();
 
+        let runnable;
+        let main_thread;
         if foreground_len == 0 && background_len == 0 {
-            return false;
-        }
-
-        let main_thread = state.random.gen_ratio(
-            foreground_len as u32,
-            (foreground_len + background_len) as u32,
-        );
-        let was_main_thread = state.is_main_thread;
-        state.is_main_thread = main_thread;
-
-        let runnable = if main_thread {
-            let state = &mut *state;
-            let runnables = state
-                .foreground
-                .values_mut()
-                .filter(|runnables| !runnables.is_empty())
-                .choose(&mut state.random)
-                .unwrap();
-            runnables.pop_front().unwrap()
+            let deprioritized_background_len = state.deprioritized_background.len();
+            if deprioritized_background_len == 0 {
+                return false;
+            }
+            let ix = state.random.gen_range(0..deprioritized_background_len);
+            main_thread = false;
+            runnable = state.deprioritized_background.swap_remove(ix);
         } else {
-            let ix = state.random.gen_range(0..background_len);
-            state.background.swap_remove(ix)
+            main_thread = state.random.gen_ratio(
+                foreground_len as u32,
+                (foreground_len + background_len) as u32,
+            );
+            if main_thread {
+                let state = &mut *state;
+                runnable = state
+                    .foreground
+                    .values_mut()
+                    .filter(|runnables| !runnables.is_empty())
+                    .choose(&mut state.random)
+                    .unwrap()
+                    .pop_front()
+                    .unwrap();
+            } else {
+                let ix = state.random.gen_range(0..background_len);
+                runnable = state.background.swap_remove(ix);
+            };
         };
 
+        let was_main_thread = state.is_main_thread;
+        state.is_main_thread = main_thread;
         drop(state);
         runnable.run();
-
         self.state.lock().is_main_thread = was_main_thread;
 
         true

crates/gpui2/src/style.rs 🔗

@@ -203,6 +203,7 @@ impl TextStyle {
                 style: self.font_style,
             },
             color: self.color,
+            background_color: None,
             underline: self.underline.clone(),
         }
     }

crates/gpui2/src/text_system.rs 🔗

@@ -3,20 +3,20 @@ mod line;
 mod line_layout;
 mod line_wrapper;
 
-use anyhow::anyhow;
 pub use font_features::*;
 pub use line::*;
 pub use line_layout::*;
 pub use line_wrapper::*;
-use smallvec::SmallVec;
 
 use crate::{
     px, Bounds, DevicePixels, Hsla, Pixels, PlatformTextSystem, Point, Result, SharedString, Size,
     UnderlineStyle,
 };
+use anyhow::anyhow;
 use collections::HashMap;
 use core::fmt;
 use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
+use smallvec::SmallVec;
 use std::{
     cmp,
     fmt::{Debug, Display, Formatter},
@@ -151,13 +151,79 @@ impl TextSystem {
         }
     }
 
-    pub fn layout_text(
+    pub fn layout_line(
         &self,
         text: &str,
         font_size: Pixels,
         runs: &[TextRun],
+    ) -> Result<Arc<LineLayout>> {
+        let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
+        for run in runs.iter() {
+            let font_id = self.font_id(&run.font)?;
+            if let Some(last_run) = font_runs.last_mut() {
+                if last_run.font_id == font_id {
+                    last_run.len += run.len;
+                    continue;
+                }
+            }
+            font_runs.push(FontRun {
+                len: run.len,
+                font_id,
+            });
+        }
+
+        let layout = self
+            .line_layout_cache
+            .layout_line(&text, font_size, &font_runs);
+
+        font_runs.clear();
+        self.font_runs_pool.lock().push(font_runs);
+
+        Ok(layout)
+    }
+
+    pub fn shape_line(
+        &self,
+        text: SharedString,
+        font_size: Pixels,
+        runs: &[TextRun],
+    ) -> Result<ShapedLine> {
+        debug_assert!(
+            text.find('\n').is_none(),
+            "text argument should not contain newlines"
+        );
+
+        let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
+        for run in runs {
+            if let Some(last_run) = decoration_runs.last_mut() {
+                if last_run.color == run.color && last_run.underline == run.underline {
+                    last_run.len += run.len as u32;
+                    continue;
+                }
+            }
+            decoration_runs.push(DecorationRun {
+                len: run.len as u32,
+                color: run.color,
+                underline: run.underline.clone(),
+            });
+        }
+
+        let layout = self.layout_line(text.as_ref(), font_size, runs)?;
+
+        Ok(ShapedLine {
+            layout,
+            text,
+            decoration_runs,
+        })
+    }
+
+    pub fn shape_text(
+        &self,
+        text: &str, // todo!("pass a SharedString and preserve it when passed a single line?")
+        font_size: Pixels,
+        runs: &[TextRun],
         wrap_width: Option<Pixels>,
-    ) -> Result<SmallVec<[Line; 1]>> {
+    ) -> Result<SmallVec<[WrappedLine; 1]>> {
         let mut runs = runs.iter().cloned().peekable();
         let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
 
@@ -210,10 +276,11 @@ impl TextSystem {
 
             let layout = self
                 .line_layout_cache
-                .layout_line(&line_text, font_size, &font_runs, wrap_width);
-            lines.push(Line {
+                .layout_wrapped_line(&line_text, font_size, &font_runs, wrap_width);
+            lines.push(WrappedLine {
                 layout,
-                decorations: decoration_runs,
+                decoration_runs,
+                text: SharedString::from(line_text),
             });
 
             line_start = line_end + 1; // Skip `\n` character.
@@ -384,6 +451,7 @@ pub struct TextRun {
     pub len: usize,
     pub font: Font,
     pub color: Hsla,
+    pub background_color: Option<Hsla>,
     pub underline: Option<UnderlineStyle>,
 }
 

crates/gpui2/src/text_system/line.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{
-    black, point, px, size, BorrowWindow, Bounds, Hsla, Pixels, Point, Result, Size,
+    black, point, px, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result, SharedString,
     UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout,
 };
 use derive_more::{Deref, DerefMut};
@@ -14,23 +14,51 @@ pub struct DecorationRun {
 }
 
 #[derive(Clone, Default, Debug, Deref, DerefMut)]
-pub struct Line {
+pub struct ShapedLine {
     #[deref]
     #[deref_mut]
-    pub(crate) layout: Arc<WrappedLineLayout>,
-    pub(crate) decorations: SmallVec<[DecorationRun; 32]>,
+    pub(crate) layout: Arc<LineLayout>,
+    pub text: SharedString,
+    pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>,
 }
 
-impl Line {
-    pub fn size(&self, line_height: Pixels) -> Size<Pixels> {
-        size(
-            self.layout.width,
-            line_height * (self.layout.wrap_boundaries.len() + 1),
-        )
+impl ShapedLine {
+    pub fn len(&self) -> usize {
+        self.layout.len
     }
 
-    pub fn wrap_count(&self) -> usize {
-        self.layout.wrap_boundaries.len()
+    pub fn paint(
+        &self,
+        origin: Point<Pixels>,
+        line_height: Pixels,
+        cx: &mut WindowContext,
+    ) -> Result<()> {
+        paint_line(
+            origin,
+            &self.layout,
+            line_height,
+            &self.decoration_runs,
+            None,
+            &[],
+            cx,
+        )?;
+
+        Ok(())
+    }
+}
+
+#[derive(Clone, Default, Debug, Deref, DerefMut)]
+pub struct WrappedLine {
+    #[deref]
+    #[deref_mut]
+    pub(crate) layout: Arc<WrappedLineLayout>,
+    pub text: SharedString,
+    pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>,
+}
+
+impl WrappedLine {
+    pub fn len(&self) -> usize {
+        self.layout.len()
     }
 
     pub fn paint(
@@ -39,75 +67,50 @@ impl Line {
         line_height: Pixels,
         cx: &mut WindowContext,
     ) -> Result<()> {
-        let padding_top =
-            (line_height - self.layout.layout.ascent - self.layout.layout.descent) / 2.;
-        let baseline_offset = point(px(0.), padding_top + self.layout.layout.ascent);
-
-        let mut style_runs = self.decorations.iter();
-        let mut wraps = self.layout.wrap_boundaries.iter().peekable();
-        let mut run_end = 0;
-        let mut color = black();
-        let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
-        let text_system = cx.text_system().clone();
-
-        let mut glyph_origin = origin;
-        let mut prev_glyph_position = Point::default();
-        for (run_ix, run) in self.layout.layout.runs.iter().enumerate() {
-            let max_glyph_size = text_system
-                .bounding_box(run.font_id, self.layout.layout.font_size)?
-                .size;
-
-            for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
-                glyph_origin.x += glyph.position.x - prev_glyph_position.x;
-
-                if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
-                    wraps.next();
-                    if let Some((underline_origin, underline_style)) = current_underline.take() {
-                        cx.paint_underline(
-                            underline_origin,
-                            glyph_origin.x - underline_origin.x,
-                            &underline_style,
-                        )?;
-                    }
-
-                    glyph_origin.x = origin.x;
-                    glyph_origin.y += line_height;
-                }
-                prev_glyph_position = glyph.position;
-
-                let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
-                if glyph.index >= run_end {
-                    if let Some(style_run) = style_runs.next() {
-                        if let Some((_, underline_style)) = &mut current_underline {
-                            if style_run.underline.as_ref() != Some(underline_style) {
-                                finished_underline = current_underline.take();
-                            }
-                        }
-                        if let Some(run_underline) = style_run.underline.as_ref() {
-                            current_underline.get_or_insert((
-                                point(
-                                    glyph_origin.x,
-                                    origin.y
-                                        + baseline_offset.y
-                                        + (self.layout.layout.descent * 0.618),
-                                ),
-                                UnderlineStyle {
-                                    color: Some(run_underline.color.unwrap_or(style_run.color)),
-                                    thickness: run_underline.thickness,
-                                    wavy: run_underline.wavy,
-                                },
-                            ));
-                        }
+        paint_line(
+            origin,
+            &self.layout.unwrapped_layout,
+            line_height,
+            &self.decoration_runs,
+            self.wrap_width,
+            &self.wrap_boundaries,
+            cx,
+        )?;
 
-                        run_end += style_run.len as usize;
-                        color = style_run.color;
-                    } else {
-                        run_end = self.layout.text.len();
-                        finished_underline = current_underline.take();
-                    }
-                }
+        Ok(())
+    }
+}
 
-                if let Some((underline_origin, underline_style)) = finished_underline {
+fn paint_line(
+    origin: Point<Pixels>,
+    layout: &LineLayout,
+    line_height: Pixels,
+    decoration_runs: &[DecorationRun],
+    wrap_width: Option<Pixels>,
+    wrap_boundaries: &[WrapBoundary],
+    cx: &mut WindowContext<'_>,
+) -> Result<()> {
+    let padding_top = (line_height - layout.ascent - layout.descent) / 2.;
+    let baseline_offset = point(px(0.), padding_top + layout.ascent);
+    let mut decoration_runs = decoration_runs.iter();
+    let mut wraps = wrap_boundaries.iter().peekable();
+    let mut run_end = 0;
+    let mut color = black();
+    let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
+    let text_system = cx.text_system().clone();
+    let mut glyph_origin = origin;
+    let mut prev_glyph_position = Point::default();
+    for (run_ix, run) in layout.runs.iter().enumerate() {
+        let max_glyph_size = text_system
+            .bounding_box(run.font_id, layout.font_size)?
+            .size;
+
+        for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
+            glyph_origin.x += glyph.position.x - prev_glyph_position.x;
+
+            if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
+                wraps.next();
+                if let Some((underline_origin, underline_style)) = current_underline.take() {
                     cx.paint_underline(
                         underline_origin,
                         glyph_origin.x - underline_origin.x,
@@ -115,42 +118,84 @@ impl Line {
                     )?;
                 }
 
-                let max_glyph_bounds = Bounds {
-                    origin: glyph_origin,
-                    size: max_glyph_size,
-                };
-
-                let content_mask = cx.content_mask();
-                if max_glyph_bounds.intersects(&content_mask.bounds) {
-                    if glyph.is_emoji {
-                        cx.paint_emoji(
-                            glyph_origin + baseline_offset,
-                            run.font_id,
-                            glyph.id,
-                            self.layout.layout.font_size,
-                        )?;
-                    } else {
-                        cx.paint_glyph(
-                            glyph_origin + baseline_offset,
-                            run.font_id,
-                            glyph.id,
-                            self.layout.layout.font_size,
-                            color,
-                        )?;
+                glyph_origin.x = origin.x;
+                glyph_origin.y += line_height;
+            }
+            prev_glyph_position = glyph.position;
+
+            let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
+            if glyph.index >= run_end {
+                if let Some(style_run) = decoration_runs.next() {
+                    if let Some((_, underline_style)) = &mut current_underline {
+                        if style_run.underline.as_ref() != Some(underline_style) {
+                            finished_underline = current_underline.take();
+                        }
                     }
+                    if let Some(run_underline) = style_run.underline.as_ref() {
+                        current_underline.get_or_insert((
+                            point(
+                                glyph_origin.x,
+                                origin.y + baseline_offset.y + (layout.descent * 0.618),
+                            ),
+                            UnderlineStyle {
+                                color: Some(run_underline.color.unwrap_or(style_run.color)),
+                                thickness: run_underline.thickness,
+                                wavy: run_underline.wavy,
+                            },
+                        ));
+                    }
+
+                    run_end += style_run.len as usize;
+                    color = style_run.color;
+                } else {
+                    run_end = layout.len;
+                    finished_underline = current_underline.take();
                 }
             }
-        }
 
-        if let Some((underline_start, underline_style)) = current_underline.take() {
-            let line_end_x = origin.x + self.layout.layout.width;
-            cx.paint_underline(
-                underline_start,
-                line_end_x - underline_start.x,
-                &underline_style,
-            )?;
+            if let Some((underline_origin, underline_style)) = finished_underline {
+                cx.paint_underline(
+                    underline_origin,
+                    glyph_origin.x - underline_origin.x,
+                    &underline_style,
+                )?;
+            }
+
+            let max_glyph_bounds = Bounds {
+                origin: glyph_origin,
+                size: max_glyph_size,
+            };
+
+            let content_mask = cx.content_mask();
+            if max_glyph_bounds.intersects(&content_mask.bounds) {
+                if glyph.is_emoji {
+                    cx.paint_emoji(
+                        glyph_origin + baseline_offset,
+                        run.font_id,
+                        glyph.id,
+                        layout.font_size,
+                    )?;
+                } else {
+                    cx.paint_glyph(
+                        glyph_origin + baseline_offset,
+                        run.font_id,
+                        glyph.id,
+                        layout.font_size,
+                        color,
+                    )?;
+                }
+            }
         }
+    }
 
-        Ok(())
+    if let Some((underline_start, underline_style)) = current_underline.take() {
+        let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width);
+        cx.paint_underline(
+            underline_start,
+            line_end_x - underline_start.x,
+            &underline_style,
+        )?;
     }
+
+    Ok(())
 }

crates/gpui2/src/text_system/line_layout.rs 🔗

@@ -1,5 +1,4 @@
-use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, SharedString};
-use derive_more::{Deref, DerefMut};
+use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, Size};
 use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
 use smallvec::SmallVec;
 use std::{
@@ -149,13 +148,11 @@ impl LineLayout {
     }
 }
 
-#[derive(Deref, DerefMut, Default, Debug)]
+#[derive(Default, Debug)]
 pub struct WrappedLineLayout {
-    #[deref]
-    #[deref_mut]
-    pub layout: LineLayout,
-    pub text: SharedString,
+    pub unwrapped_layout: Arc<LineLayout>,
     pub wrap_boundaries: SmallVec<[WrapBoundary; 1]>,
+    pub wrap_width: Option<Pixels>,
 }
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
@@ -164,31 +161,74 @@ pub struct WrapBoundary {
     pub glyph_ix: usize,
 }
 
+impl WrappedLineLayout {
+    pub fn len(&self) -> usize {
+        self.unwrapped_layout.len
+    }
+
+    pub fn width(&self) -> Pixels {
+        self.wrap_width
+            .unwrap_or(Pixels::MAX)
+            .min(self.unwrapped_layout.width)
+    }
+
+    pub fn size(&self, line_height: Pixels) -> Size<Pixels> {
+        Size {
+            width: self.width(),
+            height: line_height * (self.wrap_boundaries.len() + 1),
+        }
+    }
+
+    pub fn ascent(&self) -> Pixels {
+        self.unwrapped_layout.ascent
+    }
+
+    pub fn descent(&self) -> Pixels {
+        self.unwrapped_layout.descent
+    }
+
+    pub fn wrap_boundaries(&self) -> &[WrapBoundary] {
+        &self.wrap_boundaries
+    }
+
+    pub fn font_size(&self) -> Pixels {
+        self.unwrapped_layout.font_size
+    }
+
+    pub fn runs(&self) -> &[ShapedRun] {
+        &self.unwrapped_layout.runs
+    }
+}
+
 pub(crate) struct LineLayoutCache {
-    prev_frame: Mutex<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
-    curr_frame: RwLock<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
+    previous_frame: Mutex<HashMap<CacheKey, Arc<LineLayout>>>,
+    current_frame: RwLock<HashMap<CacheKey, Arc<LineLayout>>>,
+    previous_frame_wrapped: Mutex<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
+    current_frame_wrapped: RwLock<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
     platform_text_system: Arc<dyn PlatformTextSystem>,
 }
 
 impl LineLayoutCache {
     pub fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
         Self {
-            prev_frame: Mutex::new(HashMap::new()),
-            curr_frame: RwLock::new(HashMap::new()),
+            previous_frame: Mutex::default(),
+            current_frame: RwLock::default(),
+            previous_frame_wrapped: Mutex::default(),
+            current_frame_wrapped: RwLock::default(),
             platform_text_system,
         }
     }
 
     pub fn start_frame(&self) {
-        let mut prev_frame = self.prev_frame.lock();
-        let mut curr_frame = self.curr_frame.write();
+        let mut prev_frame = self.previous_frame.lock();
+        let mut curr_frame = self.current_frame.write();
         std::mem::swap(&mut *prev_frame, &mut *curr_frame);
         curr_frame.clear();
     }
 
-    pub fn layout_line(
+    pub fn layout_wrapped_line(
         &self,
-        text: &SharedString,
+        text: &str,
         font_size: Pixels,
         runs: &[FontRun],
         wrap_width: Option<Pixels>,
@@ -199,34 +239,66 @@ impl LineLayoutCache {
             runs,
             wrap_width,
         } as &dyn AsCacheKeyRef;
-        let curr_frame = self.curr_frame.upgradable_read();
-        if let Some(layout) = curr_frame.get(key) {
+
+        let current_frame = self.current_frame_wrapped.upgradable_read();
+        if let Some(layout) = current_frame.get(key) {
             return layout.clone();
         }
 
-        let mut curr_frame = RwLockUpgradableReadGuard::upgrade(curr_frame);
-        if let Some((key, layout)) = self.prev_frame.lock().remove_entry(key) {
-            curr_frame.insert(key, layout.clone());
+        let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
+        if let Some((key, layout)) = self.previous_frame_wrapped.lock().remove_entry(key) {
+            current_frame.insert(key, layout.clone());
             layout
         } else {
-            let layout = self.platform_text_system.layout_line(text, font_size, runs);
-            let wrap_boundaries = wrap_width
-                .map(|wrap_width| layout.compute_wrap_boundaries(text.as_ref(), wrap_width))
-                .unwrap_or_default();
-            let wrapped_line = Arc::new(WrappedLineLayout {
-                layout,
-                text: text.clone(),
+            let unwrapped_layout = self.layout_line(text, font_size, runs);
+            let wrap_boundaries = if let Some(wrap_width) = wrap_width {
+                unwrapped_layout.compute_wrap_boundaries(text.as_ref(), wrap_width)
+            } else {
+                SmallVec::new()
+            };
+            let layout = Arc::new(WrappedLineLayout {
+                unwrapped_layout,
                 wrap_boundaries,
+                wrap_width,
             });
-
             let key = CacheKey {
-                text: text.clone(),
+                text: text.into(),
                 font_size,
                 runs: SmallVec::from(runs),
                 wrap_width,
             };
-            curr_frame.insert(key, wrapped_line.clone());
-            wrapped_line
+            current_frame.insert(key, layout.clone());
+            layout
+        }
+    }
+
+    pub fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> Arc<LineLayout> {
+        let key = &CacheKeyRef {
+            text,
+            font_size,
+            runs,
+            wrap_width: None,
+        } as &dyn AsCacheKeyRef;
+
+        let current_frame = self.current_frame.upgradable_read();
+        if let Some(layout) = current_frame.get(key) {
+            return layout.clone();
+        }
+
+        let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
+        if let Some((key, layout)) = self.previous_frame.lock().remove_entry(key) {
+            current_frame.insert(key, layout.clone());
+            layout
+        } else {
+            let layout = Arc::new(self.platform_text_system.layout_line(text, font_size, runs));
+            let key = CacheKey {
+                text: text.into(),
+                font_size,
+                runs: SmallVec::from(runs),
+                wrap_width: None,
+            };
+            current_frame.insert(key, layout.clone());
+            layout
         }
     }
 }
@@ -243,7 +315,7 @@ trait AsCacheKeyRef {
 
 #[derive(Eq)]
 struct CacheKey {
-    text: SharedString,
+    text: String,
     font_size: Pixels,
     runs: SmallVec<[FontRun; 1]>,
     wrap_width: Option<Pixels>,

crates/gpui2/src/view.rs 🔗

@@ -1,7 +1,8 @@
 use crate::{
     private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace,
-    Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, LayoutId, Model, Pixels,
-    Size, ViewContext, VisualContext, WeakModel, WindowContext,
+    BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, FocusHandle,
+    FocusableView, LayoutId, Model, Pixels, Point, Size, ViewContext, VisualContext, WeakModel,
+    WindowContext,
 };
 use anyhow::{Context, Result};
 use std::{
@@ -73,6 +74,13 @@ impl<V: 'static> View<V> {
             component: Some(component),
         }
     }
+
+    pub fn focus_handle(&self, cx: &AppContext) -> FocusHandle
+    where
+        V: FocusableView,
+    {
+        self.read(cx).focus_handle(cx)
+    }
 }
 
 impl<V> Clone for View<V> {
@@ -155,8 +163,7 @@ impl<V> Eq for WeakView<V> {}
 #[derive(Clone, Debug)]
 pub struct AnyView {
     model: AnyModel,
-    initialize: fn(&AnyView, &mut WindowContext) -> AnyBox,
-    layout: fn(&AnyView, &mut AnyBox, &mut WindowContext) -> LayoutId,
+    layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box<dyn Any>),
     paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
 }
 
@@ -164,7 +171,6 @@ impl AnyView {
     pub fn downgrade(&self) -> AnyWeakView {
         AnyWeakView {
             model: self.model.downgrade(),
-            initialize: self.initialize,
             layout: self.layout,
             paint: self.paint,
         }
@@ -175,7 +181,6 @@ impl AnyView {
             Ok(model) => Ok(View { model }),
             Err(model) => Err(Self {
                 model,
-                initialize: self.initialize,
                 layout: self.layout,
                 paint: self.paint,
             }),
@@ -186,13 +191,19 @@ impl AnyView {
         self.model.entity_type
     }
 
-    pub(crate) fn draw(&self, available_space: Size<AvailableSpace>, cx: &mut WindowContext) {
-        let mut rendered_element = (self.initialize)(self, cx);
-        let layout_id = (self.layout)(self, &mut rendered_element, cx);
-        cx.window
-            .layout_engine
-            .compute_layout(layout_id, available_space);
-        (self.paint)(self, &mut rendered_element, cx);
+    pub(crate) fn draw(
+        &self,
+        origin: Point<Pixels>,
+        available_space: Size<AvailableSpace>,
+        cx: &mut WindowContext,
+    ) {
+        cx.with_absolute_element_offset(origin, |cx| {
+            let (layout_id, mut rendered_element) = (self.layout)(self, cx);
+            cx.window
+                .layout_engine
+                .compute_layout(layout_id, available_space);
+            (self.paint)(self, &mut rendered_element, cx);
+        })
     }
 }
 
@@ -206,7 +217,6 @@ impl<V: Render> From<View<V>> for AnyView {
     fn from(value: View<V>) -> Self {
         AnyView {
             model: value.model.into_any(),
-            initialize: any_view::initialize::<V>,
             layout: any_view::layout::<V>,
             paint: any_view::paint::<V>,
         }
@@ -220,22 +230,13 @@ impl<ParentViewState: 'static> Element<ParentViewState> for AnyView {
         Some(self.model.entity_id.into())
     }
 
-    fn initialize(
-        &mut self,
-        _view_state: &mut ParentViewState,
-        _element_state: Option<Self::ElementState>,
-        cx: &mut ViewContext<ParentViewState>,
-    ) -> Self::ElementState {
-        (self.initialize)(self, cx)
-    }
-
     fn layout(
         &mut self,
         _view_state: &mut ParentViewState,
-        rendered_element: &mut Self::ElementState,
+        _element_state: Option<Self::ElementState>,
         cx: &mut ViewContext<ParentViewState>,
-    ) -> LayoutId {
-        (self.layout)(self, rendered_element, cx)
+    ) -> (LayoutId, Self::ElementState) {
+        (self.layout)(self, cx)
     }
 
     fn paint(
@@ -251,8 +252,7 @@ impl<ParentViewState: 'static> Element<ParentViewState> for AnyView {
 
 pub struct AnyWeakView {
     model: AnyWeakModel,
-    initialize: fn(&AnyView, &mut WindowContext) -> AnyBox,
-    layout: fn(&AnyView, &mut AnyBox, &mut WindowContext) -> LayoutId,
+    layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box<dyn Any>),
     paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
 }
 
@@ -261,7 +261,6 @@ impl AnyWeakView {
         let model = self.model.upgrade()?;
         Some(AnyView {
             model,
-            initialize: self.initialize,
             layout: self.layout,
             paint: self.paint,
         })
@@ -272,7 +271,6 @@ impl<V: Render> From<WeakView<V>> for AnyWeakView {
     fn from(view: WeakView<V>) -> Self {
         Self {
             model: view.model.into(),
-            initialize: any_view::initialize::<V>,
             layout: any_view::layout::<V>,
             paint: any_view::paint::<V>,
         }
@@ -319,28 +317,19 @@ where
         Some(self.view.entity_id().into())
     }
 
-    fn initialize(
+    fn layout(
         &mut self,
         _: &mut ParentViewState,
         _: Option<Self::ElementState>,
         cx: &mut ViewContext<ParentViewState>,
-    ) -> Self::ElementState {
+    ) -> (LayoutId, Self::ElementState) {
         self.view.update(cx, |view, cx| {
             let mut element = self.component.take().unwrap().render();
-            element.initialize(view, cx);
-            element
+            let layout_id = element.layout(view, cx);
+            (layout_id, element)
         })
     }
 
-    fn layout(
-        &mut self,
-        _: &mut ParentViewState,
-        element: &mut Self::ElementState,
-        cx: &mut ViewContext<ParentViewState>,
-    ) -> LayoutId {
-        self.view.update(cx, |view, cx| element.layout(view, cx))
-    }
-
     fn paint(
         &mut self,
         _: Bounds<Pixels>,
@@ -356,27 +345,17 @@ mod any_view {
     use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext};
     use std::any::Any;
 
-    pub(crate) fn initialize<V: Render>(view: &AnyView, cx: &mut WindowContext) -> Box<dyn Any> {
-        cx.with_element_id(Some(view.model.entity_id), |cx| {
-            let view = view.clone().downcast::<V>().unwrap();
-            let element = view.update(cx, |view, cx| {
-                let mut element = AnyElement::new(view.render(cx));
-                element.initialize(view, cx);
-                element
-            });
-            Box::new(element)
-        })
-    }
-
     pub(crate) fn layout<V: Render>(
         view: &AnyView,
-        element: &mut Box<dyn Any>,
         cx: &mut WindowContext,
-    ) -> LayoutId {
+    ) -> (LayoutId, Box<dyn Any>) {
         cx.with_element_id(Some(view.model.entity_id), |cx| {
             let view = view.clone().downcast::<V>().unwrap();
-            let element = element.downcast_mut::<AnyElement<V>>().unwrap();
-            view.update(cx, |view, cx| element.layout(view, cx))
+            view.update(cx, |view, cx| {
+                let mut element = AnyElement::new(view.render(cx));
+                let layout_id = element.layout(view, cx);
+                (layout_id, Box::new(element) as Box<dyn Any>)
+            })
         })
     }
 

crates/gpui2/src/window.rs 🔗

@@ -185,6 +185,27 @@ impl Drop for FocusHandle {
     }
 }
 
+/// FocusableView allows users of your view to easily
+/// focus it (using cx.focus_view(view))
+pub trait FocusableView: Render {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
+}
+
+/// ManagedView is a view (like a Modal, Popover, Menu, etc.)
+/// where the lifecycle of the view is handled by another view.
+pub trait ManagedView: Render {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
+}
+
+pub struct Dismiss;
+impl<T: ManagedView> EventEmitter<Dismiss> for T {}
+
+impl<T: ManagedView> FocusableView for T {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.focus_handle(cx)
+    }
+}
+
 // Holds the state for a specific window.
 pub struct Window {
     pub(crate) handle: AnyWindowHandle,
@@ -307,8 +328,8 @@ impl Window {
             layout_engine: TaffyLayoutEngine::new(),
             root_view: None,
             element_id_stack: GlobalElementId::default(),
-            previous_frame: Frame::new(DispatchTree::new(cx.keymap.clone())),
-            current_frame: Frame::new(DispatchTree::new(cx.keymap.clone())),
+            previous_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
+            current_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
             focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
             focus_listeners: SubscriberSet::new(),
             default_prevented: true,
@@ -570,6 +591,7 @@ impl<'a> WindowContext<'a> {
         result
     }
 
+    #[must_use]
     /// Add a node to the layout tree for the current frame. Takes the `Style` of the element for which
     /// layout is being requested, along with the layout ids of any children. This method is called during
     /// calls to the `Element::layout` trait method and enables any element to participate in layout.
@@ -1076,26 +1098,22 @@ impl<'a> WindowContext<'a> {
 
         self.with_z_index(0, |cx| {
             let available_space = cx.window.viewport_size.map(Into::into);
-            root_view.draw(available_space, cx);
+            root_view.draw(Point::zero(), available_space, cx);
         });
 
         if let Some(active_drag) = self.app.active_drag.take() {
             self.with_z_index(1, |cx| {
                 let offset = cx.mouse_position() - active_drag.cursor_offset;
-                cx.with_element_offset(offset, |cx| {
-                    let available_space =
-                        size(AvailableSpace::MinContent, AvailableSpace::MinContent);
-                    active_drag.view.draw(available_space, cx);
-                    cx.active_drag = Some(active_drag);
-                });
+                let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
+                active_drag.view.draw(offset, available_space, cx);
+                cx.active_drag = Some(active_drag);
             });
         } else if let Some(active_tooltip) = self.app.active_tooltip.take() {
             self.with_z_index(1, |cx| {
-                cx.with_element_offset(active_tooltip.cursor_offset, |cx| {
-                    let available_space =
-                        size(AvailableSpace::MinContent, AvailableSpace::MinContent);
-                    active_tooltip.view.draw(available_space, cx);
-                });
+                let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
+                active_tooltip
+                    .view
+                    .draw(active_tooltip.cursor_offset, available_space, cx);
             });
         }
 
@@ -1150,6 +1168,14 @@ impl<'a> WindowContext<'a> {
                 self.window.mouse_position = mouse_move.position;
                 InputEvent::MouseMove(mouse_move)
             }
+            InputEvent::MouseDown(mouse_down) => {
+                self.window.mouse_position = mouse_down.position;
+                InputEvent::MouseDown(mouse_down)
+            }
+            InputEvent::MouseUp(mouse_up) => {
+                self.window.mouse_position = mouse_up.position;
+                InputEvent::MouseUp(mouse_up)
+            }
             // Translate dragging and dropping of external files from the operating system
             // to internal drag and drop events.
             InputEvent::FileDrop(file_drop) => match file_drop {
@@ -1550,6 +1576,12 @@ impl VisualContext for WindowContext<'_> {
         self.window.root_view = Some(view.clone().into());
         view
     }
+
+    fn focus_view<V: crate::FocusableView>(&mut self, view: &View<V>) -> Self::Result<()> {
+        self.update_view(view, |view, cx| {
+            view.focus_handle(cx).clone().focus(cx);
+        })
+    }
 }
 
 impl<'a> std::ops::Deref for WindowContext<'a> {
@@ -1633,8 +1665,8 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
         }
     }
 
-    /// Update the global element offset based on the given offset. This is used to implement
-    /// scrolling and position drag handles.
+    /// Update the global element offset relative to the current offset. This is used to implement
+    /// scrolling.
     fn with_element_offset<R>(
         &mut self,
         offset: Point<Pixels>,
@@ -1644,7 +1676,17 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
             return f(self);
         };
 
-        let offset = self.element_offset() + offset;
+        let abs_offset = self.element_offset() + offset;
+        self.with_absolute_element_offset(abs_offset, f)
+    }
+
+    /// Update the global element offset based on the given offset. This is used to implement
+    /// drag handles and other manual painting of elements.
+    fn with_absolute_element_offset<R>(
+        &mut self,
+        offset: Point<Pixels>,
+        f: impl FnOnce(&mut Self) -> R,
+    ) -> R {
         self.window_mut()
             .current_frame
             .element_offset_stack
@@ -1814,8 +1856,8 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         self.view
     }
 
-    pub fn model(&self) -> Model<V> {
-        self.view.model.clone()
+    pub fn model(&self) -> &Model<V> {
+        &self.view.model
     }
 
     /// Access the underlying window context.
@@ -2147,7 +2189,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
 
     pub fn observe_global<G: 'static>(
         &mut self,
-        f: impl Fn(&mut V, &mut ViewContext<'_, V>) + 'static,
+        mut f: impl FnMut(&mut V, &mut ViewContext<'_, V>) + 'static,
     ) -> Subscription {
         let window_handle = self.window.handle;
         let view = self.view().downgrade();
@@ -2213,9 +2255,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
                 .set_input_handler(Box::new(input_handler));
         }
     }
-}
 
-impl<V> ViewContext<'_, V> {
     pub fn emit<Evt>(&mut self, event: Evt)
     where
         Evt: 'static,
@@ -2228,6 +2268,13 @@ impl<V> ViewContext<'_, V> {
             event: Box::new(event),
         });
     }
+
+    pub fn focus_self(&mut self)
+    where
+        V: FocusableView,
+    {
+        self.defer(|view, cx| view.focus_handle(cx).focus(cx))
+    }
 }
 
 impl<V> Context for ViewContext<'_, V> {
@@ -2303,6 +2350,10 @@ impl<V: 'static> VisualContext for ViewContext<'_, V> {
     {
         self.window_cx.replace_root_view(build_view)
     }
+
+    fn focus_view<W: FocusableView>(&mut self, view: &View<W>) -> Self::Result<()> {
+        self.window_cx.focus_view(view)
+    }
 }
 
 impl<'a, V> std::ops::Deref for ViewContext<'a, V> {

crates/gpui2/tests/action_macros.rs 🔗

@@ -0,0 +1,45 @@
+use serde_derive::Deserialize;
+
+#[test]
+fn test_derive() {
+    use gpui2 as gpui;
+
+    #[derive(PartialEq, Clone, Deserialize, gpui2_macros::Action)]
+    struct AnotherTestAction;
+
+    #[gpui2_macros::register_action]
+    #[derive(PartialEq, Clone, gpui::serde_derive::Deserialize)]
+    struct RegisterableAction {}
+
+    impl gpui::Action for RegisterableAction {
+        fn boxed_clone(&self) -> Box<dyn gpui::Action> {
+            todo!()
+        }
+
+        fn as_any(&self) -> &dyn std::any::Any {
+            todo!()
+        }
+
+        fn partial_eq(&self, _action: &dyn gpui::Action) -> bool {
+            todo!()
+        }
+
+        fn name(&self) -> &str {
+            todo!()
+        }
+
+        fn debug_name() -> &'static str
+        where
+            Self: Sized,
+        {
+            todo!()
+        }
+
+        fn build(_value: serde_json::Value) -> anyhow::Result<Box<dyn gpui::Action>>
+        where
+            Self: Sized,
+        {
+            todo!()
+        }
+    }
+}

crates/gpui2_macros/Cargo.toml 🔗

@@ -9,6 +9,6 @@ path = "src/gpui2_macros.rs"
 proc-macro = true
 
 [dependencies]
-syn = "1.0.72"
+syn = { version = "1.0.72", features = ["full"] }
 quote = "1.0.9"
 proc-macro2 = "1.0.66"

crates/gpui2_macros/src/action.rs 🔗

@@ -15,48 +15,81 @@
 
 use proc_macro::TokenStream;
 use quote::quote;
-use syn::{parse_macro_input, DeriveInput};
+use syn::{parse_macro_input, DeriveInput, Error};
+
+use crate::register_action::register_action;
+
+pub fn action(input: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(input as DeriveInput);
 
-pub fn action(_attr: TokenStream, item: TokenStream) -> TokenStream {
-    let input = parse_macro_input!(item as DeriveInput);
     let name = &input.ident;
-    let attrs = input
-        .attrs
-        .into_iter()
-        .filter(|attr| !attr.path.is_ident("action"))
-        .collect::<Vec<_>>();
-
-    let attributes = quote! {
-        #[gpui::register_action]
-        #[derive(gpui::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone, std::default::Default, std::fmt::Debug)]
-        #(#attrs)*
+
+    if input.generics.lt_token.is_some() {
+        return Error::new(name.span(), "Actions must be a concrete type")
+            .into_compile_error()
+            .into();
+    }
+
+    let is_unit_struct = match input.data {
+        syn::Data::Struct(struct_data) => struct_data.fields.is_empty(),
+        syn::Data::Enum(_) => false,
+        syn::Data::Union(_) => false,
+    };
+
+    let build_impl = if is_unit_struct {
+        quote! {
+            Ok(std::boxed::Box::new(Self {}))
+        }
+    } else {
+        quote! {
+            Ok(std::boxed::Box::new(gpui::serde_json::from_value::<Self>(value)?))
+        }
     };
-    let visibility = input.vis;
-
-    let output = match input.data {
-        syn::Data::Struct(ref struct_data) => match &struct_data.fields {
-            syn::Fields::Named(_) | syn::Fields::Unnamed(_) => {
-                let fields = &struct_data.fields;
-                quote! {
-                    #attributes
-                    #visibility struct #name #fields
-                }
+
+    let register_action = register_action(&name);
+
+    let output = quote! {
+        const _: fn() = || {
+            fn assert_impl<T: ?Sized + for<'a> gpui::serde::Deserialize<'a> +  ::std::cmp::PartialEq + ::std::clone::Clone>() {}
+            assert_impl::<#name>();
+        };
+
+        impl gpui::Action for #name {
+            fn name(&self) -> &'static str
+            {
+                ::std::any::type_name::<#name>()
+            }
+
+            fn debug_name() -> &'static str
+            where
+                Self: ::std::marker::Sized
+            {
+                ::std::any::type_name::<#name>()
+            }
+
+            fn build(value: gpui::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>>
+            where
+                Self: ::std::marker::Sized {
+                    #build_impl
             }
-            syn::Fields::Unit => {
-                quote! {
-                    #attributes
-                    #visibility struct #name;
-                }
+
+            fn partial_eq(&self, action: &dyn gpui::Action) -> bool {
+                action
+                    .as_any()
+                    .downcast_ref::<Self>()
+                    .map_or(false, |a| self == a)
             }
-        },
-        syn::Data::Enum(ref enum_data) => {
-            let variants = &enum_data.variants;
-            quote! {
-                #attributes
-                #visibility enum #name { #variants }
+
+            fn boxed_clone(&self) ->  std::boxed::Box<dyn gpui::Action> {
+                ::std::boxed::Box::new(self.clone())
+            }
+
+            fn as_any(&self) -> &dyn ::std::any::Any {
+                self
             }
         }
-        _ => panic!("Expected a struct or an enum."),
+
+        #register_action
     };
 
     TokenStream::from(output)

crates/gpui2_macros/src/gpui2_macros.rs 🔗

@@ -11,14 +11,14 @@ pub fn style_helpers(args: TokenStream) -> TokenStream {
     style_helpers::style_helpers(args)
 }
 
-#[proc_macro_attribute]
-pub fn action(attr: TokenStream, item: TokenStream) -> TokenStream {
-    action::action(attr, item)
+#[proc_macro_derive(Action)]
+pub fn action(input: TokenStream) -> TokenStream {
+    action::action(input)
 }
 
 #[proc_macro_attribute]
 pub fn register_action(attr: TokenStream, item: TokenStream) -> TokenStream {
-    register_action::register_action(attr, item)
+    register_action::register_action_macro(attr, item)
 }
 
 #[proc_macro_derive(Component, attributes(component))]

crates/gpui2_macros/src/register_action.rs 🔗

@@ -12,22 +12,76 @@
 //     gpui2::register_action_builder::<Foo>()
 // }
 use proc_macro::TokenStream;
+use proc_macro2::Ident;
 use quote::{format_ident, quote};
-use syn::{parse_macro_input, DeriveInput};
+use syn::{parse_macro_input, DeriveInput, Error};
 
-pub fn register_action(_attr: TokenStream, item: TokenStream) -> TokenStream {
+pub fn register_action_macro(_attr: TokenStream, item: TokenStream) -> TokenStream {
     let input = parse_macro_input!(item as DeriveInput);
-    let type_name = &input.ident;
-    let ctor_fn_name = format_ident!("register_{}_builder", type_name.to_string().to_lowercase());
+    let registration = register_action(&input.ident);
 
-    let expanded = quote! {
+    let has_action_derive = input
+        .attrs
+        .iter()
+        .find(|attr| {
+            (|| {
+                let meta = attr.parse_meta().ok()?;
+                meta.path().is_ident("derive").then(|| match meta {
+                    syn::Meta::Path(_) => None,
+                    syn::Meta::NameValue(_) => None,
+                    syn::Meta::List(list) => list
+                        .nested
+                        .iter()
+                        .find(|list| match list {
+                            syn::NestedMeta::Meta(meta) => meta.path().is_ident("Action"),
+                            syn::NestedMeta::Lit(_) => false,
+                        })
+                        .map(|_| true),
+                })?
+            })()
+            .unwrap_or(false)
+        })
+        .is_some();
+
+    if has_action_derive {
+        return Error::new(
+            input.ident.span(),
+            "The Action derive macro has already registered this action",
+        )
+        .into_compile_error()
+        .into();
+    }
+
+    TokenStream::from(quote! {
         #input
-        #[allow(non_snake_case)]
-        #[gpui::ctor]
-        fn #ctor_fn_name() {
-            gpui::register_action::<#type_name>()
-        }
-    };
 
-    TokenStream::from(expanded)
+        #registration
+    })
+}
+
+pub(crate) fn register_action(type_name: &Ident) -> proc_macro2::TokenStream {
+    let static_slice_name =
+        format_ident!("__GPUI_ACTIONS_{}", type_name.to_string().to_uppercase());
+
+    let action_builder_fn_name = format_ident!(
+        "__gpui_actions_builder_{}",
+        type_name.to_string().to_lowercase()
+    );
+
+    quote! {
+        #[doc(hidden)]
+        #[gpui::linkme::distributed_slice(gpui::__GPUI_ACTIONS)]
+        #[linkme(crate = gpui::linkme)]
+        static #static_slice_name: gpui::MacroActionBuilder = #action_builder_fn_name;
+
+        /// This is an auto generated function, do not use.
+        #[doc(hidden)]
+        fn #action_builder_fn_name() -> gpui::ActionData {
+            gpui::ActionData {
+                name: ::std::any::type_name::<#type_name>(),
+                type_id: ::std::any::TypeId::of::<#type_name>(),
+                build: <#type_name as gpui::Action>::build,
+            }
+        }
+    }
 }

crates/language/src/buffer.rs 🔗

@@ -17,7 +17,7 @@ use crate::{
 };
 use anyhow::{anyhow, Result};
 pub use clock::ReplicaId;
-use futures::FutureExt as _;
+use futures::channel::oneshot;
 use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task};
 use lsp::LanguageServerId;
 use parking_lot::Mutex;
@@ -45,7 +45,7 @@ pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, *};
 use theme::SyntaxTheme;
 #[cfg(any(test, feature = "test-support"))]
 use util::RandomCharIter;
-use util::{RangeExt, TryFutureExt as _};
+use util::RangeExt;
 
 #[cfg(any(test, feature = "test-support"))]
 pub use {tree_sitter_rust, tree_sitter_typescript};
@@ -62,6 +62,7 @@ pub struct Buffer {
     saved_mtime: SystemTime,
     transaction_depth: usize,
     was_dirty_before_starting_transaction: Option<bool>,
+    reload_task: Option<Task<Result<()>>>,
     language: Option<Arc<Language>>,
     autoindent_requests: Vec<Arc<AutoindentRequest>>,
     pending_autoindent: Option<Task<()>>,
@@ -509,6 +510,7 @@ impl Buffer {
             saved_mtime,
             saved_version: buffer.version(),
             saved_version_fingerprint: buffer.as_rope().fingerprint(),
+            reload_task: None,
             transaction_depth: 0,
             was_dirty_before_starting_transaction: None,
             text: buffer,
@@ -608,37 +610,52 @@ impl Buffer {
         cx.notify();
     }
 
-    pub fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<Option<Transaction>>> {
-        cx.spawn(|this, mut cx| async move {
-            if let Some((new_mtime, new_text)) = this.read_with(&cx, |this, cx| {
+    pub fn reload(
+        &mut self,
+        cx: &mut ModelContext<Self>,
+    ) -> oneshot::Receiver<Option<Transaction>> {
+        let (tx, rx) = futures::channel::oneshot::channel();
+        let prev_version = self.text.version();
+        self.reload_task = Some(cx.spawn(|this, mut cx| async move {
+            let Some((new_mtime, new_text)) = this.update(&mut cx, |this, cx| {
                 let file = this.file.as_ref()?.as_local()?;
                 Some((file.mtime(), file.load(cx)))
-            }) {
-                let new_text = new_text.await?;
-                let diff = this
-                    .read_with(&cx, |this, cx| this.diff(new_text, cx))
-                    .await;
-                this.update(&mut cx, |this, cx| {
-                    if this.version() == diff.base_version {
-                        this.finalize_last_transaction();
-                        this.apply_diff(diff, cx);
-                        if let Some(transaction) = this.finalize_last_transaction().cloned() {
-                            this.did_reload(
-                                this.version(),
-                                this.as_rope().fingerprint(),
-                                this.line_ending(),
-                                new_mtime,
-                                cx,
-                            );
-                            return Ok(Some(transaction));
-                        }
-                    }
-                    Ok(None)
-                })
-            } else {
-                Ok(None)
-            }
-        })
+            }) else {
+                return Ok(());
+            };
+
+            let new_text = new_text.await?;
+            let diff = this
+                .update(&mut cx, |this, cx| this.diff(new_text.clone(), cx))
+                .await;
+            this.update(&mut cx, |this, cx| {
+                if this.version() == diff.base_version {
+                    this.finalize_last_transaction();
+                    this.apply_diff(diff, cx);
+                    tx.send(this.finalize_last_transaction().cloned()).ok();
+
+                    this.did_reload(
+                        this.version(),
+                        this.as_rope().fingerprint(),
+                        this.line_ending(),
+                        new_mtime,
+                        cx,
+                    );
+                } else {
+                    this.did_reload(
+                        prev_version,
+                        Rope::text_fingerprint(&new_text),
+                        this.line_ending(),
+                        this.saved_mtime,
+                        cx,
+                    );
+                }
+
+                this.reload_task.take();
+            });
+            Ok(())
+        }));
+        rx
     }
 
     pub fn did_reload(
@@ -667,13 +684,8 @@ impl Buffer {
         cx.notify();
     }
 
-    pub fn file_updated(
-        &mut self,
-        new_file: Arc<dyn File>,
-        cx: &mut ModelContext<Self>,
-    ) -> Task<()> {
+    pub fn file_updated(&mut self, new_file: Arc<dyn File>, cx: &mut ModelContext<Self>) {
         let mut file_changed = false;
-        let mut task = Task::ready(());
 
         if let Some(old_file) = self.file.as_ref() {
             if new_file.path() != old_file.path() {
@@ -693,8 +705,7 @@ impl Buffer {
                     file_changed = true;
 
                     if !self.is_dirty() {
-                        let reload = self.reload(cx).log_err().map(drop);
-                        task = cx.foreground().spawn(reload);
+                        self.reload(cx).close();
                     }
                 }
             }
@@ -708,7 +719,6 @@ impl Buffer {
             cx.emit(Event::FileHandleChanged);
             cx.notify();
         }
-        task
     }
 
     pub fn diff_base(&self) -> Option<&str> {

crates/language2/src/buffer.rs 🔗

@@ -16,8 +16,9 @@ use crate::{
 };
 use anyhow::{anyhow, Result};
 pub use clock::ReplicaId;
-use futures::FutureExt as _;
-use gpui::{AppContext, EventEmitter, HighlightStyle, ModelContext, Task};
+use futures::channel::oneshot;
+use gpui::{AppContext, EventEmitter, HighlightStyle, ModelContext, Task, TaskLabel};
+use lazy_static::lazy_static;
 use lsp::LanguageServerId;
 use parking_lot::Mutex;
 use similar::{ChangeTag, TextDiff};
@@ -44,23 +45,33 @@ pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, *};
 use theme::SyntaxTheme;
 #[cfg(any(test, feature = "test-support"))]
 use util::RandomCharIter;
-use util::{RangeExt, TryFutureExt as _};
+use util::RangeExt;
 
 #[cfg(any(test, feature = "test-support"))]
 pub use {tree_sitter_rust, tree_sitter_typescript};
 
 pub use lsp::DiagnosticSeverity;
 
+lazy_static! {
+    pub static ref BUFFER_DIFF_TASK: TaskLabel = TaskLabel::new();
+}
+
 pub struct Buffer {
     text: TextBuffer,
     diff_base: Option<String>,
     git_diff: git::diff::BufferDiff,
     file: Option<Arc<dyn File>>,
-    saved_version: clock::Global,
-    saved_version_fingerprint: RopeFingerprint,
+    /// The mtime of the file when this buffer was last loaded from
+    /// or saved to disk.
     saved_mtime: SystemTime,
+    /// The version vector when this buffer was last loaded from
+    /// or saved to disk.
+    saved_version: clock::Global,
+    /// A hash of the current contents of the buffer's file.
+    file_fingerprint: RopeFingerprint,
     transaction_depth: usize,
     was_dirty_before_starting_transaction: Option<bool>,
+    reload_task: Option<Task<Result<()>>>,
     language: Option<Arc<Language>>,
     autoindent_requests: Vec<Arc<AutoindentRequest>>,
     pending_autoindent: Option<Task<()>>,
@@ -380,8 +391,7 @@ impl Buffer {
                 .ok_or_else(|| anyhow!("missing line_ending"))?,
         ));
         this.saved_version = proto::deserialize_version(&message.saved_version);
-        this.saved_version_fingerprint =
-            proto::deserialize_fingerprint(&message.saved_version_fingerprint)?;
+        this.file_fingerprint = proto::deserialize_fingerprint(&message.saved_version_fingerprint)?;
         this.saved_mtime = message
             .saved_mtime
             .ok_or_else(|| anyhow!("invalid saved_mtime"))?
@@ -397,7 +407,7 @@ impl Buffer {
             diff_base: self.diff_base.as_ref().map(|h| h.to_string()),
             line_ending: proto::serialize_line_ending(self.line_ending()) as i32,
             saved_version: proto::serialize_version(&self.saved_version),
-            saved_version_fingerprint: proto::serialize_fingerprint(self.saved_version_fingerprint),
+            saved_version_fingerprint: proto::serialize_fingerprint(self.file_fingerprint),
             saved_mtime: Some(self.saved_mtime.into()),
         }
     }
@@ -467,7 +477,8 @@ impl Buffer {
         Self {
             saved_mtime,
             saved_version: buffer.version(),
-            saved_version_fingerprint: buffer.as_rope().fingerprint(),
+            file_fingerprint: buffer.as_rope().fingerprint(),
+            reload_task: None,
             transaction_depth: 0,
             was_dirty_before_starting_transaction: None,
             text: buffer,
@@ -533,7 +544,7 @@ impl Buffer {
     }
 
     pub fn saved_version_fingerprint(&self) -> RopeFingerprint {
-        self.saved_version_fingerprint
+        self.file_fingerprint
     }
 
     pub fn saved_mtime(&self) -> SystemTime {
@@ -561,43 +572,58 @@ impl Buffer {
         cx: &mut ModelContext<Self>,
     ) {
         self.saved_version = version;
-        self.saved_version_fingerprint = fingerprint;
+        self.file_fingerprint = fingerprint;
         self.saved_mtime = mtime;
         cx.emit(Event::Saved);
         cx.notify();
     }
 
-    pub fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<Option<Transaction>>> {
-        cx.spawn(|this, mut cx| async move {
-            if let Some((new_mtime, new_text)) = this.update(&mut cx, |this, cx| {
+    pub fn reload(
+        &mut self,
+        cx: &mut ModelContext<Self>,
+    ) -> oneshot::Receiver<Option<Transaction>> {
+        let (tx, rx) = futures::channel::oneshot::channel();
+        let prev_version = self.text.version();
+        self.reload_task = Some(cx.spawn(|this, mut cx| async move {
+            let Some((new_mtime, new_text)) = this.update(&mut cx, |this, cx| {
                 let file = this.file.as_ref()?.as_local()?;
                 Some((file.mtime(), file.load(cx)))
-            })? {
-                let new_text = new_text.await?;
-                let diff = this
-                    .update(&mut cx, |this, cx| this.diff(new_text, cx))?
-                    .await;
-                this.update(&mut cx, |this, cx| {
-                    if this.version() == diff.base_version {
-                        this.finalize_last_transaction();
-                        this.apply_diff(diff, cx);
-                        if let Some(transaction) = this.finalize_last_transaction().cloned() {
-                            this.did_reload(
-                                this.version(),
-                                this.as_rope().fingerprint(),
-                                this.line_ending(),
-                                new_mtime,
-                                cx,
-                            );
-                            return Some(transaction);
-                        }
-                    }
-                    None
-                })
-            } else {
-                Ok(None)
-            }
-        })
+            })?
+            else {
+                return Ok(());
+            };
+
+            let new_text = new_text.await?;
+            let diff = this
+                .update(&mut cx, |this, cx| this.diff(new_text.clone(), cx))?
+                .await;
+            this.update(&mut cx, |this, cx| {
+                if this.version() == diff.base_version {
+                    this.finalize_last_transaction();
+                    this.apply_diff(diff, cx);
+                    tx.send(this.finalize_last_transaction().cloned()).ok();
+
+                    this.did_reload(
+                        this.version(),
+                        this.as_rope().fingerprint(),
+                        this.line_ending(),
+                        new_mtime,
+                        cx,
+                    );
+                } else {
+                    this.did_reload(
+                        prev_version,
+                        Rope::text_fingerprint(&new_text),
+                        this.line_ending(),
+                        this.saved_mtime,
+                        cx,
+                    );
+                }
+
+                this.reload_task.take();
+            })
+        }));
+        rx
     }
 
     pub fn did_reload(
@@ -609,14 +635,14 @@ impl Buffer {
         cx: &mut ModelContext<Self>,
     ) {
         self.saved_version = version;
-        self.saved_version_fingerprint = fingerprint;
+        self.file_fingerprint = fingerprint;
         self.text.set_line_ending(line_ending);
         self.saved_mtime = mtime;
         if let Some(file) = self.file.as_ref().and_then(|f| f.as_local()) {
             file.buffer_reloaded(
                 self.remote_id(),
                 &self.saved_version,
-                self.saved_version_fingerprint,
+                self.file_fingerprint,
                 self.line_ending(),
                 self.saved_mtime,
                 cx,
@@ -626,13 +652,8 @@ impl Buffer {
         cx.notify();
     }
 
-    pub fn file_updated(
-        &mut self,
-        new_file: Arc<dyn File>,
-        cx: &mut ModelContext<Self>,
-    ) -> Task<()> {
+    pub fn file_updated(&mut self, new_file: Arc<dyn File>, cx: &mut ModelContext<Self>) {
         let mut file_changed = false;
-        let mut task = Task::ready(());
 
         if let Some(old_file) = self.file.as_ref() {
             if new_file.path() != old_file.path() {
@@ -652,8 +673,7 @@ impl Buffer {
                     file_changed = true;
 
                     if !self.is_dirty() {
-                        let reload = self.reload(cx).log_err().map(drop);
-                        task = cx.background_executor().spawn(reload);
+                        self.reload(cx).close();
                     }
                 }
             }
@@ -667,7 +687,6 @@ impl Buffer {
             cx.emit(Event::FileHandleChanged);
             cx.notify();
         }
-        task
     }
 
     pub fn diff_base(&self) -> Option<&str> {
@@ -1118,36 +1137,72 @@ impl Buffer {
     pub fn diff(&self, mut new_text: String, cx: &AppContext) -> Task<Diff> {
         let old_text = self.as_rope().clone();
         let base_version = self.version();
-        cx.background_executor().spawn(async move {
-            let old_text = old_text.to_string();
-            let line_ending = LineEnding::detect(&new_text);
-            LineEnding::normalize(&mut new_text);
-            let diff = TextDiff::from_chars(old_text.as_str(), new_text.as_str());
-            let mut edits = Vec::new();
-            let mut offset = 0;
-            let empty: Arc<str> = "".into();
-            for change in diff.iter_all_changes() {
-                let value = change.value();
-                let end_offset = offset + value.len();
-                match change.tag() {
-                    ChangeTag::Equal => {
-                        offset = end_offset;
-                    }
-                    ChangeTag::Delete => {
-                        edits.push((offset..end_offset, empty.clone()));
-                        offset = end_offset;
+        cx.background_executor()
+            .spawn_labeled(*BUFFER_DIFF_TASK, async move {
+                let old_text = old_text.to_string();
+                let line_ending = LineEnding::detect(&new_text);
+                LineEnding::normalize(&mut new_text);
+
+                let diff = TextDiff::from_chars(old_text.as_str(), new_text.as_str());
+                let empty: Arc<str> = "".into();
+
+                let mut edits = Vec::new();
+                let mut old_offset = 0;
+                let mut new_offset = 0;
+                let mut last_edit: Option<(Range<usize>, Range<usize>)> = None;
+                for change in diff.iter_all_changes().map(Some).chain([None]) {
+                    if let Some(change) = &change {
+                        let len = change.value().len();
+                        match change.tag() {
+                            ChangeTag::Equal => {
+                                old_offset += len;
+                                new_offset += len;
+                            }
+                            ChangeTag::Delete => {
+                                let old_end_offset = old_offset + len;
+                                if let Some((last_old_range, _)) = &mut last_edit {
+                                    last_old_range.end = old_end_offset;
+                                } else {
+                                    last_edit =
+                                        Some((old_offset..old_end_offset, new_offset..new_offset));
+                                }
+                                old_offset = old_end_offset;
+                            }
+                            ChangeTag::Insert => {
+                                let new_end_offset = new_offset + len;
+                                if let Some((_, last_new_range)) = &mut last_edit {
+                                    last_new_range.end = new_end_offset;
+                                } else {
+                                    last_edit =
+                                        Some((old_offset..old_offset, new_offset..new_end_offset));
+                                }
+                                new_offset = new_end_offset;
+                            }
+                        }
                     }
-                    ChangeTag::Insert => {
-                        edits.push((offset..offset, value.into()));
+
+                    if let Some((old_range, new_range)) = &last_edit {
+                        if old_offset > old_range.end
+                            || new_offset > new_range.end
+                            || change.is_none()
+                        {
+                            let text = if new_range.is_empty() {
+                                empty.clone()
+                            } else {
+                                new_text[new_range.clone()].into()
+                            };
+                            edits.push((old_range.clone(), text));
+                            last_edit.take();
+                        }
                     }
                 }
-            }
-            Diff {
-                base_version,
-                line_ending,
-                edits,
-            }
-        })
+
+                Diff {
+                    base_version,
+                    line_ending,
+                    edits,
+                }
+            })
     }
 
     /// Spawn a background task that searches the buffer for any whitespace
@@ -1231,12 +1286,12 @@ impl Buffer {
     }
 
     pub fn is_dirty(&self) -> bool {
-        self.saved_version_fingerprint != self.as_rope().fingerprint()
+        self.file_fingerprint != self.as_rope().fingerprint()
             || self.file.as_ref().map_or(false, |file| file.is_deleted())
     }
 
     pub fn has_conflict(&self) -> bool {
-        self.saved_version_fingerprint != self.as_rope().fingerprint()
+        self.file_fingerprint != self.as_rope().fingerprint()
             && self
                 .file
                 .as_ref()

crates/live_kit_client2/examples/test_app.rs → crates/live_kit_client2/examples/test_app2.rs 🔗

@@ -1,7 +1,7 @@
 use std::{sync::Arc, time::Duration};
 
 use futures::StreamExt;
-use gpui::KeyBinding;
+use gpui::{Action, KeyBinding};
 use live_kit_client2::{
     LocalAudioTrack, LocalVideoTrack, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate, Room,
 };
@@ -10,7 +10,7 @@ use log::LevelFilter;
 use serde_derive::Deserialize;
 use simplelog::SimpleLogger;
 
-#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
+#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Action)]
 struct Quit;
 
 fn main() {

crates/picker2/src/picker2.rs 🔗

@@ -1,7 +1,7 @@
 use editor::Editor;
 use gpui::{
-    div, prelude::*, uniform_list, Component, Div, MouseButton, Render, Task,
-    UniformListScrollHandle, View, ViewContext, WindowContext,
+    div, prelude::*, uniform_list, AppContext, Component, Div, FocusHandle, FocusableView,
+    MouseButton, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
 };
 use std::{cmp, sync::Arc};
 use ui::{prelude::*, v_stack, Divider, Label, TextColor};
@@ -35,6 +35,12 @@ pub trait PickerDelegate: Sized + 'static {
     ) -> Self::ListItem;
 }
 
+impl<D: PickerDelegate> FocusableView for Picker<D> {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.editor.focus_handle(cx)
+    }
+}
+
 impl<D: PickerDelegate> Picker<D> {
     pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
         let editor = cx.build_view(|cx| {

crates/project/src/project.rs 🔗

@@ -6190,7 +6190,7 @@ impl Project {
                                 .log_err();
                         }
 
-                        buffer.file_updated(Arc::new(new_file), cx).detach();
+                        buffer.file_updated(Arc::new(new_file), cx);
                     }
                 }
             });
@@ -7182,7 +7182,7 @@ impl Project {
                     .ok_or_else(|| anyhow!("no such worktree"))?;
                 let file = File::from_proto(file, worktree, cx)?;
                 buffer.update(cx, |buffer, cx| {
-                    buffer.file_updated(Arc::new(file), cx).detach();
+                    buffer.file_updated(Arc::new(file), cx);
                 });
                 this.detect_language_for_buffer(&buffer, cx);
             }

crates/project/src/worktree.rs 🔗

@@ -959,7 +959,7 @@ impl LocalWorktree {
 
                 buffer_handle.update(&mut cx, |buffer, cx| {
                     if has_changed_file {
-                        buffer.file_updated(new_file, cx).detach();
+                        buffer.file_updated(new_file, cx);
                     }
                 });
             }

crates/project2/src/project2.rs 🔗

@@ -6262,7 +6262,7 @@ impl Project {
                                 .log_err();
                         }
 
-                        buffer.file_updated(Arc::new(new_file), cx).detach();
+                        buffer.file_updated(Arc::new(new_file), cx);
                     }
                 }
             });
@@ -7256,7 +7256,7 @@ impl Project {
                     .ok_or_else(|| anyhow!("no such worktree"))?;
                 let file = File::from_proto(file, worktree, cx)?;
                 buffer.update(cx, |buffer, cx| {
-                    buffer.file_updated(Arc::new(file), cx).detach();
+                    buffer.file_updated(Arc::new(file), cx);
                 });
                 this.detect_language_for_buffer(&buffer, cx);
             }

crates/project2/src/project_tests.rs 🔗

@@ -2587,6 +2587,125 @@ async fn test_save_file(cx: &mut gpui::TestAppContext) {
     assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text()));
 }
 
+#[gpui::test(iterations = 30)]
+async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
+    let fs = FakeFs::new(cx.executor().clone());
+    fs.insert_tree(
+        "/dir",
+        json!({
+            "file1": "the original contents",
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+    let worktree = project.read_with(cx, |project, _| project.worktrees().next().unwrap());
+    let buffer = project
+        .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
+        .await
+        .unwrap();
+
+    // Simulate buffer diffs being slow, so that they don't complete before
+    // the next file change occurs.
+    cx.executor().deprioritize(*language::BUFFER_DIFF_TASK);
+
+    // Change the buffer's file on disk, and then wait for the file change
+    // to be detected by the worktree, so that the buffer starts reloading.
+    fs.save(
+        "/dir/file1".as_ref(),
+        &"the first contents".into(),
+        Default::default(),
+    )
+    .await
+    .unwrap();
+    worktree.next_event(cx);
+
+    // Change the buffer's file again. Depending on the random seed, the
+    // previous file change may still be in progress.
+    fs.save(
+        "/dir/file1".as_ref(),
+        &"the second contents".into(),
+        Default::default(),
+    )
+    .await
+    .unwrap();
+    worktree.next_event(cx);
+
+    cx.executor().run_until_parked();
+    let on_disk_text = fs.load(Path::new("/dir/file1")).await.unwrap();
+    buffer.read_with(cx, |buffer, _| {
+        assert_eq!(buffer.text(), on_disk_text);
+        assert!(!buffer.is_dirty(), "buffer should not be dirty");
+        assert!(!buffer.has_conflict(), "buffer should not be dirty");
+    });
+}
+
+#[gpui::test(iterations = 30)]
+async fn test_edit_buffer_while_it_reloads(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
+    let fs = FakeFs::new(cx.executor().clone());
+    fs.insert_tree(
+        "/dir",
+        json!({
+            "file1": "the original contents",
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+    let worktree = project.read_with(cx, |project, _| project.worktrees().next().unwrap());
+    let buffer = project
+        .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
+        .await
+        .unwrap();
+
+    // Simulate buffer diffs being slow, so that they don't complete before
+    // the next file change occurs.
+    cx.executor().deprioritize(*language::BUFFER_DIFF_TASK);
+
+    // Change the buffer's file on disk, and then wait for the file change
+    // to be detected by the worktree, so that the buffer starts reloading.
+    fs.save(
+        "/dir/file1".as_ref(),
+        &"the first contents".into(),
+        Default::default(),
+    )
+    .await
+    .unwrap();
+    worktree.next_event(cx);
+
+    cx.executor()
+        .spawn(cx.executor().simulate_random_delay())
+        .await;
+
+    // Perform a noop edit, causing the buffer's version to increase.
+    buffer.update(cx, |buffer, cx| {
+        buffer.edit([(0..0, " ")], None, cx);
+        buffer.undo(cx);
+    });
+
+    cx.executor().run_until_parked();
+    let on_disk_text = fs.load(Path::new("/dir/file1")).await.unwrap();
+    buffer.read_with(cx, |buffer, _| {
+        let buffer_text = buffer.text();
+        if buffer_text == on_disk_text {
+            assert!(
+                !buffer.is_dirty() && !buffer.has_conflict(),
+                "buffer shouldn't be dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}",
+            );
+        }
+        // If the file change occurred while the buffer was processing the first
+        // change, the buffer will be in a conflicting state.
+        else {
+            assert!(buffer.is_dirty(), "buffer should report that it is dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}");
+            assert!(buffer.has_conflict(), "buffer should report that it is dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}");
+        }
+    });
+}
+
 #[gpui::test]
 async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
     init_test(cx);
@@ -4017,7 +4136,7 @@ async fn search(
 
 fn init_test(cx: &mut gpui::TestAppContext) {
     if std::env::var("RUST_LOG").is_ok() {
-        env_logger::init();
+        env_logger::try_init().ok();
     }
 
     cx.update(|cx| {

crates/project2/src/worktree.rs 🔗

@@ -276,6 +276,7 @@ struct ShareState {
     _maintain_remote_snapshot: Task<Option<()>>,
 }
 
+#[derive(Clone)]
 pub enum Event {
     UpdatedEntries(UpdatedEntriesSet),
     UpdatedGitRepositories(UpdatedGitRepositoriesSet),
@@ -961,7 +962,7 @@ impl LocalWorktree {
 
                 buffer_handle.update(&mut cx, |buffer, cx| {
                     if has_changed_file {
-                        buffer.file_updated(new_file, cx).detach();
+                        buffer.file_updated(new_file, cx);
                     }
                 })?;
             }

crates/project_panel2/src/project_panel.rs 🔗

@@ -9,10 +9,10 @@ use file_associations::FileAssociations;
 use anyhow::{anyhow, Result};
 use gpui::{
     actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
-    ClipboardItem, Component, Div, EventEmitter, FocusHandle, Focusable, InteractiveComponent,
-    Model, MouseButton, ParentComponent, Pixels, Point, PromptLevel, Render, Stateful,
-    StatefulInteractiveComponent, Styled, Task, UniformListScrollHandle, View, ViewContext,
-    VisualContext as _, WeakView, WindowContext,
+    ClipboardItem, Component, Div, EventEmitter, FocusHandle, Focusable, FocusableView,
+    InteractiveComponent, Model, MouseButton, ParentComponent, Pixels, Point, PromptLevel, Render,
+    Stateful, StatefulInteractiveComponent, Styled, Task, UniformListScrollHandle, View,
+    ViewContext, VisualContext as _, WeakView, WindowContext,
 };
 use menu::{Confirm, SelectNext, SelectPrev};
 use project::{
@@ -32,7 +32,7 @@ use std::{
 use theme::ActiveTheme as _;
 use ui::{h_stack, v_stack, IconElement, Label};
 use unicase::UniCase;
-use util::{maybe, TryFutureExt};
+use util::{maybe, ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, PanelEvent},
     Workspace,
@@ -130,6 +130,13 @@ pub fn init_settings(cx: &mut AppContext) {
 pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
     init_settings(cx);
     file_associations::init(assets, cx);
+
+    cx.observe_new_views(|workspace: &mut Workspace, _| {
+        workspace.register_action(|workspace, _: &ToggleFocus, cx| {
+            workspace.toggle_panel_focus::<ProjectPanel>(cx);
+        });
+    })
+    .detach();
 }
 
 #[derive(Debug)]
@@ -304,32 +311,31 @@ impl ProjectPanel {
         project_panel
     }
 
-    pub fn load(
+    pub async fn load(
         workspace: WeakView<Workspace>,
-        cx: AsyncWindowContext,
-    ) -> Task<Result<View<Self>>> {
-        cx.spawn(|mut cx| async move {
-            // let serialized_panel = if let Some(panel) = cx
-            //     .background_executor()
-            //     .spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) })
-            //     .await
-            //     .log_err()
-            //     .flatten()
-            // {
-            //     Some(serde_json::from_str::<SerializedProjectPanel>(&panel)?)
-            // } else {
-            //     None
-            // };
-            workspace.update(&mut cx, |workspace, cx| {
-                let panel = ProjectPanel::new(workspace, cx);
-                // if let Some(serialized_panel) = serialized_panel {
-                //     panel.update(cx, |panel, cx| {
-                //         panel.width = serialized_panel.width;
-                //         cx.notify();
-                //     });
-                // }
-                panel
-            })
+        mut cx: AsyncWindowContext,
+    ) -> Result<View<Self>> {
+        let serialized_panel = cx
+            .background_executor()
+            .spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) })
+            .await
+            .map_err(|e| anyhow!("Failed to load project panel: {}", e))
+            .log_err()
+            .flatten()
+            .map(|panel| serde_json::from_str::<SerializedProjectPanel>(&panel))
+            .transpose()
+            .log_err()
+            .flatten();
+
+        workspace.update(&mut cx, |workspace, cx| {
+            let panel = ProjectPanel::new(workspace, cx);
+            if let Some(serialized_panel) = serialized_panel {
+                panel.update(cx, |panel, cx| {
+                    panel.width = serialized_panel.width;
+                    cx.notify();
+                });
+            }
+            panel
         })
     }
 
@@ -1517,33 +1523,27 @@ impl workspace::dock::Panel for ProjectPanel {
         cx.notify();
     }
 
-    fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
-        Some("icons/project.svg")
+    fn icon(&self, _: &WindowContext) -> Option<ui::Icon> {
+        Some(ui::Icon::FileTree)
     }
 
-    fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
-        ("Project Panel".into(), Some(Box::new(ToggleFocus)))
+    fn toggle_action(&self) -> Box<dyn Action> {
+        Box::new(ToggleFocus)
     }
 
-    // fn should_change_position_on_event(event: &Self::Event) -> bool {
-    //     matches!(event, Event::DockPositionChanged)
-    // }
-
     fn has_focus(&self, _: &WindowContext) -> bool {
         self.has_focus
     }
 
-    fn persistent_name(&self) -> &'static str {
+    fn persistent_name() -> &'static str {
         "Project Panel"
     }
+}
 
-    fn focus_handle(&self, _cx: &WindowContext) -> FocusHandle {
+impl FocusableView for ProjectPanel {
+    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
         self.focus_handle.clone()
     }
-
-    // fn is_focus_event(event: &Self::Event) -> bool {
-    //     matches!(event, Event::Focus)
-    // }
 }
 
 impl ClipboardEntry {
@@ -1580,7 +1580,7 @@ mod tests {
         path::{Path, PathBuf},
         sync::atomic::{self, AtomicUsize},
     };
-    use workspace::{pane, AppState};
+    use workspace::AppState;
 
     #[gpui::test]
     async fn test_visible_list(cx: &mut gpui::TestAppContext) {
@@ -2786,7 +2786,7 @@ mod tests {
             let settings_store = SettingsStore::test(cx);
             cx.set_global(settings_store);
             init_settings(cx);
-            theme::init(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             language::init(cx);
             editor::init_settings(cx);
             crate::init((), cx);
@@ -2799,11 +2799,10 @@ mod tests {
     fn init_test_with_editor(cx: &mut TestAppContext) {
         cx.update(|cx| {
             let app_state = AppState::test(cx);
-            theme::init(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             init_settings(cx);
             language::init(cx);
             editor::init(cx);
-            pane::init(cx);
             crate::init((), cx);
             workspace::init(app_state.clone(), cx);
             Project::init_settings(cx);

crates/rope/src/rope.rs 🔗

@@ -41,6 +41,10 @@ impl Rope {
         Self::default()
     }
 
+    pub fn text_fingerprint(text: &str) -> RopeFingerprint {
+        bromberg_sl2::hash_strict(text.as_bytes())
+    }
+
     pub fn append(&mut self, rope: Rope) {
         let mut chunks = rope.chunks.cursor::<()>();
         chunks.next(&());
@@ -931,7 +935,7 @@ impl<'a> From<&'a str> for ChunkSummary {
     fn from(text: &'a str) -> Self {
         Self {
             text: TextSummary::from(text),
-            fingerprint: bromberg_sl2::hash_strict(text.as_bytes()),
+            fingerprint: Rope::text_fingerprint(text),
         }
     }
 }

crates/rope2/src/rope2.rs 🔗

@@ -41,6 +41,10 @@ impl Rope {
         Self::default()
     }
 
+    pub fn text_fingerprint(text: &str) -> RopeFingerprint {
+        bromberg_sl2::hash_strict(text.as_bytes())
+    }
+
     pub fn append(&mut self, rope: Rope) {
         let mut chunks = rope.chunks.cursor::<()>();
         chunks.next(&());
@@ -931,7 +935,7 @@ impl<'a> From<&'a str> for ChunkSummary {
     fn from(text: &'a str) -> Self {
         Self {
             text: TextSummary::from(text),
-            fingerprint: bromberg_sl2::hash_strict(text.as_bytes()),
+            fingerprint: Rope::text_fingerprint(text),
         }
     }
 }

crates/settings2/src/keymap_file.rs 🔗

@@ -9,7 +9,7 @@ use schemars::{
 };
 use serde::Deserialize;
 use serde_json::Value;
-use util::asset_str;
+use util::{asset_str, ResultExt};
 
 #[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
 #[serde(transparent)]
@@ -73,9 +73,9 @@ impl KeymapFile {
                                     "Expected first item in array to be a string."
                                 )));
                             };
-                            gpui::build_action(&name, Some(data))
+                            cx.build_action(&name, Some(data))
                         }
-                        Value::String(name) => gpui::build_action(&name, None),
+                        Value::String(name) => cx.build_action(&name, None),
                         Value::Null => Ok(no_action()),
                         _ => {
                             return Some(Err(anyhow!("Expected two-element array, got {action:?}")))
@@ -86,9 +86,7 @@ impl KeymapFile {
                             "invalid binding value for keystroke {keystroke}, context {context:?}"
                         )
                     })
-                    // todo!()
-                    .ok()
-                    // .log_err()
+                    .log_err()
                     .map(|action| KeyBinding::load(&keystroke, action, context.as_deref()))
                 })
                 .collect::<Result<Vec<_>>>()?;

crates/settings2/src/settings_file.rs 🔗

@@ -16,6 +16,9 @@ pub fn test_settings() -> String {
     .unwrap();
     util::merge_non_null_json_value_into(
         serde_json::json!({
+            "ui_font_family": "Courier",
+            "ui_font_features": {},
+            "ui_font_size": 14,
             "buffer_font_family": "Courier",
             "buffer_font_features": {},
             "buffer_font_size": 14,

crates/sqlez/src/bindable.rs 🔗

@@ -164,6 +164,23 @@ impl Column for i64 {
     }
 }
 
+impl StaticColumnCount for u64 {}
+impl Bind for u64 {
+    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+        statement
+            .bind_int64(start_index, (*self) as i64)
+            .with_context(|| format!("Failed to bind i64 at index {start_index}"))?;
+        Ok(start_index + 1)
+    }
+}
+
+impl Column for u64 {
+    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+        let result = statement.column_int64(start_index)? as u64;
+        Ok((result, start_index + 1))
+    }
+}
+
 impl StaticColumnCount for u32 {}
 impl Bind for u32 {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {

crates/storybook2/src/stories/focus.rs 🔗

@@ -57,7 +57,6 @@ impl Render for FocusStory {
             .size_full()
             .bg(color_1)
             .focus(|style| style.bg(color_2))
-            .focus_in(|style| style.bg(color_3))
             .child(
                 div()
                     .track_focus(&self.child_1_focus)

crates/storybook2/src/stories/scroll.rs 🔗

@@ -1,5 +1,6 @@
 use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext};
 use theme2::ActiveTheme;
+use ui::Tooltip;
 
 pub struct ScrollStory;
 
@@ -35,16 +36,18 @@ impl Render for ScrollStory {
                         } else {
                             color_2
                         };
-                        div().id(id).bg(bg).size(px(100. as f32)).when(
-                            row >= 5 && column >= 5,
-                            |d| {
+                        div()
+                            .id(id)
+                            .tooltip(move |_, cx| Tooltip::text(format!("{}, {}", row, column), cx))
+                            .bg(bg)
+                            .size(px(100. as f32))
+                            .when(row >= 5 && column >= 5, |d| {
                                 d.overflow_scroll()
                                     .child(div().size(px(50.)).bg(color_1))
                                     .child(div().size(px(50.)).bg(color_2))
                                     .child(div().size(px(50.)).bg(color_1))
                                     .child(div().size(px(50.)).bg(color_2))
-                            },
-                        )
+                            })
                     }))
             }))
     }

crates/storybook2/src/storybook2.rs 🔗

@@ -60,13 +60,12 @@ fn main() {
             .unwrap();
         cx.set_global(store);
 
-        theme2::init(cx);
+        theme2::init(theme2::LoadThemes::All, cx);
 
         let selector =
             story_selector.unwrap_or(StorySelector::Component(ComponentStory::Workspace));
 
         let theme_registry = cx.global::<ThemeRegistry>();
-
         let mut theme_settings = ThemeSettings::get_global(cx).clone();
         theme_settings.active_theme = theme_registry.get(&theme_name).unwrap();
         ThemeSettings::override_global(theme_settings, cx);
@@ -114,6 +113,7 @@ impl Render for StoryWrapper {
             .flex()
             .flex_col()
             .size_full()
+            .font("Zed Mono")
             .child(self.story.clone())
     }
 }

crates/storybook3/Cargo.toml 🔗

@@ -0,0 +1,17 @@
+[package]
+name = "storybook3"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[[bin]]
+name = "storybook"
+path = "src/storybook3.rs"
+
+[dependencies]
+anyhow.workspace = true
+
+gpui = { package = "gpui2", path = "../gpui2" }
+ui = { package = "ui2", path = "../ui2", features = ["stories"] }
+theme = { package = "theme2", path = "../theme2", features = ["stories"] }
+settings = { package = "settings2", path = "../settings2"}

crates/storybook3/src/storybook3.rs 🔗

@@ -0,0 +1,73 @@
+use anyhow::Result;
+use gpui::AssetSource;
+use gpui::{
+    div, px, size, AnyView, Bounds, Div, Render, ViewContext, VisualContext, WindowBounds,
+    WindowOptions,
+};
+use settings::{default_settings, Settings, SettingsStore};
+use std::borrow::Cow;
+use std::sync::Arc;
+use theme::ThemeSettings;
+use ui::{prelude::*, ContextMenuStory};
+
+struct Assets;
+
+impl AssetSource for Assets {
+    fn load(&self, _path: &str) -> Result<Cow<[u8]>> {
+        todo!();
+    }
+
+    fn list(&self, _path: &str) -> Result<Vec<SharedString>> {
+        Ok(vec![])
+    }
+}
+
+fn main() {
+    let asset_source = Arc::new(Assets);
+    gpui::App::production(asset_source).run(move |cx| {
+        let mut store = SettingsStore::default();
+        store
+            .set_default_settings(default_settings().as_ref(), cx)
+            .unwrap();
+        cx.set_global(store);
+        ui::settings::init(cx);
+        theme::init(theme::LoadThemes::JustBase, cx);
+
+        cx.open_window(
+            WindowOptions {
+                bounds: WindowBounds::Fixed(Bounds {
+                    origin: Default::default(),
+                    size: size(px(1500.), px(780.)).into(),
+                }),
+                ..Default::default()
+            },
+            move |cx| {
+                let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
+                cx.set_rem_size(ui_font_size);
+
+                cx.build_view(|cx| TestView {
+                    story: cx.build_view(|_| ContextMenuStory).into(),
+                })
+            },
+        );
+
+        cx.activate(true);
+    })
+}
+
+struct TestView {
+    story: AnyView,
+}
+
+impl Render for TestView {
+    type Element = Div<Self>;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        div()
+            .flex()
+            .flex_col()
+            .size_full()
+            .font("Helvetica")
+            .child(self.story.clone())
+    }
+}

crates/terminal2/src/terminal_settings.rs 🔗

@@ -1,4 +1,4 @@
-use gpui::{AppContext, FontFeatures};
+use gpui::{AppContext, FontFeatures, Pixels};
 use schemars::JsonSchema;
 use serde_derive::{Deserialize, Serialize};
 use std::{collections::HashMap, path::PathBuf};
@@ -15,7 +15,7 @@ pub enum TerminalDockPosition {
 pub struct TerminalSettings {
     pub shell: Shell,
     pub working_directory: WorkingDirectory,
-    font_size: Option<f32>,
+    pub font_size: Option<Pixels>,
     pub font_family: Option<String>,
     pub line_height: TerminalLineHeight,
     pub font_features: Option<FontFeatures>,
@@ -90,14 +90,6 @@ pub struct TerminalSettingsContent {
     pub detect_venv: Option<VenvSettings>,
 }
 
-impl TerminalSettings {
-    // todo!("move to terminal element")
-    // pub fn font_size(&self, cx: &AppContext) -> Option<f32> {
-    //     self.font_size
-    //         .map(|size| theme2::adjusted_font_size(size, cx))
-    // }
-}
-
 impl settings::Settings for TerminalSettings {
     const KEY: Option<&'static str> = Some("terminal");
 

crates/terminal_view2/Cargo.toml 🔗

@@ -0,0 +1,45 @@
+[package]
+name = "terminal_view2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/terminal_view.rs"
+doctest = false
+
+[dependencies]
+editor = { package = "editor2", path = "../editor2" }
+language = { package = "language2", path = "../language2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+project = { package = "project2", path = "../project2" }
+# search = { package = "search2", path = "../search2" }
+settings = { package = "settings2", path = "../settings2" }
+theme = { package = "theme2", path = "../theme2" }
+util = { path = "../util" }
+workspace = { package = "workspace2", path = "../workspace2" }
+db = { package = "db2", path = "../db2" }
+procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false }
+terminal = { package = "terminal2", path = "../terminal2" }
+smallvec.workspace = true
+smol.workspace = true
+mio-extras = "2.0.6"
+futures.workspace = true
+ordered-float.workspace = true
+itertools = "0.10"
+dirs = "4.0.0"
+shellexpand = "2.1.0"
+libc = "0.2"
+anyhow.workspace = true
+thiserror.workspace = true
+lazy_static.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+
+[dev-dependencies]
+editor = { package = "editor2", path = "../editor2", 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"] }
+rand.workspace = true

crates/terminal_view2/README.md 🔗

@@ -0,0 +1,23 @@
+Design notes:
+
+This crate is split into two conceptual halves:
+- The terminal.rs file and the src/mappings/ folder, these contain the code for interacting with Alacritty and maintaining the pty event loop. Some behavior in this file is constrained by terminal protocols and standards. The Zed init function is also placed here.
+- Everything else. These other files integrate the `Terminal` struct created in terminal.rs into the rest of GPUI. The main entry point for GPUI is the terminal_view.rs file and the modal.rs file.
+
+ttys are created externally, and so can fail in unexpected ways. However, GPUI currently does not have an API for models than can fail to instantiate. `TerminalBuilder` solves this by using Rust's type system to split tty instantiation into a 2 step process: first attempt to create the file handles with `TerminalBuilder::new()`, check the result, then call `TerminalBuilder::subscribe(cx)` from within a model context.
+
+The TerminalView struct abstracts over failed and successful terminals, passing focus through to the associated view and allowing clients to build a terminal without worrying about errors.
+
+#Input
+
+There are currently many distinct paths for getting keystrokes to the terminal:
+
+1. Terminal specific characters and bindings. Things like ctrl-a mapping to ASCII control character 1, ANSI escape codes associated with the function keys, etc. These are caught with a raw key-down handler in the element and are processed immediately. This is done with the `try_keystroke()` method on Terminal
+
+2. GPU Action handlers. GPUI clobbers a few vital keys by adding bindings to them in the global context. These keys are synthesized and then dispatched through the same `try_keystroke()` API as the above mappings
+
+3. IME text. When the special character mappings fail, we pass the keystroke back to GPUI to hand it to the IME system. This comes back to us in the `View::replace_text_in_range()` method, and we then send that to the terminal directly, bypassing `try_keystroke()`.
+
+4. Pasted text has a separate pathway.
+
+Generally, there's a distinction between 'keystrokes that need to be mapped' and 'strings which need to be written'. I've attempted to unify these under the '.try_keystroke()' API and the `.input()` API (which try_keystroke uses) so we have consistent input handling across the terminal

crates/terminal_view2/scripts/print256color.sh 🔗

@@ -0,0 +1,96 @@
+#!/bin/bash
+
+# Tom Hale, 2016. MIT Licence.
+# Print out 256 colours, with each number printed in its corresponding colour
+# See http://askubuntu.com/questions/821157/print-a-256-color-test-pattern-in-the-terminal/821163#821163
+
+set -eu # Fail on errors or undeclared variables
+
+printable_colours=256
+
+# Return a colour that contrasts with the given colour
+# Bash only does integer division, so keep it integral
+function contrast_colour {
+    local r g b luminance
+    colour="$1"
+
+    if (( colour < 16 )); then # Initial 16 ANSI colours
+        (( colour == 0 )) && printf "15" || printf "0"
+        return
+    fi
+
+    # Greyscale # rgb_R = rgb_G = rgb_B = (number - 232) * 10 + 8
+    if (( colour > 231 )); then # Greyscale ramp
+        (( colour < 244 )) && printf "15" || printf "0"
+        return
+    fi
+
+    # All other colours:
+    # 6x6x6 colour cube = 16 + 36*R + 6*G + B  # Where RGB are [0..5]
+    # See http://stackoverflow.com/a/27165165/5353461
+
+    # r=$(( (colour-16) / 36 ))
+    g=$(( ((colour-16) % 36) / 6 ))
+    # b=$(( (colour-16) % 6 ))
+
+    # If luminance is bright, print number in black, white otherwise.
+    # Green contributes 587/1000 to human perceived luminance - ITU R-REC-BT.601
+    (( g > 2)) && printf "0" || printf "15"
+    return
+
+    # Uncomment the below for more precise luminance calculations
+
+    # # Calculate perceived brightness
+    # # See https://www.w3.org/TR/AERT#color-contrast
+    # # and http://www.itu.int/rec/R-REC-BT.601
+    # # Luminance is in range 0..5000 as each value is 0..5
+    # luminance=$(( (r * 299) + (g * 587) + (b * 114) ))
+    # (( $luminance > 2500 )) && printf "0" || printf "15"
+}
+
+# Print a coloured block with the number of that colour
+function print_colour {
+    local colour="$1" contrast
+    contrast=$(contrast_colour "$1")
+    printf "\e[48;5;%sm" "$colour"                # Start block of colour
+    printf "\e[38;5;%sm%3d" "$contrast" "$colour" # In contrast, print number
+    printf "\e[0m "                               # Reset colour
+}
+
+# Starting at $1, print a run of $2 colours
+function print_run {
+    local i
+    for (( i = "$1"; i < "$1" + "$2" && i < printable_colours; i++ )) do
+        print_colour "$i"
+    done
+    printf "  "
+}
+
+# Print blocks of colours
+function print_blocks {
+    local start="$1" i
+    local end="$2" # inclusive
+    local block_cols="$3"
+    local block_rows="$4"
+    local blocks_per_line="$5"
+    local block_length=$((block_cols * block_rows))
+
+    # Print sets of blocks
+    for (( i = start; i <= end; i += (blocks_per_line-1) * block_length )) do
+        printf "\n" # Space before each set of blocks
+        # For each block row
+        for (( row = 0; row < block_rows; row++ )) do
+            # Print block columns for all blocks on the line
+            for (( block = 0; block < blocks_per_line; block++ )) do
+                print_run $(( i + (block * block_length) )) "$block_cols"
+            done
+            (( i += block_cols )) # Prepare to print the next row
+            printf "\n"
+        done
+    done
+}
+
+print_run 0 16 # The first 16 colours are spread over the whole spectrum
+printf "\n"
+print_blocks 16 231 6 6 3 # 6x6x6 colour cube between 16 and 231 inclusive
+print_blocks 232 255 12 2 1 # Not 50, but 24 Shades of Grey

crates/terminal_view2/scripts/truecolor.sh 🔗

@@ -0,0 +1,19 @@
+#!/bin/bash
+# Copied from: https://unix.stackexchange.com/a/696756
+# Based on: https://gist.github.com/XVilka/8346728 and https://unix.stackexchange.com/a/404415/395213
+
+awk -v term_cols="${width:-$(tput cols || echo 80)}" -v term_lines="${height:-1}" 'BEGIN{
+    s="/\\";
+    total_cols=term_cols*term_lines;
+    for (colnum = 0; colnum<total_cols; colnum++) {
+        r = 255-(colnum*255/total_cols);
+        g = (colnum*510/total_cols);
+        b = (colnum*255/total_cols);
+        if (g>255) g = 510-g;
+        printf "\033[48;2;%d;%d;%dm", r,g,b;
+        printf "\033[38;2;%d;%d;%dm", 255-r,255-g,255-b;
+        printf "%s\033[0m", substr(s,colnum%2+1,1);
+        if (colnum%term_cols==term_cols) printf "\n";
+    }
+    printf "\n";
+}'

crates/terminal_view2/src/persistence.rs 🔗

@@ -0,0 +1,71 @@
+use std::path::PathBuf;
+
+use db::{define_connection, query, sqlez_macros::sql};
+use workspace::{ItemId, WorkspaceDb, WorkspaceId};
+
+define_connection! {
+    pub static ref TERMINAL_DB: TerminalDb<WorkspaceDb> =
+        &[sql!(
+            CREATE TABLE terminals (
+                workspace_id INTEGER,
+                item_id INTEGER UNIQUE,
+                working_directory BLOB,
+                PRIMARY KEY(workspace_id, item_id),
+                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
+                ON DELETE CASCADE
+            ) STRICT;
+        ),
+        // Remove the unique constraint on the item_id table
+        // SQLite doesn't have a way of doing this automatically, so
+        // we have to do this silly copying.
+        sql!(
+            CREATE TABLE terminals2 (
+                workspace_id INTEGER,
+                item_id INTEGER,
+                working_directory BLOB,
+                PRIMARY KEY(workspace_id, item_id),
+                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
+                ON DELETE CASCADE
+            ) STRICT;
+
+            INSERT INTO terminals2 (workspace_id, item_id, working_directory)
+            SELECT workspace_id, item_id, working_directory FROM terminals;
+
+            DROP TABLE terminals;
+
+            ALTER TABLE terminals2 RENAME TO terminals;
+        )];
+}
+
+impl TerminalDb {
+    query! {
+       pub async fn update_workspace_id(
+            new_id: WorkspaceId,
+            old_id: WorkspaceId,
+            item_id: ItemId
+        ) -> Result<()> {
+            UPDATE terminals
+            SET workspace_id = ?
+            WHERE workspace_id = ? AND item_id = ?
+        }
+    }
+
+    query! {
+        pub async fn save_working_directory(
+            item_id: ItemId,
+            workspace_id: WorkspaceId,
+            working_directory: PathBuf
+        ) -> Result<()> {
+            INSERT OR REPLACE INTO terminals(item_id, workspace_id, working_directory)
+            VALUES (?, ?, ?)
+        }
+    }
+
+    query! {
+        pub fn get_working_directory(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<PathBuf>> {
+            SELECT working_directory
+            FROM terminals
+            WHERE item_id = ? AND workspace_id = ?
+        }
+    }
+}

crates/terminal_view2/src/terminal_element.rs 🔗

@@ -0,0 +1,954 @@
+// use editor::{Cursor, HighlightedRange, HighlightedRangeLine};
+// use gpui::{
+//     point, transparent_black, AnyElement, AppContext, Bounds, Component, CursorStyle, Element,
+//     FontStyle, FontWeight, HighlightStyle, Hsla, LayoutId, Line, ModelContext, MouseButton,
+//     Overlay, Pixels, Point, Quad, TextStyle, Underline, ViewContext, WeakModel, WindowContext,
+// };
+// use itertools::Itertools;
+// use language::CursorShape;
+// use ordered_float::OrderedFloat;
+// use settings::Settings;
+// use terminal::{
+//     alacritty_terminal::{
+//         ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor},
+//         grid::Dimensions,
+//         index::Point as AlacPoint,
+//         term::{cell::Flags, TermMode},
+//     },
+//     // mappings::colors::convert_color,
+//     terminal_settings::TerminalSettings,
+//     IndexedCell,
+//     Terminal,
+//     TerminalContent,
+//     TerminalSize,
+// };
+// use theme::ThemeSettings;
+// use workspace::ElementId;
+
+// use std::mem;
+// use std::{fmt::Debug, ops::RangeInclusive};
+
+// use crate::TerminalView;
+
+// ///The information generated during layout that is necessary for painting
+// pub struct LayoutState {
+//     cells: Vec<LayoutCell>,
+//     rects: Vec<LayoutRect>,
+//     relative_highlighted_ranges: Vec<(RangeInclusive<AlacPoint>, Hsla)>,
+//     cursor: Option<Cursor>,
+//     background_color: Hsla,
+//     size: TerminalSize,
+//     mode: TermMode,
+//     display_offset: usize,
+//     hyperlink_tooltip: Option<AnyElement<TerminalView>>,
+//     gutter: f32,
+// }
+
+// ///Helper struct for converting data between alacritty's cursor points, and displayed cursor points
+// struct DisplayCursor {
+//     line: i32,
+//     col: usize,
+// }
+
+// impl DisplayCursor {
+//     fn from(cursor_point: AlacPoint, display_offset: usize) -> Self {
+//         Self {
+//             line: cursor_point.line.0 + display_offset as i32,
+//             col: cursor_point.column.0,
+//         }
+//     }
+
+//     pub fn line(&self) -> i32 {
+//         self.line
+//     }
+
+//     pub fn col(&self) -> usize {
+//         self.col
+//     }
+// }
+
+// #[derive(Clone, Debug, Default)]
+// struct LayoutCell {
+//     point: AlacPoint<i32, i32>,
+//     text: Line,
+// }
+
+// impl LayoutCell {
+//     fn new(point: AlacPoint<i32, i32>, text: Line) -> LayoutCell {
+//         LayoutCell { point, text }
+//     }
+
+//     fn paint(
+//         &self,
+//         origin: Point<Pixels>,
+//         layout: &LayoutState,
+//         _visible_bounds: Bounds<Pixels>,
+//         _view: &mut TerminalView,
+//         cx: &mut WindowContext,
+//     ) {
+//         let pos = {
+//             let point = self.point;
+
+//             Point::new(
+//                 (origin.x + point.column as f32 * layout.size.cell_width).floor(),
+//                 origin.y + point.line as f32 * layout.size.line_height,
+//             )
+//         };
+
+//         self.text.paint(pos, layout.size.line_height, cx);
+//     }
+// }
+
+// #[derive(Clone, Debug, Default)]
+// struct LayoutRect {
+//     point: AlacPoint<i32, i32>,
+//     num_of_cells: usize,
+//     color: Hsla,
+// }
+
+// impl LayoutRect {
+//     fn new(point: AlacPoint<i32, i32>, num_of_cells: usize, color: Hsla) -> LayoutRect {
+//         LayoutRect {
+//             point,
+//             num_of_cells,
+//             color,
+//         }
+//     }
+
+//     fn extend(&self) -> Self {
+//         LayoutRect {
+//             point: self.point,
+//             num_of_cells: self.num_of_cells + 1,
+//             color: self.color,
+//         }
+//     }
+
+//     fn paint(
+//         &self,
+//         origin: Point<Pixels>,
+//         layout: &LayoutState,
+//         _view: &mut TerminalView,
+//         cx: &mut ViewContext<TerminalView>,
+//     ) {
+//         let position = {
+//             let alac_point = self.point;
+//             point(
+//                 (origin.x + alac_point.column as f32 * layout.size.cell_width).floor(),
+//                 origin.y + alac_point.line as f32 * layout.size.line_height,
+//             )
+//         };
+//         let size = point(
+//             (layout.size.cell_width * self.num_of_cells as f32).ceil(),
+//             layout.size.line_height,
+//         )
+//         .into();
+
+//         cx.paint_quad(
+//             Bounds::new(position, size),
+//             Default::default(),
+//             self.color,
+//             Default::default(),
+//             transparent_black(),
+//         );
+//     }
+// }
+
+// ///The GPUI element that paints the terminal.
+// ///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection?
+// pub struct TerminalElement {
+//     terminal: WeakModel<Terminal>,
+//     focused: bool,
+//     cursor_visible: bool,
+//     can_navigate_to_selected_word: bool,
+// }
+
+// impl TerminalElement {
+//     pub fn new(
+//         terminal: WeakModel<Terminal>,
+//         focused: bool,
+//         cursor_visible: bool,
+//         can_navigate_to_selected_word: bool,
+//     ) -> TerminalElement {
+//         TerminalElement {
+//             terminal,
+//             focused,
+//             cursor_visible,
+//             can_navigate_to_selected_word,
+//         }
+//     }
+
+//     //Vec<Range<AlacPoint>> -> Clip out the parts of the ranges
+
+//     fn layout_grid(
+//         grid: &Vec<IndexedCell>,
+//         text_style: &TextStyle,
+//         terminal_theme: &TerminalStyle,
+//         text_layout_cache: &TextLayoutCache,
+//         font_cache: &FontCache,
+//         hyperlink: Option<(HighlightStyle, &RangeInclusive<AlacPoint>)>,
+//     ) -> (Vec<LayoutCell>, Vec<LayoutRect>) {
+//         let mut cells = vec![];
+//         let mut rects = vec![];
+
+//         let mut cur_rect: Option<LayoutRect> = None;
+//         let mut cur_alac_color = None;
+
+//         let linegroups = grid.into_iter().group_by(|i| i.point.line);
+//         for (line_index, (_, line)) in linegroups.into_iter().enumerate() {
+//             for cell in line {
+//                 let mut fg = cell.fg;
+//                 let mut bg = cell.bg;
+//                 if cell.flags.contains(Flags::INVERSE) {
+//                     mem::swap(&mut fg, &mut bg);
+//                 }
+
+//                 //Expand background rect range
+//                 {
+//                     if matches!(bg, Named(NamedColor::Background)) {
+//                         //Continue to next cell, resetting variables if necessary
+//                         cur_alac_color = None;
+//                         if let Some(rect) = cur_rect {
+//                             rects.push(rect);
+//                             cur_rect = None
+//                         }
+//                     } else {
+//                         match cur_alac_color {
+//                             Some(cur_color) => {
+//                                 if bg == cur_color {
+//                                     cur_rect = cur_rect.take().map(|rect| rect.extend());
+//                                 } else {
+//                                     cur_alac_color = Some(bg);
+//                                     if cur_rect.is_some() {
+//                                         rects.push(cur_rect.take().unwrap());
+//                                     }
+//                                     cur_rect = Some(LayoutRect::new(
+//                                         AlacPoint::new(
+//                                             line_index as i32,
+//                                             cell.point.column.0 as i32,
+//                                         ),
+//                                         1,
+//                                         convert_color(&bg, &terminal_theme),
+//                                     ));
+//                                 }
+//                             }
+//                             None => {
+//                                 cur_alac_color = Some(bg);
+//                                 cur_rect = Some(LayoutRect::new(
+//                                     AlacPoint::new(line_index as i32, cell.point.column.0 as i32),
+//                                     1,
+//                                     convert_color(&bg, &terminal_theme),
+//                                 ));
+//                             }
+//                         }
+//                     }
+//                 }
+
+//                 //Layout current cell text
+//                 {
+//                     let cell_text = &cell.c.to_string();
+//                     if !is_blank(&cell) {
+//                         let cell_style = TerminalElement::cell_style(
+//                             &cell,
+//                             fg,
+//                             terminal_theme,
+//                             text_style,
+//                             font_cache,
+//                             hyperlink,
+//                         );
+
+//                         let layout_cell = text_layout_cache.layout_str(
+//                             cell_text,
+//                             text_style.font_size,
+//                             &[(cell_text.len(), cell_style)],
+//                         );
+
+//                         cells.push(LayoutCell::new(
+//                             AlacPoint::new(line_index as i32, cell.point.column.0 as i32),
+//                             layout_cell,
+//                         ))
+//                     };
+//                 }
+//             }
+
+//             if cur_rect.is_some() {
+//                 rects.push(cur_rect.take().unwrap());
+//             }
+//         }
+//         (cells, rects)
+//     }
+
+//     // Compute the cursor position and expected block width, may return a zero width if x_for_index returns
+//     // the same position for sequential indexes. Use em_width instead
+//     fn shape_cursor(
+//         cursor_point: DisplayCursor,
+//         size: TerminalSize,
+//         text_fragment: &Line,
+//     ) -> Option<(Point<Pixels>, Pixels)> {
+//         if cursor_point.line() < size.total_lines() as i32 {
+//             let cursor_width = if text_fragment.width == Pixels::ZERO {
+//                 size.cell_width()
+//             } else {
+//                 text_fragment.width
+//             };
+
+//             //Cursor should always surround as much of the text as possible,
+//             //hence when on pixel boundaries round the origin down and the width up
+//             Some((
+//                 point(
+//                     (cursor_point.col() as f32 * size.cell_width()).floor(),
+//                     (cursor_point.line() as f32 * size.line_height()).floor(),
+//                 ),
+//                 cursor_width.ceil(),
+//             ))
+//         } else {
+//             None
+//         }
+//     }
+
+//     ///Convert the Alacritty cell styles to GPUI text styles and background color
+//     fn cell_style(
+//         indexed: &IndexedCell,
+//         fg: terminal::alacritty_terminal::ansi::Color,
+//         style: &TerminalStyle,
+//         text_style: &TextStyle,
+//         font_cache: &FontCache,
+//         hyperlink: Option<(HighlightStyle, &RangeInclusive<AlacPoint>)>,
+//     ) -> RunStyle {
+//         let flags = indexed.cell.flags;
+//         let fg = convert_color(&fg, &style);
+
+//         let mut underline = flags
+//             .intersects(Flags::ALL_UNDERLINES)
+//             .then(|| Underline {
+//                 color: Some(fg),
+//                 squiggly: flags.contains(Flags::UNDERCURL),
+//                 thickness: OrderedFloat(1.),
+//             })
+//             .unwrap_or_default();
+
+//         if indexed.cell.hyperlink().is_some() {
+//             if underline.thickness == OrderedFloat(0.) {
+//                 underline.thickness = OrderedFloat(1.);
+//             }
+//         }
+
+//         let mut properties = Properties::new();
+//         if indexed.flags.intersects(Flags::BOLD | Flags::DIM_BOLD) {
+//             properties = *properties.weight(FontWeight::BOLD);
+//         }
+//         if indexed.flags.intersects(Flags::ITALIC) {
+//             properties = *properties.style(FontStyle::Italic);
+//         }
+
+//         let font_id = font_cache
+//             .select_font(text_style.font_family, &properties)
+//             .unwrap_or(text_style.font_id);
+
+//         let mut result = RunStyle {
+//             color: fg,
+//             font_id,
+//             underline,
+//         };
+
+//         if let Some((style, range)) = hyperlink {
+//             if range.contains(&indexed.point) {
+//                 if let Some(underline) = style.underline {
+//                     result.underline = underline;
+//                 }
+
+//                 if let Some(color) = style.color {
+//                     result.color = color;
+//                 }
+//             }
+//         }
+
+//         result
+//     }
+
+//     fn generic_button_handler<E>(
+//         connection: WeakModel<Terminal>,
+//         origin: Point<Pixels>,
+//         f: impl Fn(&mut Terminal, Point<Pixels>, E, &mut ModelContext<Terminal>),
+//     ) -> impl Fn(E, &mut TerminalView, &mut EventContext<TerminalView>) {
+//         move |event, _: &mut TerminalView, cx| {
+//             cx.focus_parent();
+//             if let Some(conn_handle) = connection.upgrade() {
+//                 conn_handle.update(cx, |terminal, cx| {
+//                     f(terminal, origin, event, cx);
+
+//                     cx.notify();
+//                 })
+//             }
+//         }
+//     }
+
+//     fn attach_mouse_handlers(
+//         &self,
+//         origin: Point<Pixels>,
+//         visible_bounds: Bounds<Pixels>,
+//         mode: TermMode,
+//         cx: &mut ViewContext<TerminalView>,
+//     ) {
+//         let connection = self.terminal;
+
+//         let mut region = MouseRegion::new::<Self>(cx.view_id(), 0, visible_bounds);
+
+//         // Terminal Emulator controlled behavior:
+//         region = region
+//             // Start selections
+//             .on_down(MouseButton::Left, move |event, v: &mut TerminalView, cx| {
+//                 let terminal_view = cx.handle();
+//                 cx.focus(&terminal_view);
+//                 v.context_menu.update(cx, |menu, _cx| menu.delay_cancel());
+//                 if let Some(conn_handle) = connection.upgrade() {
+//                     conn_handle.update(cx, |terminal, cx| {
+//                         terminal.mouse_down(&event, origin);
+
+//                         cx.notify();
+//                     })
+//                 }
+//             })
+//             // Update drag selections
+//             .on_drag(MouseButton::Left, move |event, _: &mut TerminalView, cx| {
+//                 if event.end {
+//                     return;
+//                 }
+
+//                 if cx.is_self_focused() {
+//                     if let Some(conn_handle) = connection.upgrade() {
+//                         conn_handle.update(cx, |terminal, cx| {
+//                             terminal.mouse_drag(event, origin);
+//                             cx.notify();
+//                         })
+//                     }
+//                 }
+//             })
+//             // Copy on up behavior
+//             .on_up(
+//                 MouseButton::Left,
+//                 TerminalElement::generic_button_handler(
+//                     connection,
+//                     origin,
+//                     move |terminal, origin, e, cx| {
+//                         terminal.mouse_up(&e, origin, cx);
+//                     },
+//                 ),
+//             )
+//             // Context menu
+//             .on_click(
+//                 MouseButton::Right,
+//                 move |event, view: &mut TerminalView, cx| {
+//                     let mouse_mode = if let Some(conn_handle) = connection.upgrade() {
+//                         conn_handle.update(cx, |terminal, _cx| terminal.mouse_mode(event.shift))
+//                     } else {
+//                         // If we can't get the model handle, probably can't deploy the context menu
+//                         true
+//                     };
+//                     if !mouse_mode {
+//                         view.deploy_context_menu(event.position, cx);
+//                     }
+//                 },
+//             )
+//             .on_move(move |event, _: &mut TerminalView, cx| {
+//                 if cx.is_self_focused() {
+//                     if let Some(conn_handle) = connection.upgrade() {
+//                         conn_handle.update(cx, |terminal, cx| {
+//                             terminal.mouse_move(&event, origin);
+//                             cx.notify();
+//                         })
+//                     }
+//                 }
+//             })
+//             .on_scroll(move |event, _: &mut TerminalView, cx| {
+//                 if let Some(conn_handle) = connection.upgrade() {
+//                     conn_handle.update(cx, |terminal, cx| {
+//                         terminal.scroll_wheel(event, origin);
+//                         cx.notify();
+//                     })
+//                 }
+//             });
+
+//         // Mouse mode handlers:
+//         // All mouse modes need the extra click handlers
+//         if mode.intersects(TermMode::MOUSE_MODE) {
+//             region = region
+//                 .on_down(
+//                     MouseButton::Right,
+//                     TerminalElement::generic_button_handler(
+//                         connection,
+//                         origin,
+//                         move |terminal, origin, e, _cx| {
+//                             terminal.mouse_down(&e, origin);
+//                         },
+//                     ),
+//                 )
+//                 .on_down(
+//                     MouseButton::Middle,
+//                     TerminalElement::generic_button_handler(
+//                         connection,
+//                         origin,
+//                         move |terminal, origin, e, _cx| {
+//                             terminal.mouse_down(&e, origin);
+//                         },
+//                     ),
+//                 )
+//                 .on_up(
+//                     MouseButton::Right,
+//                     TerminalElement::generic_button_handler(
+//                         connection,
+//                         origin,
+//                         move |terminal, origin, e, cx| {
+//                             terminal.mouse_up(&e, origin, cx);
+//                         },
+//                     ),
+//                 )
+//                 .on_up(
+//                     MouseButton::Middle,
+//                     TerminalElement::generic_button_handler(
+//                         connection,
+//                         origin,
+//                         move |terminal, origin, e, cx| {
+//                             terminal.mouse_up(&e, origin, cx);
+//                         },
+//                     ),
+//                 )
+//         }
+
+//         cx.scene().push_mouse_region(region);
+//     }
+// }
+
+// impl Element<TerminalView> for TerminalElement {
+//     type ElementState = LayoutState;
+
+//     fn layout(
+//         &mut self,
+//         view_state: &mut TerminalView,
+//         element_state: Option<Self::ElementState>,
+//         cx: &mut ViewContext<TerminalView>,
+//     ) -> (LayoutId, Self::ElementState) {
+//         let settings = ThemeSettings::get_global(cx);
+//         let terminal_settings = TerminalSettings::get_global(cx);
+
+//         //Setup layout information
+//         let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone.
+//         let link_style = settings.theme.editor.link_definition;
+//         let tooltip_style = settings.theme.tooltip.clone();
+
+//         let font_cache = cx.font_cache();
+//         let font_size = font_size(&terminal_settings, cx).unwrap_or(settings.buffer_font_size(cx));
+//         let font_family_name = terminal_settings
+//             .font_family
+//             .as_ref()
+//             .unwrap_or(&settings.buffer_font_family_name);
+//         let font_features = terminal_settings
+//             .font_features
+//             .as_ref()
+//             .unwrap_or(&settings.buffer_font_features);
+//         let family_id = font_cache
+//             .load_family(&[font_family_name], &font_features)
+//             .log_err()
+//             .unwrap_or(settings.buffer_font_family);
+//         let font_id = font_cache
+//             .select_font(family_id, &Default::default())
+//             .unwrap();
+
+//         let text_style = TextStyle {
+//             color: settings.theme.editor.text_color,
+//             font_family_id: family_id,
+//             font_family_name: font_cache.family_name(family_id).unwrap(),
+//             font_id,
+//             font_size,
+//             font_properties: Default::default(),
+//             underline: Default::default(),
+//             soft_wrap: false,
+//         };
+//         let selection_color = settings.theme.editor.selection.selection;
+//         let match_color = settings.theme.search.match_background;
+//         let gutter;
+//         let dimensions = {
+//             let line_height = text_style.font_size * terminal_settings.line_height.value();
+//             let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size);
+//             gutter = cell_width;
+
+//             let size = constraint.max - point(gutter, 0.);
+//             TerminalSize::new(line_height, cell_width, size)
+//         };
+
+//         let search_matches = if let Some(terminal_model) = self.terminal.upgrade() {
+//             terminal_model.read(cx).matches.clone()
+//         } else {
+//             Default::default()
+//         };
+
+//         let background_color = terminal_theme.background;
+//         let terminal_handle = self.terminal.upgrade().unwrap();
+
+//         let last_hovered_word = terminal_handle.update(cx, |terminal, cx| {
+//             terminal.set_size(dimensions);
+//             terminal.try_sync(cx);
+//             if self.can_navigate_to_selected_word && terminal.can_navigate_to_selected_word() {
+//                 terminal.last_content.last_hovered_word.clone()
+//             } else {
+//                 None
+//             }
+//         });
+
+//         let hyperlink_tooltip = last_hovered_word.clone().map(|hovered_word| {
+//             let mut tooltip = Overlay::new(
+//                 Empty::new()
+//                     .contained()
+//                     .constrained()
+//                     .with_width(dimensions.width())
+//                     .with_height(dimensions.height())
+//                     .with_tooltip::<TerminalElement>(
+//                         hovered_word.id,
+//                         hovered_word.word,
+//                         None,
+//                         tooltip_style,
+//                         cx,
+//                     ),
+//             )
+//             .with_position_mode(gpui::OverlayPositionMode::Local)
+//             .into_any();
+
+//             tooltip.layout(
+//                 SizeConstraint::new(Point::zero(), cx.window_size()),
+//                 view_state,
+//                 cx,
+//             );
+//             tooltip
+//         });
+
+//         let TerminalContent {
+//             cells,
+//             mode,
+//             display_offset,
+//             cursor_char,
+//             selection,
+//             cursor,
+//             ..
+//         } = { &terminal_handle.read(cx).last_content };
+
+//         // searches, highlights to a single range representations
+//         let mut relative_highlighted_ranges = Vec::new();
+//         for search_match in search_matches {
+//             relative_highlighted_ranges.push((search_match, match_color))
+//         }
+//         if let Some(selection) = selection {
+//             relative_highlighted_ranges.push((selection.start..=selection.end, selection_color));
+//         }
+
+//         // then have that representation be converted to the appropriate highlight data structure
+
+//         let (cells, rects) = TerminalElement::layout_grid(
+//             cells,
+//             &text_style,
+//             &terminal_theme,
+//             cx.text_layout_cache(),
+//             cx.font_cache(),
+//             last_hovered_word
+//                 .as_ref()
+//                 .map(|last_hovered_word| (link_style, &last_hovered_word.word_match)),
+//         );
+
+//         //Layout cursor. Rectangle is used for IME, so we should lay it out even
+//         //if we don't end up showing it.
+//         let cursor = if let AlacCursorShape::Hidden = cursor.shape {
+//             None
+//         } else {
+//             let cursor_point = DisplayCursor::from(cursor.point, *display_offset);
+//             let cursor_text = {
+//                 let str_trxt = cursor_char.to_string();
+
+//                 let color = if self.focused {
+//                     terminal_theme.background
+//                 } else {
+//                     terminal_theme.foreground
+//                 };
+
+//                 cx.text_layout_cache().layout_str(
+//                     &str_trxt,
+//                     text_style.font_size,
+//                     &[(
+//                         str_trxt.len(),
+//                         RunStyle {
+//                             font_id: text_style.font_id,
+//                             color,
+//                             underline: Default::default(),
+//                         },
+//                     )],
+//                 )
+//             };
+
+//             let focused = self.focused;
+//             TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map(
+//                 move |(cursor_position, block_width)| {
+//                     let (shape, text) = match cursor.shape {
+//                         AlacCursorShape::Block if !focused => (CursorShape::Hollow, None),
+//                         AlacCursorShape::Block => (CursorShape::Block, Some(cursor_text)),
+//                         AlacCursorShape::Underline => (CursorShape::Underscore, None),
+//                         AlacCursorShape::Beam => (CursorShape::Bar, None),
+//                         AlacCursorShape::HollowBlock => (CursorShape::Hollow, None),
+//                         //This case is handled in the if wrapping the whole cursor layout
+//                         AlacCursorShape::Hidden => unreachable!(),
+//                     };
+
+//                     Cursor::new(
+//                         cursor_position,
+//                         block_width,
+//                         dimensions.line_height,
+//                         terminal_theme.cursor,
+//                         shape,
+//                         text,
+//                     )
+//                 },
+//             )
+//         };
+
+//         //Done!
+//         (
+//             constraint.max,
+//             Self::ElementState {
+//                 cells,
+//                 cursor,
+//                 background_color,
+//                 size: dimensions,
+//                 rects,
+//                 relative_highlighted_ranges,
+//                 mode: *mode,
+//                 display_offset: *display_offset,
+//                 hyperlink_tooltip,
+//                 gutter,
+//             },
+//         )
+//     }
+
+//     fn paint(
+//         &mut self,
+//         bounds: Bounds<Pixels>,
+//         view_state: &mut TerminalView,
+//         element_state: &mut Self::ElementState,
+//         cx: &mut ViewContext<TerminalView>,
+//     ) {
+//         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
+
+//         //Setup element stuff
+//         let clip_bounds = Some(visible_bounds);
+
+//         cx.paint_layer(clip_bounds, |cx| {
+//             let origin = bounds.origin + point(element_state.gutter, 0.);
+
+//             // Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
+//             self.attach_mouse_handlers(origin, visible_bounds, element_state.mode, cx);
+
+//             cx.scene().push_cursor_region(gpui::CursorRegion {
+//                 bounds,
+//                 style: if element_state.hyperlink_tooltip.is_some() {
+//                     CursorStyle::AlacPointingHand
+//                 } else {
+//                     CursorStyle::IBeam
+//                 },
+//             });
+
+//             cx.paint_layer(clip_bounds, |cx| {
+//                 //Start with a background color
+//                 cx.scene().push_quad(Quad {
+//                     bounds,
+//                     background: Some(element_state.background_color),
+//                     border: Default::default(),
+//                     corner_radii: Default::default(),
+//                 });
+
+//                 for rect in &element_state.rects {
+//                     rect.paint(origin, element_state, view_state, cx);
+//                 }
+//             });
+
+//             //Draw Highlighted Backgrounds
+//             cx.paint_layer(clip_bounds, |cx| {
+//                 for (relative_highlighted_range, color) in
+//                     element_state.relative_highlighted_ranges.iter()
+//                 {
+//                     if let Some((start_y, highlighted_range_lines)) = to_highlighted_range_lines(
+//                         relative_highlighted_range,
+//                         element_state,
+//                         origin,
+//                     ) {
+//                         let hr = HighlightedRange {
+//                             start_y, //Need to change this
+//                             line_height: element_state.size.line_height,
+//                             lines: highlighted_range_lines,
+//                             color: color.clone(),
+//                             //Copied from editor. TODO: move to theme or something
+//                             corner_radius: 0.15 * element_state.size.line_height,
+//                         };
+//                         hr.paint(bounds, cx);
+//                     }
+//                 }
+//             });
+
+//             //Draw the text cells
+//             cx.paint_layer(clip_bounds, |cx| {
+//                 for cell in &element_state.cells {
+//                     cell.paint(origin, element_state, visible_bounds, view_state, cx);
+//                 }
+//             });
+
+//             //Draw cursor
+//             if self.cursor_visible {
+//                 if let Some(cursor) = &element_state.cursor {
+//                     cx.paint_layer(clip_bounds, |cx| {
+//                         cursor.paint(origin, cx);
+//                     })
+//                 }
+//             }
+
+//             if let Some(element) = &mut element_state.hyperlink_tooltip {
+//                 element.paint(origin, visible_bounds, view_state, cx)
+//             }
+//         });
+//     }
+
+//     fn element_id(&self) -> Option<ElementId> {
+//         todo!()
+//     }
+
+//     // todo!() remove?
+//     // fn metadata(&self) -> Option<&dyn std::any::Any> {
+//     //     None
+//     // }
+
+//     // fn debug(
+//     //     &self,
+//     //     _: Bounds<Pixels>,
+//     //     _: &Self::ElementState,
+//     //     _: &Self::PaintState,
+//     //     _: &TerminalView,
+//     //     _: &gpui::ViewContext<TerminalView>,
+//     // ) -> gpui::serde_json::Value {
+//     //     json!({
+//     //         "type": "TerminalElement",
+//     //     })
+//     // }
+
+//     // fn rect_for_text_range(
+//     //     &self,
+//     //     _: Range<usize>,
+//     //     bounds: Bounds<Pixels>,
+//     //     _: Bounds<Pixels>,
+//     //     layout: &Self::ElementState,
+//     //     _: &Self::PaintState,
+//     //     _: &TerminalView,
+//     //     _: &gpui::ViewContext<TerminalView>,
+//     // ) -> Option<Bounds<Pixels>> {
+//     //     // Use the same origin that's passed to `Cursor::paint` in the paint
+//     //     // method bove.
+//     //     let mut origin = bounds.origin() + point(layout.size.cell_width, 0.);
+
+//     //     // TODO - Why is it necessary to move downward one line to get correct
+//     //     // positioning? I would think that we'd want the same rect that is
+//     //     // painted for the cursor.
+//     //     origin += point(0., layout.size.line_height);
+
+//     //     Some(layout.cursor.as_ref()?.bounding_rect(origin))
+//     // }
+// }
+
+// impl Component<TerminalView> for TerminalElement {
+//     fn render(self) -> AnyElement<TerminalView> {
+//         todo!()
+//     }
+// }
+
+// fn is_blank(cell: &IndexedCell) -> bool {
+//     if cell.c != ' ' {
+//         return false;
+//     }
+
+//     if cell.bg != AnsiColor::Named(NamedColor::Background) {
+//         return false;
+//     }
+
+//     if cell.hyperlink().is_some() {
+//         return false;
+//     }
+
+//     if cell
+//         .flags
+//         .intersects(Flags::ALL_UNDERLINES | Flags::INVERSE | Flags::STRIKEOUT)
+//     {
+//         return false;
+//     }
+
+//     return true;
+// }
+
+// fn to_highlighted_range_lines(
+//     range: &RangeInclusive<AlacPoint>,
+//     layout: &LayoutState,
+//     origin: Point<Pixels>,
+// ) -> Option<(Pixels, Vec<HighlightedRangeLine>)> {
+//     // Step 1. Normalize the points to be viewport relative.
+//     // When display_offset = 1, here's how the grid is arranged:
+//     //-2,0 -2,1...
+//     //--- Viewport top
+//     //-1,0 -1,1...
+//     //--------- Terminal Top
+//     // 0,0  0,1...
+//     // 1,0  1,1...
+//     //--- Viewport Bottom
+//     // 2,0  2,1...
+//     //--------- Terminal Bottom
+
+//     // Normalize to viewport relative, from terminal relative.
+//     // lines are i32s, which are negative above the top left corner of the terminal
+//     // If the user has scrolled, we use the display_offset to tell us which offset
+//     // of the grid data we should be looking at. But for the rendering step, we don't
+//     // want negatives. We want things relative to the 'viewport' (the area of the grid
+//     // which is currently shown according to the display offset)
+//     let unclamped_start = AlacPoint::new(
+//         range.start().line + layout.display_offset,
+//         range.start().column,
+//     );
+//     let unclamped_end =
+//         AlacPoint::new(range.end().line + layout.display_offset, range.end().column);
+
+//     // Step 2. Clamp range to viewport, and return None if it doesn't overlap
+//     if unclamped_end.line.0 < 0 || unclamped_start.line.0 > layout.size.num_lines() as i32 {
+//         return None;
+//     }
+
+//     let clamped_start_line = unclamped_start.line.0.max(0) as usize;
+//     let clamped_end_line = unclamped_end.line.0.min(layout.size.num_lines() as i32) as usize;
+//     //Convert the start of the range to pixels
+//     let start_y = origin.y + clamped_start_line as f32 * layout.size.line_height;
+
+//     // Step 3. Expand ranges that cross lines into a collection of single-line ranges.
+//     //  (also convert to pixels)
+//     let mut highlighted_range_lines = Vec::new();
+//     for line in clamped_start_line..=clamped_end_line {
+//         let mut line_start = 0;
+//         let mut line_end = layout.size.columns();
+
+//         if line == clamped_start_line {
+//             line_start = unclamped_start.column.0 as usize;
+//         }
+//         if line == clamped_end_line {
+//             line_end = unclamped_end.column.0 as usize + 1; //+1 for inclusive
+//         }
+
+//         highlighted_range_lines.push(HighlightedRangeLine {
+//             start_x: origin.x + line_start as f32 * layout.size.cell_width,
+//             end_x: origin.x + line_end as f32 * layout.size.cell_width,
+//         });
+//     }
+
+//     Some((start_y, highlighted_range_lines))
+// }
+
+// fn font_size(terminal_settings: &TerminalSettings, cx: &mut AppContext) -> Option<Pixels> {
+//     terminal_settings
+//         .font_size
+//         .map(|size| theme::adjusted_font_size(size, cx))
+// }

crates/terminal_view2/src/terminal_panel.rs 🔗

@@ -0,0 +1,446 @@
+use std::{path::PathBuf, sync::Arc};
+
+use crate::TerminalView;
+use db::kvp::KEY_VALUE_STORE;
+use gpui::{
+    actions, div, serde_json, AppContext, AsyncWindowContext, Div, Entity, EventEmitter,
+    FocusHandle, FocusableView, ParentComponent, Render, Subscription, Task, View, ViewContext,
+    VisualContext, WeakView, WindowContext,
+};
+use project::Fs;
+use serde::{Deserialize, Serialize};
+use settings::{Settings, SettingsStore};
+use terminal::terminal_settings::{TerminalDockPosition, TerminalSettings};
+use util::{ResultExt, TryFutureExt};
+use workspace::{
+    dock::{DockPosition, Panel, PanelEvent},
+    item::Item,
+    pane,
+    ui::Icon,
+    Pane, Workspace,
+};
+
+use anyhow::Result;
+
+const TERMINAL_PANEL_KEY: &'static str = "TerminalPanel";
+
+actions!(ToggleFocus);
+
+pub fn init(cx: &mut AppContext) {
+    cx.observe_new_views(
+        |workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
+            workspace.register_action(TerminalPanel::new_terminal);
+            workspace.register_action(TerminalPanel::open_terminal);
+            workspace.register_action(|workspace, _: &ToggleFocus, cx| {
+                workspace.toggle_panel_focus::<TerminalPanel>(cx);
+            });
+        },
+    )
+    .detach();
+}
+
+pub struct TerminalPanel {
+    pane: View<Pane>,
+    fs: Arc<dyn Fs>,
+    workspace: WeakView<Workspace>,
+    width: Option<f32>,
+    height: Option<f32>,
+    pending_serialization: Task<Option<()>>,
+    _subscriptions: Vec<Subscription>,
+}
+
+impl TerminalPanel {
+    fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
+        let _weak_self = cx.view().downgrade();
+        let pane = cx.build_view(|cx| {
+            let _window = cx.window_handle();
+            let mut pane = Pane::new(
+                workspace.weak_handle(),
+                workspace.project().clone(),
+                Default::default(),
+                cx,
+            );
+            pane.set_can_split(false, cx);
+            pane.set_can_navigate(false, cx);
+            // todo!()
+            // pane.on_can_drop(move |drag_and_drop, cx| {
+            //     drag_and_drop
+            //         .currently_dragged::<DraggedItem>(window)
+            //         .map_or(false, |(_, item)| {
+            //             item.handle.act_as::<TerminalView>(cx).is_some()
+            //         })
+            // });
+            // pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
+            //     let this = weak_self.clone();
+            //     Flex::row()
+            //         .with_child(Pane::render_tab_bar_button(
+            //             0,
+            //             "icons/plus.svg",
+            //             false,
+            //             Some(("New Terminal", Some(Box::new(workspace::NewTerminal)))),
+            //             cx,
+            //             move |_, cx| {
+            //                 let this = this.clone();
+            //                 cx.window_context().defer(move |cx| {
+            //                     if let Some(this) = this.upgrade() {
+            //                         this.update(cx, |this, cx| {
+            //                             this.add_terminal(None, cx);
+            //                         });
+            //                     }
+            //                 })
+            //             },
+            //             |_, _| {},
+            //             None,
+            //         ))
+            //         .with_child(Pane::render_tab_bar_button(
+            //             1,
+            //             if pane.is_zoomed() {
+            //                 "icons/minimize.svg"
+            //             } else {
+            //                 "icons/maximize.svg"
+            //             },
+            //             pane.is_zoomed(),
+            //             Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))),
+            //             cx,
+            //             move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
+            //             |_, _| {},
+            //             None,
+            //         ))
+            //         .into_any()
+            // });
+            // let buffer_search_bar = cx.build_view(search::BufferSearchBar::new);
+            // pane.toolbar()
+            //     .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx));
+            pane
+        });
+        let subscriptions = vec![
+            cx.observe(&pane, |_, _, cx| cx.notify()),
+            cx.subscribe(&pane, Self::handle_pane_event),
+        ];
+        let this = Self {
+            pane,
+            fs: workspace.app_state().fs.clone(),
+            workspace: workspace.weak_handle(),
+            pending_serialization: Task::ready(None),
+            width: None,
+            height: None,
+            _subscriptions: subscriptions,
+        };
+        let mut old_dock_position = this.position(cx);
+        cx.observe_global::<SettingsStore>(move |this, cx| {
+            let new_dock_position = this.position(cx);
+            if new_dock_position != old_dock_position {
+                old_dock_position = new_dock_position;
+                cx.emit(PanelEvent::ChangePosition);
+            }
+        })
+        .detach();
+        this
+    }
+
+    pub async fn load(
+        workspace: WeakView<Workspace>,
+        mut cx: AsyncWindowContext,
+    ) -> Result<View<Self>> {
+        let serialized_panel = cx
+            .background_executor()
+            .spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) })
+            .await
+            .log_err()
+            .flatten()
+            .map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel))
+            .transpose()
+            .log_err()
+            .flatten();
+
+        let (panel, pane, items) = workspace.update(&mut cx, |workspace, cx| {
+            let panel = cx.build_view(|cx| TerminalPanel::new(workspace, cx));
+            let items = if let Some(serialized_panel) = serialized_panel.as_ref() {
+                panel.update(cx, |panel, cx| {
+                    cx.notify();
+                    panel.height = serialized_panel.height;
+                    panel.width = serialized_panel.width;
+                    panel.pane.update(cx, |_, cx| {
+                        serialized_panel
+                            .items
+                            .iter()
+                            .map(|item_id| {
+                                TerminalView::deserialize(
+                                    workspace.project().clone(),
+                                    workspace.weak_handle(),
+                                    workspace.database_id(),
+                                    *item_id,
+                                    cx,
+                                )
+                            })
+                            .collect::<Vec<_>>()
+                    })
+                })
+            } else {
+                Default::default()
+            };
+            let pane = panel.read(cx).pane.clone();
+            (panel, pane, items)
+        })?;
+
+        let pane = pane.downgrade();
+        let items = futures::future::join_all(items).await;
+        pane.update(&mut cx, |pane, cx| {
+            let active_item_id = serialized_panel
+                .as_ref()
+                .and_then(|panel| panel.active_item_id);
+            let mut active_ix = None;
+            for item in items {
+                if let Some(item) = item.log_err() {
+                    let item_id = item.entity_id().as_u64();
+                    pane.add_item(Box::new(item), false, false, None, cx);
+                    if Some(item_id) == active_item_id {
+                        active_ix = Some(pane.items_len() - 1);
+                    }
+                }
+            }
+
+            if let Some(active_ix) = active_ix {
+                pane.activate_item(active_ix, false, false, cx)
+            }
+        })?;
+
+        Ok(panel)
+    }
+
+    fn handle_pane_event(
+        &mut self,
+        _pane: View<Pane>,
+        event: &pane::Event,
+        cx: &mut ViewContext<Self>,
+    ) {
+        match event {
+            pane::Event::ActivateItem { .. } => self.serialize(cx),
+            pane::Event::RemoveItem { .. } => self.serialize(cx),
+            pane::Event::Remove => cx.emit(PanelEvent::Close),
+            pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
+            pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
+            pane::Event::Focus => cx.emit(PanelEvent::Focus),
+
+            pane::Event::AddItem { item } => {
+                if let Some(workspace) = self.workspace.upgrade() {
+                    let pane = self.pane.clone();
+                    workspace.update(cx, |workspace, cx| item.added_to_pane(workspace, pane, cx))
+                }
+            }
+
+            _ => {}
+        }
+    }
+
+    pub fn open_terminal(
+        workspace: &mut Workspace,
+        action: &workspace::OpenTerminal,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        let Some(this) = workspace.focus_panel::<Self>(cx) else {
+            return;
+        };
+
+        this.update(cx, |this, cx| {
+            this.add_terminal(Some(action.working_directory.clone()), cx)
+        })
+    }
+
+    ///Create a new Terminal in the current working directory or the user's home directory
+    fn new_terminal(
+        workspace: &mut Workspace,
+        _: &workspace::NewTerminal,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        let Some(this) = workspace.focus_panel::<Self>(cx) else {
+            return;
+        };
+
+        this.update(cx, |this, cx| this.add_terminal(None, cx))
+    }
+
+    fn add_terminal(&mut self, working_directory: Option<PathBuf>, cx: &mut ViewContext<Self>) {
+        let workspace = self.workspace.clone();
+        cx.spawn(|this, mut cx| async move {
+            let pane = this.update(&mut cx, |this, _| this.pane.clone())?;
+            workspace.update(&mut cx, |workspace, cx| {
+                let working_directory = if let Some(working_directory) = working_directory {
+                    Some(working_directory)
+                } else {
+                    let working_directory_strategy =
+                        TerminalSettings::get_global(cx).working_directory.clone();
+                    crate::get_working_directory(workspace, cx, working_directory_strategy)
+                };
+
+                let window = cx.window_handle();
+                if let Some(terminal) = workspace.project().update(cx, |project, cx| {
+                    project
+                        .create_terminal(working_directory, window, cx)
+                        .log_err()
+                }) {
+                    let terminal = Box::new(cx.build_view(|cx| {
+                        TerminalView::new(
+                            terminal,
+                            workspace.weak_handle(),
+                            workspace.database_id(),
+                            cx,
+                        )
+                    }));
+                    pane.update(cx, |pane, cx| {
+                        let focus = pane.has_focus(cx);
+                        pane.add_item(terminal, true, focus, None, cx);
+                    });
+                }
+            })?;
+            this.update(&mut cx, |this, cx| this.serialize(cx))?;
+            anyhow::Ok(())
+        })
+        .detach_and_log_err(cx);
+    }
+
+    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
+        let items = self
+            .pane
+            .read(cx)
+            .items()
+            .map(|item| item.item_id().as_u64())
+            .collect::<Vec<_>>();
+        let active_item_id = self
+            .pane
+            .read(cx)
+            .active_item()
+            .map(|item| item.item_id().as_u64());
+        let height = self.height;
+        let width = self.width;
+        self.pending_serialization = cx.background_executor().spawn(
+            async move {
+                KEY_VALUE_STORE
+                    .write_kvp(
+                        TERMINAL_PANEL_KEY.into(),
+                        serde_json::to_string(&SerializedTerminalPanel {
+                            items,
+                            active_item_id,
+                            height,
+                            width,
+                        })?,
+                    )
+                    .await?;
+                anyhow::Ok(())
+            }
+            .log_err(),
+        );
+    }
+}
+
+impl EventEmitter<PanelEvent> for TerminalPanel {}
+
+impl Render for TerminalPanel {
+    type Element = Div<Self>;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        div().child(self.pane.clone())
+    }
+}
+
+impl FocusableView for TerminalPanel {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.pane.focus_handle(cx)
+    }
+}
+
+impl Panel for TerminalPanel {
+    fn position(&self, cx: &WindowContext) -> DockPosition {
+        match TerminalSettings::get_global(cx).dock {
+            TerminalDockPosition::Left => DockPosition::Left,
+            TerminalDockPosition::Bottom => DockPosition::Bottom,
+            TerminalDockPosition::Right => DockPosition::Right,
+        }
+    }
+
+    fn position_is_valid(&self, _: DockPosition) -> bool {
+        true
+    }
+
+    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
+        settings::update_settings_file::<TerminalSettings>(self.fs.clone(), cx, move |settings| {
+            let dock = match position {
+                DockPosition::Left => TerminalDockPosition::Left,
+                DockPosition::Bottom => TerminalDockPosition::Bottom,
+                DockPosition::Right => TerminalDockPosition::Right,
+            };
+            settings.dock = Some(dock);
+        });
+    }
+
+    fn size(&self, cx: &WindowContext) -> f32 {
+        let settings = TerminalSettings::get_global(cx);
+        match self.position(cx) {
+            DockPosition::Left | DockPosition::Right => {
+                self.width.unwrap_or_else(|| settings.default_width)
+            }
+            DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height),
+        }
+    }
+
+    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
+        match self.position(cx) {
+            DockPosition::Left | DockPosition::Right => self.width = size,
+            DockPosition::Bottom => self.height = size,
+        }
+        self.serialize(cx);
+        cx.notify();
+    }
+
+    fn is_zoomed(&self, cx: &WindowContext) -> bool {
+        self.pane.read(cx).is_zoomed()
+    }
+
+    fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
+        self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
+    }
+
+    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
+        if active && self.pane.read(cx).items_len() == 0 {
+            self.add_terminal(None, cx)
+        }
+    }
+
+    fn icon_label(&self, cx: &WindowContext) -> Option<String> {
+        let count = self.pane.read(cx).items_len();
+        if count == 0 {
+            None
+        } else {
+            Some(count.to_string())
+        }
+    }
+
+    fn has_focus(&self, cx: &WindowContext) -> bool {
+        self.pane.read(cx).has_focus(cx)
+    }
+
+    fn persistent_name() -> &'static str {
+        "TerminalPanel"
+    }
+
+    // todo!()
+    // fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
+    //     ("Terminal Panel".into(), Some(Box::new(ToggleFocus)))
+    // }
+
+    fn icon(&self, _cx: &WindowContext) -> Option<Icon> {
+        Some(Icon::Terminal)
+    }
+
+    fn toggle_action(&self) -> Box<dyn gpui::Action> {
+        Box::new(ToggleFocus)
+    }
+}
+
+#[derive(Serialize, Deserialize)]
+struct SerializedTerminalPanel {
+    items: Vec<u64>,
+    active_item_id: Option<u64>,
+    width: Option<f32>,
+    height: Option<f32>,
+}

crates/terminal_view2/src/terminal_view.rs 🔗

@@ -0,0 +1,1204 @@
+#![allow(unused_variables)]
+//todo!(remove)
+
+mod persistence;
+pub mod terminal_element;
+pub mod terminal_panel;
+
+// todo!()
+// use crate::terminal_element::TerminalElement;
+use editor::{scroll::autoscroll::Autoscroll, Editor};
+use gpui::{
+    actions, div, img, red, Action, AnyElement, AppContext, Component, DispatchPhase, Div,
+    EventEmitter, FocusEvent, FocusHandle, Focusable, FocusableComponent, FocusableView,
+    InputHandler, InteractiveComponent, KeyDownEvent, Keystroke, Model, MouseButton,
+    ParentComponent, Pixels, Render, SharedString, Styled, Task, View, ViewContext, VisualContext,
+    WeakView,
+};
+use language::Bias;
+use persistence::TERMINAL_DB;
+use project::{search::SearchQuery, LocalWorktree, Project};
+use terminal::{
+    alacritty_terminal::{
+        index::Point,
+        term::{search::RegexSearch, TermMode},
+    },
+    terminal_settings::{TerminalBlink, TerminalSettings, WorkingDirectory},
+    Event, MaybeNavigationTarget, Terminal,
+};
+use util::{paths::PathLikeWithPosition, ResultExt};
+use workspace::{
+    item::{BreadcrumbText, Item, ItemEvent},
+    notifications::NotifyResultExt,
+    register_deserializable_item,
+    searchable::{SearchEvent, SearchOptions, SearchableItem},
+    ui::{ContextMenu, Label},
+    CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
+};
+
+use anyhow::Context;
+use dirs::home_dir;
+use serde::Deserialize;
+use settings::Settings;
+use smol::Timer;
+
+use std::{
+    ops::RangeInclusive,
+    path::{Path, PathBuf},
+    sync::Arc,
+    time::Duration,
+};
+
+const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
+
+///Event to transmit the scroll from the element to the view
+#[derive(Clone, Debug, PartialEq)]
+pub struct ScrollTerminal(pub i32);
+
+#[derive(Clone, Debug, Default, Deserialize, PartialEq, Action)]
+pub struct SendText(String);
+
+#[derive(Clone, Debug, Default, Deserialize, PartialEq, Action)]
+pub struct SendKeystroke(String);
+
+actions!(Clear, Copy, Paste, ShowCharacterPalette, SearchTest);
+
+pub fn init(cx: &mut AppContext) {
+    workspace::ui::init(cx);
+    terminal_panel::init(cx);
+    terminal::init(cx);
+
+    register_deserializable_item::<TerminalView>(cx);
+
+    cx.observe_new_views(
+        |workspace: &mut Workspace, cx: &mut ViewContext<Workspace>| {
+            workspace.register_action(TerminalView::deploy);
+        },
+    )
+    .detach();
+}
+
+///A terminal view, maintains the PTY's file handles and communicates with the terminal
+pub struct TerminalView {
+    terminal: Model<Terminal>,
+    focus_handle: FocusHandle,
+    has_new_content: bool,
+    //Currently using iTerm bell, show bell emoji in tab until input is received
+    has_bell: bool,
+    context_menu: Option<View<ContextMenu>>,
+    blink_state: bool,
+    blinking_on: bool,
+    blinking_paused: bool,
+    blink_epoch: usize,
+    can_navigate_to_selected_word: bool,
+    workspace_id: WorkspaceId,
+}
+
+impl EventEmitter<Event> for TerminalView {}
+impl EventEmitter<ItemEvent> for TerminalView {}
+impl EventEmitter<SearchEvent> for TerminalView {}
+
+impl FocusableView for TerminalView {
+    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl TerminalView {
+    ///Create a new Terminal in the current working directory or the user's home directory
+    pub fn deploy(
+        workspace: &mut Workspace,
+        _: &NewCenterTerminal,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        let strategy = TerminalSettings::get_global(cx);
+        let working_directory =
+            get_working_directory(workspace, cx, strategy.working_directory.clone());
+
+        let window = cx.window_handle();
+        let terminal = workspace
+            .project()
+            .update(cx, |project, cx| {
+                project.create_terminal(working_directory, window, cx)
+            })
+            .notify_err(workspace, cx);
+
+        if let Some(terminal) = terminal {
+            let view = cx.build_view(|cx| {
+                TerminalView::new(
+                    terminal,
+                    workspace.weak_handle(),
+                    workspace.database_id(),
+                    cx,
+                )
+            });
+            workspace.add_item(Box::new(view), cx)
+        }
+    }
+
+    pub fn new(
+        terminal: Model<Terminal>,
+        workspace: WeakView<Workspace>,
+        workspace_id: WorkspaceId,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let view_id = cx.entity_id();
+        cx.observe(&terminal, |_, _, cx| cx.notify()).detach();
+        cx.subscribe(&terminal, move |this, _, event, cx| match event {
+            Event::Wakeup => {
+                if !this.focus_handle.is_focused(cx) {
+                    this.has_new_content = true;
+                }
+                cx.notify();
+                cx.emit(Event::Wakeup);
+                cx.emit(ItemEvent::UpdateTab);
+                cx.emit(SearchEvent::MatchesInvalidated);
+            }
+
+            Event::Bell => {
+                this.has_bell = true;
+                cx.emit(Event::Wakeup);
+            }
+
+            Event::BlinkChanged => this.blinking_on = !this.blinking_on,
+
+            Event::TitleChanged => {
+                cx.emit(ItemEvent::UpdateTab);
+                if let Some(foreground_info) = &this.terminal().read(cx).foreground_process_info {
+                    let cwd = foreground_info.cwd.clone();
+
+                    let item_id = cx.entity_id();
+                    let workspace_id = this.workspace_id;
+                    cx.background_executor()
+                        .spawn(async move {
+                            TERMINAL_DB
+                                .save_working_directory(item_id.as_u64(), workspace_id, cwd)
+                                .await
+                                .log_err();
+                        })
+                        .detach();
+                }
+            }
+
+            Event::NewNavigationTarget(maybe_navigation_target) => {
+                this.can_navigate_to_selected_word = match maybe_navigation_target {
+                    Some(MaybeNavigationTarget::Url(_)) => true,
+                    Some(MaybeNavigationTarget::PathLike(maybe_path)) => {
+                        !possible_open_targets(&workspace, maybe_path, cx).is_empty()
+                    }
+                    None => false,
+                }
+            }
+
+            Event::Open(maybe_navigation_target) => match maybe_navigation_target {
+                MaybeNavigationTarget::Url(url) => cx.open_url(url),
+
+                MaybeNavigationTarget::PathLike(maybe_path) => {
+                    if !this.can_navigate_to_selected_word {
+                        return;
+                    }
+                    let potential_abs_paths = possible_open_targets(&workspace, maybe_path, cx);
+                    if let Some(path) = potential_abs_paths.into_iter().next() {
+                        let is_dir = path.path_like.is_dir();
+                        let task_workspace = workspace.clone();
+                        cx.spawn(|_, mut cx| async move {
+                            let opened_items = task_workspace
+                                .update(&mut cx, |workspace, cx| {
+                                    workspace.open_paths(vec![path.path_like], is_dir, cx)
+                                })
+                                .context("workspace update")?
+                                .await;
+                            anyhow::ensure!(
+                                opened_items.len() == 1,
+                                "For a single path open, expected single opened item"
+                            );
+                            let opened_item = opened_items
+                                .into_iter()
+                                .next()
+                                .unwrap()
+                                .transpose()
+                                .context("path open")?;
+                            if is_dir {
+                                task_workspace.update(&mut cx, |workspace, cx| {
+                                    workspace.project().update(cx, |_, cx| {
+                                        cx.emit(project::Event::ActivateProjectPanel);
+                                    })
+                                })?;
+                            } else {
+                                if let Some(row) = path.row {
+                                    let col = path.column.unwrap_or(0);
+                                    if let Some(active_editor) =
+                                        opened_item.and_then(|item| item.downcast::<Editor>())
+                                    {
+                                        active_editor
+                                            .downgrade()
+                                            .update(&mut cx, |editor, cx| {
+                                                let snapshot = editor.snapshot(cx).display_snapshot;
+                                                let point = snapshot.buffer_snapshot.clip_point(
+                                                    language::Point::new(
+                                                        row.saturating_sub(1),
+                                                        col.saturating_sub(1),
+                                                    ),
+                                                    Bias::Left,
+                                                );
+                                                editor.change_selections(
+                                                    Some(Autoscroll::center()),
+                                                    cx,
+                                                    |s| s.select_ranges([point..point]),
+                                                );
+                                            })
+                                            .log_err();
+                                    }
+                                }
+                            }
+                            anyhow::Ok(())
+                        })
+                        .detach_and_log_err(cx);
+                    }
+                }
+            },
+            Event::BreadcrumbsChanged => cx.emit(ItemEvent::UpdateBreadcrumbs),
+            Event::CloseTerminal => cx.emit(ItemEvent::CloseItem),
+            Event::SelectionsChanged => cx.emit(SearchEvent::ActiveMatchChanged),
+        })
+        .detach();
+
+        Self {
+            terminal,
+            has_new_content: true,
+            has_bell: false,
+            focus_handle: cx.focus_handle(),
+            context_menu: None,
+            blink_state: true,
+            blinking_on: false,
+            blinking_paused: false,
+            blink_epoch: 0,
+            can_navigate_to_selected_word: false,
+            workspace_id,
+        }
+    }
+
+    pub fn model(&self) -> &Model<Terminal> {
+        &self.terminal
+    }
+
+    pub fn has_new_content(&self) -> bool {
+        self.has_new_content
+    }
+
+    pub fn has_bell(&self) -> bool {
+        self.has_bell
+    }
+
+    pub fn clear_bel(&mut self, cx: &mut ViewContext<TerminalView>) {
+        self.has_bell = false;
+        cx.emit(Event::Wakeup);
+    }
+
+    pub fn deploy_context_menu(
+        &mut self,
+        position: gpui::Point<Pixels>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.context_menu = Some(cx.build_view(|cx| {
+            ContextMenu::new(cx)
+                .entry(Label::new("Clear"), Box::new(Clear))
+                .entry(
+                    Label::new("Close"),
+                    Box::new(CloseActiveItem { save_intent: None }),
+                )
+        }));
+        dbg!(&position);
+        // todo!()
+        //     self.context_menu
+        //         .show(position, AnchorCorner::TopLeft, menu_entries, cx);
+        //     cx.notify();
+    }
+
+    fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
+        if !self
+            .terminal
+            .read(cx)
+            .last_content
+            .mode
+            .contains(TermMode::ALT_SCREEN)
+        {
+            cx.show_character_palette();
+        } else {
+            self.terminal.update(cx, |term, cx| {
+                term.try_keystroke(
+                    &Keystroke::parse("ctrl-cmd-space").unwrap(),
+                    TerminalSettings::get_global(cx).option_as_meta,
+                )
+            });
+        }
+    }
+
+    fn select_all(&mut self, _: &editor::SelectAll, cx: &mut ViewContext<Self>) {
+        self.terminal.update(cx, |term, _| term.select_all());
+        cx.notify();
+    }
+
+    fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
+        self.terminal.update(cx, |term, _| term.clear());
+        cx.notify();
+    }
+
+    pub fn should_show_cursor(&self, focused: bool, cx: &mut gpui::ViewContext<Self>) -> bool {
+        //Don't blink the cursor when not focused, blinking is disabled, or paused
+        if !focused
+            || !self.blinking_on
+            || self.blinking_paused
+            || self
+                .terminal
+                .read(cx)
+                .last_content
+                .mode
+                .contains(TermMode::ALT_SCREEN)
+        {
+            return true;
+        }
+
+        match TerminalSettings::get_global(cx).blinking {
+            //If the user requested to never blink, don't blink it.
+            TerminalBlink::Off => true,
+            //If the terminal is controlling it, check terminal mode
+            TerminalBlink::TerminalControlled | TerminalBlink::On => self.blink_state,
+        }
+    }
+
+    fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
+        if epoch == self.blink_epoch && !self.blinking_paused {
+            self.blink_state = !self.blink_state;
+            cx.notify();
+
+            let epoch = self.next_blink_epoch();
+            cx.spawn(|this, mut cx| async move {
+                Timer::after(CURSOR_BLINK_INTERVAL).await;
+                this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx))
+                    .log_err();
+            })
+            .detach();
+        }
+    }
+
+    pub fn pause_cursor_blinking(&mut self, cx: &mut ViewContext<Self>) {
+        self.blink_state = true;
+        cx.notify();
+
+        let epoch = self.next_blink_epoch();
+        cx.spawn(|this, mut cx| async move {
+            Timer::after(CURSOR_BLINK_INTERVAL).await;
+            this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
+                .ok();
+        })
+        .detach();
+    }
+
+    pub fn find_matches(
+        &mut self,
+        query: Arc<project::search::SearchQuery>,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Vec<RangeInclusive<Point>>> {
+        let searcher = regex_search_for_query(&query);
+
+        if let Some(searcher) = searcher {
+            self.terminal
+                .update(cx, |term, cx| term.find_matches(searcher, cx))
+        } else {
+            cx.background_executor().spawn(async { Vec::new() })
+        }
+    }
+
+    pub fn terminal(&self) -> &Model<Terminal> {
+        &self.terminal
+    }
+
+    fn next_blink_epoch(&mut self) -> usize {
+        self.blink_epoch += 1;
+        self.blink_epoch
+    }
+
+    fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
+        if epoch == self.blink_epoch {
+            self.blinking_paused = false;
+            self.blink_cursors(epoch, cx);
+        }
+    }
+
+    ///Attempt to paste the clipboard into the terminal
+    fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
+        self.terminal.update(cx, |term, _| term.copy())
+    }
+
+    ///Attempt to paste the clipboard into the terminal
+    fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
+        if let Some(item) = cx.read_from_clipboard() {
+            self.terminal
+                .update(cx, |terminal, _cx| terminal.paste(item.text()));
+        }
+    }
+
+    fn send_text(&mut self, text: &SendText, cx: &mut ViewContext<Self>) {
+        self.clear_bel(cx);
+        self.terminal.update(cx, |term, _| {
+            term.input(text.0.to_string());
+        });
+    }
+
+    fn send_keystroke(&mut self, text: &SendKeystroke, cx: &mut ViewContext<Self>) {
+        if let Some(keystroke) = Keystroke::parse(&text.0).log_err() {
+            self.clear_bel(cx);
+            self.terminal.update(cx, |term, cx| {
+                term.try_keystroke(&keystroke, TerminalSettings::get_global(cx).option_as_meta);
+            });
+        }
+    }
+}
+
+fn possible_open_targets(
+    workspace: &WeakView<Workspace>,
+    maybe_path: &String,
+    cx: &mut ViewContext<'_, TerminalView>,
+) -> Vec<PathLikeWithPosition<PathBuf>> {
+    let path_like = PathLikeWithPosition::parse_str(maybe_path.as_str(), |path_str| {
+        Ok::<_, std::convert::Infallible>(Path::new(path_str).to_path_buf())
+    })
+    .expect("infallible");
+    let maybe_path = path_like.path_like;
+    let potential_abs_paths = if maybe_path.is_absolute() {
+        vec![maybe_path]
+    } else if maybe_path.starts_with("~") {
+        if let Some(abs_path) = maybe_path
+            .strip_prefix("~")
+            .ok()
+            .and_then(|maybe_path| Some(dirs::home_dir()?.join(maybe_path)))
+        {
+            vec![abs_path]
+        } else {
+            Vec::new()
+        }
+    } else if let Some(workspace) = workspace.upgrade() {
+        workspace.update(cx, |workspace, cx| {
+            workspace
+                .worktrees(cx)
+                .map(|worktree| worktree.read(cx).abs_path().join(&maybe_path))
+                .collect()
+        })
+    } else {
+        Vec::new()
+    };
+
+    potential_abs_paths
+        .into_iter()
+        .filter(|path| path.exists())
+        .map(|path| PathLikeWithPosition {
+            path_like: path,
+            row: path_like.row,
+            column: path_like.column,
+        })
+        .collect()
+}
+
+pub fn regex_search_for_query(query: &project::search::SearchQuery) -> Option<RegexSearch> {
+    let query = query.as_str();
+    let searcher = RegexSearch::new(&query);
+    searcher.ok()
+}
+
+impl TerminalView {
+    fn key_down(
+        &mut self,
+        event: &KeyDownEvent,
+        _dispatch_phase: DispatchPhase,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.clear_bel(cx);
+        self.pause_cursor_blinking(cx);
+
+        self.terminal.update(cx, |term, cx| {
+            term.try_keystroke(
+                &event.keystroke,
+                TerminalSettings::get_global(cx).option_as_meta,
+            )
+        });
+    }
+
+    fn focus_in(&mut self, event: &FocusEvent, cx: &mut ViewContext<Self>) {
+        self.has_new_content = false;
+        self.terminal.read(cx).focus_in();
+        self.blink_cursors(self.blink_epoch, cx);
+        cx.notify();
+    }
+
+    fn focus_out(&mut self, event: &FocusEvent, cx: &mut ViewContext<Self>) {
+        self.terminal.update(cx, |terminal, _| {
+            terminal.focus_out();
+        });
+        cx.notify();
+    }
+}
+
+impl Render for TerminalView {
+    type Element = Focusable<Self, Div<Self>>;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        let terminal_handle = self.terminal.clone().downgrade();
+
+        let self_id = cx.entity_id();
+        let focused = self.focus_handle.is_focused(cx);
+
+        div()
+            .relative()
+            .child(
+                div()
+                    .z_index(0)
+                    .absolute()
+                    .on_key_down(Self::key_down)
+                    .on_action(TerminalView::send_text)
+                    .on_action(TerminalView::send_keystroke)
+                    .on_action(TerminalView::copy)
+                    .on_action(TerminalView::paste)
+                    .on_action(TerminalView::clear)
+                    .on_action(TerminalView::show_character_palette)
+                    .on_action(TerminalView::select_all)
+                    // todo!()
+                    .child(
+                        "TERMINAL HERE", //     TerminalElement::new(
+                                         //     terminal_handle,
+                                         //     focused,
+                                         //     self.should_show_cursor(focused, cx),
+                                         //     self.can_navigate_to_selected_word,
+                                         // )
+                    )
+                    .on_mouse_down(MouseButton::Right, |this, event, cx| {
+                        this.deploy_context_menu(event.position, cx);
+                        cx.notify();
+                    }),
+            )
+            .children(
+                self.context_menu
+                    .clone()
+                    .map(|context_menu| div().z_index(1).absolute().child(context_menu.render())),
+            )
+            .track_focus(&self.focus_handle)
+            .on_focus_in(Self::focus_in)
+            .on_focus_out(Self::focus_out)
+    }
+}
+
+// impl View for TerminalView {
+//todo!()
+// fn modifiers_changed(
+//     &mut self,
+//     event: &ModifiersChangedEvent,
+//     cx: &mut ViewContext<Self>,
+// ) -> bool {
+//     let handled = self
+//         .terminal()
+//         .update(cx, |term, _| term.try_modifiers_change(&event.modifiers));
+//     if handled {
+//         cx.notify();
+//     }
+//     handled
+// }
+// }
+
+// todo!()
+// fn update_keymap_context(&self, keymap: &mut KeymapContext, cx: &gpui::AppContext) {
+//     Self::reset_to_default_keymap_context(keymap);
+
+//     let mode = self.terminal.read(cx).last_content.mode;
+//     keymap.add_key(
+//         "screen",
+//         if mode.contains(TermMode::ALT_SCREEN) {
+//             "alt"
+//         } else {
+//             "normal"
+//         },
+//     );
+
+//     if mode.contains(TermMode::APP_CURSOR) {
+//         keymap.add_identifier("DECCKM");
+//     }
+//     if mode.contains(TermMode::APP_KEYPAD) {
+//         keymap.add_identifier("DECPAM");
+//     } else {
+//         keymap.add_identifier("DECPNM");
+//     }
+//     if mode.contains(TermMode::SHOW_CURSOR) {
+//         keymap.add_identifier("DECTCEM");
+//     }
+//     if mode.contains(TermMode::LINE_WRAP) {
+//         keymap.add_identifier("DECAWM");
+//     }
+//     if mode.contains(TermMode::ORIGIN) {
+//         keymap.add_identifier("DECOM");
+//     }
+//     if mode.contains(TermMode::INSERT) {
+//         keymap.add_identifier("IRM");
+//     }
+//     //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
+//     if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
+//         keymap.add_identifier("LNM");
+//     }
+//     if mode.contains(TermMode::FOCUS_IN_OUT) {
+//         keymap.add_identifier("report_focus");
+//     }
+//     if mode.contains(TermMode::ALTERNATE_SCROLL) {
+//         keymap.add_identifier("alternate_scroll");
+//     }
+//     if mode.contains(TermMode::BRACKETED_PASTE) {
+//         keymap.add_identifier("bracketed_paste");
+//     }
+//     if mode.intersects(TermMode::MOUSE_MODE) {
+//         keymap.add_identifier("any_mouse_reporting");
+//     }
+//     {
+//         let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
+//             "click"
+//         } else if mode.contains(TermMode::MOUSE_DRAG) {
+//             "drag"
+//         } else if mode.contains(TermMode::MOUSE_MOTION) {
+//             "motion"
+//         } else {
+//             "off"
+//         };
+//         keymap.add_key("mouse_reporting", mouse_reporting);
+//     }
+//     {
+//         let format = if mode.contains(TermMode::SGR_MOUSE) {
+//             "sgr"
+//         } else if mode.contains(TermMode::UTF8_MOUSE) {
+//             "utf8"
+//         } else {
+//             "normal"
+//         };
+//         keymap.add_key("mouse_format", format);
+//     }
+// }
+
+impl InputHandler for TerminalView {
+    fn text_for_range(
+        &mut self,
+        range: std::ops::Range<usize>,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<String> {
+        todo!()
+    }
+
+    fn selected_text_range(
+        &mut self,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<std::ops::Range<usize>> {
+        if self
+            .terminal
+            .read(cx)
+            .last_content
+            .mode
+            .contains(TermMode::ALT_SCREEN)
+        {
+            None
+        } else {
+            Some(0..0)
+        }
+    }
+
+    fn marked_text_range(&self, cx: &mut ViewContext<Self>) -> Option<std::ops::Range<usize>> {
+        todo!()
+    }
+
+    fn unmark_text(&mut self, cx: &mut ViewContext<Self>) {
+        todo!()
+    }
+
+    fn replace_text_in_range(
+        &mut self,
+        _: Option<std::ops::Range<usize>>,
+        text: &str,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.terminal.update(cx, |terminal, _| {
+            terminal.input(text.into());
+        });
+    }
+
+    fn replace_and_mark_text_in_range(
+        &mut self,
+        range: Option<std::ops::Range<usize>>,
+        new_text: &str,
+        new_selected_range: Option<std::ops::Range<usize>>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        todo!()
+    }
+
+    fn bounds_for_range(
+        &mut self,
+        range_utf16: std::ops::Range<usize>,
+        element_bounds: gpui::Bounds<Pixels>,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<gpui::Bounds<Pixels>> {
+        todo!()
+    }
+}
+
+impl Item for TerminalView {
+    fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
+        Some(self.terminal().read(cx).title().into())
+    }
+
+    fn tab_content<T: 'static>(
+        &self,
+        _detail: Option<usize>,
+        cx: &gpui::AppContext,
+    ) -> AnyElement<T> {
+        let title = self.terminal().read(cx).title();
+
+        div()
+            .child(img().uri("icons/terminal.svg").bg(red()))
+            .child(title)
+            .render()
+    }
+
+    fn clone_on_split(
+        &self,
+        _workspace_id: WorkspaceId,
+        _cx: &mut ViewContext<Self>,
+    ) -> Option<View<Self>> {
+        //From what I can tell, there's no  way to tell the current working
+        //Directory of the terminal from outside the shell. There might be
+        //solutions to this, but they are non-trivial and require more IPC
+
+        // Some(TerminalContainer::new(
+        //     Err(anyhow::anyhow!("failed to instantiate terminal")),
+        //     workspace_id,
+        //     cx,
+        // ))
+
+        // TODO
+        None
+    }
+
+    fn is_dirty(&self, _cx: &gpui::AppContext) -> bool {
+        self.has_bell()
+    }
+
+    fn has_conflict(&self, _cx: &AppContext) -> bool {
+        false
+    }
+
+    // todo!()
+    // fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+    //     Some(Box::new(handle.clone()))
+    // }
+
+    fn breadcrumb_location(&self) -> ToolbarItemLocation {
+        ToolbarItemLocation::PrimaryLeft { flex: None }
+    }
+
+    fn breadcrumbs(&self, _: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
+        Some(vec![BreadcrumbText {
+            text: self.terminal().read(cx).breadcrumb_text.clone(),
+            highlights: None,
+        }])
+    }
+
+    fn serialized_item_kind() -> Option<&'static str> {
+        Some("Terminal")
+    }
+
+    fn deserialize(
+        project: Model<Project>,
+        workspace: WeakView<Workspace>,
+        workspace_id: workspace::WorkspaceId,
+        item_id: workspace::ItemId,
+        cx: &mut ViewContext<Pane>,
+    ) -> Task<anyhow::Result<View<Self>>> {
+        let window = cx.window_handle();
+        cx.spawn(|pane, mut cx| async move {
+            let cwd = None;
+            // todo!()
+            // TERMINAL_DB
+            // .get_working_directory(item_id, workspace_id)
+            // .log_err()
+            // .flatten()
+            // .or_else(|| {
+            //     cx.read(|cx| {
+            //         let strategy = TerminalSettings::get_global(cx).working_directory.clone();
+            //         workspace
+            //             .upgrade()
+            //             .map(|workspace| {
+            //                 get_working_directory(workspace.read(cx), cx, strategy)
+            //             })
+            //             .flatten()
+            //     })
+            // });
+
+            let terminal = project.update(&mut cx, |project, cx| {
+                project.create_terminal(cwd, window, cx)
+            })??;
+            pane.update(&mut cx, |_, cx| {
+                cx.build_view(|cx| TerminalView::new(terminal, workspace, workspace_id, cx))
+            })
+        })
+    }
+
+    fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
+        // todo!()
+        // cx.background()
+        //     .spawn(TERMINAL_DB.update_workspace_id(
+        //         workspace.database_id(),
+        //         self.workspace_id,
+        //         cx.view_id(),
+        //     ))
+        //     .detach();
+        self.workspace_id = workspace.database_id();
+    }
+}
+
+impl SearchableItem for TerminalView {
+    type Match = RangeInclusive<Point>;
+
+    fn supported_options() -> SearchOptions {
+        SearchOptions {
+            case: false,
+            word: false,
+            regex: false,
+            replacement: false,
+        }
+    }
+
+    /// Clear stored matches
+    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
+        self.terminal().update(cx, |term, _| term.matches.clear())
+    }
+
+    /// Store matches returned from find_matches somewhere for rendering
+    fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
+        self.terminal().update(cx, |term, _| term.matches = matches)
+    }
+
+    /// Return the selection content to pre-load into this search
+    fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
+        self.terminal()
+            .read(cx)
+            .last_content
+            .selection_text
+            .clone()
+            .unwrap_or_default()
+    }
+
+    /// Focus match at given index into the Vec of matches
+    fn activate_match(&mut self, index: usize, _: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
+        self.terminal()
+            .update(cx, |term, _| term.activate_match(index));
+        cx.notify();
+    }
+
+    /// Add selections for all matches given.
+    fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
+        self.terminal()
+            .update(cx, |term, _| term.select_matches(matches));
+        cx.notify();
+    }
+
+    /// Get all of the matches for this query, should be done on the background
+    fn find_matches(
+        &mut self,
+        query: Arc<project::search::SearchQuery>,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Vec<Self::Match>> {
+        if let Some(searcher) = regex_search_for_query(&query) {
+            self.terminal()
+                .update(cx, |term, cx| term.find_matches(searcher, cx))
+        } else {
+            Task::ready(vec![])
+        }
+    }
+
+    /// Reports back to the search toolbar what the active match should be (the selection)
+    fn active_match_index(
+        &mut self,
+        matches: Vec<Self::Match>,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<usize> {
+        // Selection head might have a value if there's a selection that isn't
+        // associated with a match. Therefore, if there are no matches, we should
+        // report None, no matter the state of the terminal
+        let res = if matches.len() > 0 {
+            if let Some(selection_head) = self.terminal().read(cx).selection_head {
+                // If selection head is contained in a match. Return that match
+                if let Some(ix) = matches
+                    .iter()
+                    .enumerate()
+                    .find(|(_, search_match)| {
+                        search_match.contains(&selection_head)
+                            || search_match.start() > &selection_head
+                    })
+                    .map(|(ix, _)| ix)
+                {
+                    Some(ix)
+                } else {
+                    // If no selection after selection head, return the last match
+                    Some(matches.len().saturating_sub(1))
+                }
+            } else {
+                // Matches found but no active selection, return the first last one (closest to cursor)
+                Some(matches.len().saturating_sub(1))
+            }
+        } else {
+            None
+        };
+
+        res
+    }
+    fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext<Self>) {
+        // Replacement is not supported in terminal view, so this is a no-op.
+    }
+}
+
+///Get's the working directory for the given workspace, respecting the user's settings.
+pub fn get_working_directory(
+    workspace: &Workspace,
+    cx: &AppContext,
+    strategy: WorkingDirectory,
+) -> Option<PathBuf> {
+    let res = match strategy {
+        WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx)
+            .or_else(|| first_project_directory(workspace, cx)),
+        WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
+        WorkingDirectory::AlwaysHome => None,
+        WorkingDirectory::Always { directory } => {
+            shellexpand::full(&directory) //TODO handle this better
+                .ok()
+                .map(|dir| Path::new(&dir.to_string()).to_path_buf())
+                .filter(|dir| dir.is_dir())
+        }
+    };
+    res.or_else(home_dir)
+}
+
+///Get's the first project's home directory, or the home directory
+fn first_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
+    workspace
+        .worktrees(cx)
+        .next()
+        .and_then(|worktree_handle| worktree_handle.read(cx).as_local())
+        .and_then(get_path_from_wt)
+}
+
+///Gets the intuitively correct working directory from the given workspace
+///If there is an active entry for this project, returns that entry's worktree root.
+///If there's no active entry but there is a worktree, returns that worktrees root.
+///If either of these roots are files, or if there are any other query failures,
+///  returns the user's home directory
+fn current_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
+    let project = workspace.project().read(cx);
+
+    project
+        .active_entry()
+        .and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
+        .or_else(|| workspace.worktrees(cx).next())
+        .and_then(|worktree_handle| worktree_handle.read(cx).as_local())
+        .and_then(get_path_from_wt)
+}
+
+fn get_path_from_wt(wt: &LocalWorktree) -> Option<PathBuf> {
+    wt.root_entry()
+        .filter(|re| re.is_dir())
+        .map(|_| wt.abs_path().to_path_buf())
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use gpui::TestAppContext;
+    use project::{Entry, Project, ProjectPath, Worktree};
+    use std::path::Path;
+    use workspace::AppState;
+
+    // Working directory calculation tests
+
+    // No Worktrees in project -> home_dir()
+    #[gpui::test]
+    async fn no_worktree(cx: &mut TestAppContext) {
+        let (project, workspace) = init_test(cx).await;
+        cx.read(|cx| {
+            let workspace = workspace.read(cx);
+            let active_entry = project.read(cx).active_entry();
+
+            //Make sure environment is as expected
+            assert!(active_entry.is_none());
+            assert!(workspace.worktrees(cx).next().is_none());
+
+            let res = current_project_directory(workspace, cx);
+            assert_eq!(res, None);
+            let res = first_project_directory(workspace, cx);
+            assert_eq!(res, None);
+        });
+    }
+
+    // No active entry, but a worktree, worktree is a file -> home_dir()
+    #[gpui::test]
+    async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) {
+        let (project, workspace) = init_test(cx).await;
+
+        create_file_wt(project.clone(), "/root.txt", cx).await;
+        cx.read(|cx| {
+            let workspace = workspace.read(cx);
+            let active_entry = project.read(cx).active_entry();
+
+            //Make sure environment is as expected
+            assert!(active_entry.is_none());
+            assert!(workspace.worktrees(cx).next().is_some());
+
+            let res = current_project_directory(workspace, cx);
+            assert_eq!(res, None);
+            let res = first_project_directory(workspace, cx);
+            assert_eq!(res, None);
+        });
+    }
+
+    // No active entry, but a worktree, worktree is a folder -> worktree_folder
+    #[gpui::test]
+    async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) {
+        let (project, workspace) = init_test(cx).await;
+
+        let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await;
+        cx.update(|cx| {
+            let workspace = workspace.read(cx);
+            let active_entry = project.read(cx).active_entry();
+
+            assert!(active_entry.is_none());
+            assert!(workspace.worktrees(cx).next().is_some());
+
+            let res = current_project_directory(workspace, cx);
+            assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
+            let res = first_project_directory(workspace, cx);
+            assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
+        });
+    }
+
+    // Active entry with a work tree, worktree is a file -> home_dir()
+    #[gpui::test]
+    async fn active_entry_worktree_is_file(cx: &mut TestAppContext) {
+        let (project, workspace) = init_test(cx).await;
+
+        let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
+        let (wt2, entry2) = create_file_wt(project.clone(), "/root2.txt", cx).await;
+        insert_active_entry_for(wt2, entry2, project.clone(), cx);
+
+        cx.update(|cx| {
+            let workspace = workspace.read(cx);
+            let active_entry = project.read(cx).active_entry();
+
+            assert!(active_entry.is_some());
+
+            let res = current_project_directory(workspace, cx);
+            assert_eq!(res, None);
+            let res = first_project_directory(workspace, cx);
+            assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
+        });
+    }
+
+    // Active entry, with a worktree, worktree is a folder -> worktree_folder
+    #[gpui::test]
+    async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) {
+        let (project, workspace) = init_test(cx).await;
+
+        let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
+        let (wt2, entry2) = create_folder_wt(project.clone(), "/root2/", cx).await;
+        insert_active_entry_for(wt2, entry2, project.clone(), cx);
+
+        cx.update(|cx| {
+            let workspace = workspace.read(cx);
+            let active_entry = project.read(cx).active_entry();
+
+            assert!(active_entry.is_some());
+
+            let res = current_project_directory(workspace, cx);
+            assert_eq!(res, Some((Path::new("/root2/")).to_path_buf()));
+            let res = first_project_directory(workspace, cx);
+            assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
+        });
+    }
+
+    /// Creates a worktree with 1 file: /root.txt
+    pub async fn init_test(cx: &mut TestAppContext) -> (Model<Project>, View<Workspace>) {
+        let params = cx.update(AppState::test);
+        cx.update(|cx| {
+            theme::init(theme::LoadThemes::JustBase, cx);
+            Project::init_settings(cx);
+            language::init(cx);
+        });
+
+        let project = Project::test(params.fs.clone(), [], cx).await;
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root_view(cx)
+            .unwrap();
+
+        (project, workspace)
+    }
+
+    /// Creates a worktree with 1 folder: /root{suffix}/
+    async fn create_folder_wt(
+        project: Model<Project>,
+        path: impl AsRef<Path>,
+        cx: &mut TestAppContext,
+    ) -> (Model<Worktree>, Entry) {
+        create_wt(project, true, path, cx).await
+    }
+
+    /// Creates a worktree with 1 file: /root{suffix}.txt
+    async fn create_file_wt(
+        project: Model<Project>,
+        path: impl AsRef<Path>,
+        cx: &mut TestAppContext,
+    ) -> (Model<Worktree>, Entry) {
+        create_wt(project, false, path, cx).await
+    }
+
+    async fn create_wt(
+        project: Model<Project>,
+        is_dir: bool,
+        path: impl AsRef<Path>,
+        cx: &mut TestAppContext,
+    ) -> (Model<Worktree>, Entry) {
+        let (wt, _) = project
+            .update(cx, |project, cx| {
+                project.find_or_create_local_worktree(path, true, cx)
+            })
+            .await
+            .unwrap();
+
+        let entry = cx
+            .update(|cx| {
+                wt.update(cx, |wt, cx| {
+                    wt.as_local()
+                        .unwrap()
+                        .create_entry(Path::new(""), is_dir, cx)
+                })
+            })
+            .await
+            .unwrap();
+
+        (wt, entry)
+    }
+
+    pub fn insert_active_entry_for(
+        wt: Model<Worktree>,
+        entry: Entry,
+        project: Model<Project>,
+        cx: &mut TestAppContext,
+    ) {
+        cx.update(|cx| {
+            let p = ProjectPath {
+                worktree_id: wt.read(cx).id(),
+                path: entry.path,
+            };
+            project.update(cx, |project, cx| project.set_active_path(Some(p), cx));
+        });
+    }
+}

crates/theme2/src/registry.rs 🔗

@@ -100,6 +100,11 @@ impl ThemeRegistry {
             .ok_or_else(|| anyhow!("theme not found: {}", name))
             .cloned()
     }
+
+    pub fn load_user_themes(&mut self) {
+        #[cfg(not(feature = "importing-themes"))]
+        self.insert_user_theme_familes(crate::all_user_themes());
+    }
 }
 
 impl Default for ThemeRegistry {
@@ -110,9 +115,6 @@ impl Default for ThemeRegistry {
 
         this.insert_theme_families([one_family()]);
 
-        #[cfg(not(feature = "importing-themes"))]
-        this.insert_user_theme_familes(crate::all_user_themes());
-
         this
     }
 }

crates/theme2/src/settings.rs 🔗

@@ -34,6 +34,10 @@ pub struct ThemeSettingsContent {
     #[serde(default)]
     pub ui_font_size: Option<f32>,
     #[serde(default)]
+    pub ui_font_family: Option<String>,
+    #[serde(default)]
+    pub ui_font_features: Option<FontFeatures>,
+    #[serde(default)]
     pub buffer_font_family: Option<String>,
     #[serde(default)]
     pub buffer_font_size: Option<f32>,
@@ -117,13 +121,13 @@ impl settings::Settings for ThemeSettings {
         user_values: &[&Self::FileContent],
         cx: &mut AppContext,
     ) -> Result<Self> {
-        let themes = cx.default_global::<Arc<ThemeRegistry>>();
+        let themes = cx.default_global::<ThemeRegistry>();
 
         let mut this = Self {
-            ui_font_size: defaults.ui_font_size.unwrap_or(16.).into(),
+            ui_font_size: defaults.ui_font_size.unwrap().into(),
             ui_font: Font {
-                family: "Helvetica".into(),
-                features: Default::default(),
+                family: defaults.ui_font_family.clone().unwrap().into(),
+                features: defaults.ui_font_features.clone().unwrap(),
                 weight: Default::default(),
                 style: Default::default(),
             },
@@ -149,6 +153,13 @@ impl settings::Settings for ThemeSettings {
                 this.buffer_font.features = value;
             }
 
+            if let Some(value) = value.ui_font_family {
+                this.ui_font.family = value.into();
+            }
+            if let Some(value) = value.ui_font_features {
+                this.ui_font.features = value;
+            }
+
             if let Some(value) = &value.theme {
                 if let Some(theme) = themes.get(value).log_err() {
                     this.active_theme = theme;

crates/theme2/src/theme2.rs 🔗

@@ -31,8 +31,25 @@ pub enum Appearance {
     Dark,
 }
 
-pub fn init(cx: &mut AppContext) {
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum LoadThemes {
+    /// Only load the base theme.
+    ///
+    /// No user themes will be loaded.
+    JustBase,
+
+    /// Load all of the built-in themes.
+    All,
+}
+
+pub fn init(themes_to_load: LoadThemes, cx: &mut AppContext) {
     cx.set_global(ThemeRegistry::default());
+
+    match themes_to_load {
+        LoadThemes::JustBase => (),
+        LoadThemes::All => cx.global_mut::<ThemeRegistry>().load_user_themes(),
+    }
+
     ThemeSettings::register(cx);
 }
 

crates/ui2/Cargo.toml 🔗

@@ -9,6 +9,7 @@ anyhow.workspace = true
 chrono = "0.4"
 gpui = { package = "gpui2", path = "../gpui2" }
 itertools = { version = "0.11.0", optional = true }
+menu = { package = "menu2", path = "../menu2"}
 serde.workspace = true
 settings2 = { path = "../settings2" }
 smallvec.workspace = true

crates/ui2/src/components/button.rs 🔗

@@ -61,7 +61,7 @@ impl ButtonVariant {
     }
 }
 
-pub type ClickHandler<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>) + Send + Sync>;
+pub type ClickHandler<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>)>;
 
 struct ButtonHandlers<V: 'static> {
     click: Option<ClickHandler<V>>,
@@ -178,6 +178,7 @@ impl<V: 'static> Button<V> {
             .text_ui()
             .rounded_md()
             .bg(self.variant.bg_color(cx))
+            .cursor_pointer()
             .hover(|style| style.bg(self.variant.bg_color_hover(cx)))
             .active(|style| style.bg(self.variant.bg_color_active(cx)));
 

crates/ui2/src/components/context_menu.rs 🔗

@@ -1,60 +1,255 @@
-use crate::{prelude::*, ListItemVariant};
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use crate::prelude::*;
 use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader};
+use gpui::{
+    overlay, px, Action, AnchorCorner, AnyElement, Bounds, Dismiss, DispatchPhase, Div,
+    FocusHandle, LayoutId, ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View,
+};
 
-pub enum ContextMenuItem {
-    Header(SharedString),
-    Entry(Label),
-    Separator,
+pub struct ContextMenu {
+    items: Vec<ListItem>,
+    focus_handle: FocusHandle,
 }
 
-impl ContextMenuItem {
-    fn to_list_item<V: 'static>(self) -> ListItem {
-        match self {
-            ContextMenuItem::Header(label) => ListSubHeader::new(label).into(),
-            ContextMenuItem::Entry(label) => {
-                ListEntry::new(label).variant(ListItemVariant::Inset).into()
-            }
-            ContextMenuItem::Separator => ListSeparator::new().into(),
+impl ManagedView for ContextMenu {
+    fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl ContextMenu {
+    pub fn new(cx: &mut WindowContext) -> Self {
+        Self {
+            items: Default::default(),
+            focus_handle: cx.focus_handle(),
         }
     }
 
-    pub fn header(label: impl Into<SharedString>) -> Self {
-        Self::Header(label.into())
+    pub fn header(mut self, title: impl Into<SharedString>) -> Self {
+        self.items.push(ListItem::Header(ListSubHeader::new(title)));
+        self
     }
 
-    pub fn separator() -> Self {
-        Self::Separator
+    pub fn separator(mut self) -> Self {
+        self.items.push(ListItem::Separator(ListSeparator));
+        self
     }
 
-    pub fn entry(label: Label) -> Self {
-        Self::Entry(label)
+    pub fn entry(mut self, label: Label, action: Box<dyn Action>) -> Self {
+        self.items.push(ListEntry::new(label).action(action).into());
+        self
+    }
+
+    pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
+        // todo!()
+        cx.emit(Dismiss);
+    }
+
+    pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
+        cx.emit(Dismiss);
     }
 }
 
-#[derive(Component)]
-pub struct ContextMenu {
-    items: Vec<ContextMenuItem>,
+impl Render for ContextMenu {
+    type Element = Div<Self>;
+    // todo!()
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        div().elevation_2(cx).flex().flex_row().child(
+            v_stack()
+                .min_w(px(200.))
+                .track_focus(&self.focus_handle)
+                .on_mouse_down_out(|this: &mut Self, _, cx| this.cancel(&Default::default(), cx))
+                // .on_action(ContextMenu::select_first)
+                // .on_action(ContextMenu::select_last)
+                // .on_action(ContextMenu::select_next)
+                // .on_action(ContextMenu::select_prev)
+                .on_action(ContextMenu::confirm)
+                .on_action(ContextMenu::cancel)
+                .flex_none()
+                // .bg(cx.theme().colors().elevated_surface_background)
+                // .border()
+                // .border_color(cx.theme().colors().border)
+                .child(List::new(self.items.clone())),
+        )
+    }
 }
 
-impl ContextMenu {
-    pub fn new(items: impl IntoIterator<Item = ContextMenuItem>) -> Self {
-        Self {
-            items: items.into_iter().collect(),
+pub struct MenuHandle<V: 'static, M: ManagedView> {
+    id: Option<ElementId>,
+    child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement<V> + 'static>>,
+    menu_builder: Option<Rc<dyn Fn(&mut V, &mut ViewContext<V>) -> View<M> + 'static>>,
+
+    anchor: Option<AnchorCorner>,
+    attach: Option<AnchorCorner>,
+}
+
+impl<V: 'static, M: ManagedView> MenuHandle<V, M> {
+    pub fn id(mut self, id: impl Into<ElementId>) -> Self {
+        self.id = Some(id.into());
+        self
+    }
+
+    pub fn menu(mut self, f: impl Fn(&mut V, &mut ViewContext<V>) -> View<M> + 'static) -> Self {
+        self.menu_builder = Some(Rc::new(f));
+        self
+    }
+
+    pub fn child<R: Component<V>>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self {
+        self.child_builder = Some(Box::new(|b| f(b).render()));
+        self
+    }
+
+    /// anchor defines which corner of the menu to anchor to the attachment point
+    /// (by default the cursor position, but see attach)
+    pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
+        self.anchor = Some(anchor);
+        self
+    }
+
+    /// attach defines which corner of the handle to attach the menu's anchor to
+    pub fn attach(mut self, attach: AnchorCorner) -> Self {
+        self.attach = Some(attach);
+        self
+    }
+}
+
+pub fn menu_handle<V: 'static, M: ManagedView>() -> MenuHandle<V, M> {
+    MenuHandle {
+        id: None,
+        child_builder: None,
+        menu_builder: None,
+        anchor: None,
+        attach: None,
+    }
+}
+
+pub struct MenuHandleState<V, M> {
+    menu: Rc<RefCell<Option<View<M>>>>,
+    position: Rc<RefCell<Point<Pixels>>>,
+    child_layout_id: Option<LayoutId>,
+    child_element: Option<AnyElement<V>>,
+    menu_element: Option<AnyElement<V>>,
+}
+impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
+    type ElementState = MenuHandleState<V, M>;
+
+    fn element_id(&self) -> Option<gpui::ElementId> {
+        Some(self.id.clone().expect("menu_handle must have an id()"))
+    }
+
+    fn layout(
+        &mut self,
+        view_state: &mut V,
+        element_state: Option<Self::ElementState>,
+        cx: &mut crate::ViewContext<V>,
+    ) -> (gpui::LayoutId, Self::ElementState) {
+        let (menu, position) = if let Some(element_state) = element_state {
+            (element_state.menu, element_state.position)
+        } else {
+            (Rc::default(), Rc::default())
+        };
+
+        let mut menu_layout_id = None;
+
+        let menu_element = menu.borrow_mut().as_mut().map(|menu| {
+            let mut overlay = overlay::<V>().snap_to_window();
+            if let Some(anchor) = self.anchor {
+                overlay = overlay.anchor(anchor);
+            }
+            overlay = overlay.position(*position.borrow());
+
+            let mut view = overlay.child(menu.clone()).render();
+            menu_layout_id = Some(view.layout(view_state, cx));
+            view
+        });
+
+        let mut child_element = self
+            .child_builder
+            .take()
+            .map(|child_builder| (child_builder)(menu.borrow().is_some()));
+
+        let child_layout_id = child_element
+            .as_mut()
+            .map(|child_element| child_element.layout(view_state, cx));
+
+        let layout_id = cx.request_layout(
+            &gpui::Style::default(),
+            menu_layout_id.into_iter().chain(child_layout_id),
+        );
+
+        (
+            layout_id,
+            MenuHandleState {
+                menu,
+                position,
+                child_element,
+                child_layout_id,
+                menu_element,
+            },
+        )
+    }
+
+    fn paint(
+        &mut self,
+        bounds: Bounds<gpui::Pixels>,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut crate::ViewContext<V>,
+    ) {
+        if let Some(child) = element_state.child_element.as_mut() {
+            child.paint(view_state, cx);
         }
+
+        if let Some(menu) = element_state.menu_element.as_mut() {
+            menu.paint(view_state, cx);
+            return;
+        }
+
+        let Some(builder) = self.menu_builder.clone() else {
+            return;
+        };
+        let menu = element_state.menu.clone();
+        let position = element_state.position.clone();
+        let attach = self.attach.clone();
+        let child_layout_id = element_state.child_layout_id.clone();
+
+        cx.on_mouse_event(move |view_state, event: &MouseDownEvent, phase, cx| {
+            if phase == DispatchPhase::Bubble
+                && event.button == MouseButton::Right
+                && bounds.contains_point(&event.position)
+            {
+                cx.stop_propagation();
+                cx.prevent_default();
+
+                let new_menu = (builder)(view_state, cx);
+                let menu2 = menu.clone();
+                cx.subscribe(&new_menu, move |this, modal, e, cx| match e {
+                    &Dismiss => {
+                        *menu2.borrow_mut() = None;
+                        cx.notify();
+                    }
+                })
+                .detach();
+                *menu.borrow_mut() = Some(new_menu);
+
+                *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
+                    attach
+                        .unwrap()
+                        .corner(cx.layout_bounds(child_layout_id.unwrap()))
+                } else {
+                    cx.mouse_position()
+                };
+                cx.notify();
+            }
+        });
     }
+}
 
-    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        v_stack()
-            .flex()
-            .bg(cx.theme().colors().elevated_surface_background)
-            .border()
-            .border_color(cx.theme().colors().border)
-            .child(List::new(
-                self.items
-                    .into_iter()
-                    .map(ContextMenuItem::to_list_item::<V>)
-                    .collect(),
-            ))
+impl<V: 'static, M: ManagedView> Component<V> for MenuHandle<V, M> {
+    fn render(self) -> AnyElement<V> {
+        AnyElement::new(self)
     }
 }
 
@@ -65,7 +260,18 @@ pub use stories::*;
 mod stories {
     use super::*;
     use crate::story::Story;
-    use gpui::{Div, Render};
+    use gpui::{actions, Div, Render, VisualContext};
+
+    actions!(PrintCurrentDate);
+
+    fn build_menu(cx: &mut WindowContext, header: impl Into<SharedString>) -> View<ContextMenu> {
+        cx.build_view(|cx| {
+            ContextMenu::new(cx).header(header).separator().entry(
+                Label::new("Print current time"),
+                PrintCurrentDate.boxed_clone(),
+            )
+        })
+    }
 
     pub struct ContextMenuStory;
 
@@ -74,13 +280,83 @@ mod stories {
 
         fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             Story::container(cx)
-                .child(Story::title_for::<_, ContextMenu>(cx))
-                .child(Story::label(cx, "Default"))
-                .child(ContextMenu::new([
-                    ContextMenuItem::header("Section header"),
-                    ContextMenuItem::Separator,
-                    ContextMenuItem::entry(Label::new("Some entry")),
-                ]))
+                .on_action(|_, _: &PrintCurrentDate, _| {
+                    if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
+                        println!("Current Unix time is {:?}", unix_time.as_secs());
+                    }
+                })
+                .flex()
+                .flex_row()
+                .justify_between()
+                .child(
+                    div()
+                        .flex()
+                        .flex_col()
+                        .justify_between()
+                        .child(
+                            menu_handle()
+                                .id("test2")
+                                .child(|is_open| {
+                                    Label::new(if is_open {
+                                        "TOP LEFT"
+                                    } else {
+                                        "RIGHT CLICK ME"
+                                    })
+                                    .render()
+                                })
+                                .menu(move |_, cx| build_menu(cx, "top left")),
+                        )
+                        .child(
+                            menu_handle()
+                                .id("test1")
+                                .child(|is_open| {
+                                    Label::new(if is_open {
+                                        "BOTTOM LEFT"
+                                    } else {
+                                        "RIGHT CLICK ME"
+                                    })
+                                    .render()
+                                })
+                                .anchor(AnchorCorner::BottomLeft)
+                                .attach(AnchorCorner::TopLeft)
+                                .menu(move |_, cx| build_menu(cx, "bottom left")),
+                        ),
+                )
+                .child(
+                    div()
+                        .flex()
+                        .flex_col()
+                        .justify_between()
+                        .child(
+                            menu_handle()
+                                .id("test3")
+                                .child(|is_open| {
+                                    Label::new(if is_open {
+                                        "TOP RIGHT"
+                                    } else {
+                                        "RIGHT CLICK ME"
+                                    })
+                                    .render()
+                                })
+                                .anchor(AnchorCorner::TopRight)
+                                .menu(move |_, cx| build_menu(cx, "top right")),
+                        )
+                        .child(
+                            menu_handle()
+                                .id("test4")
+                                .child(|is_open| {
+                                    Label::new(if is_open {
+                                        "BOTTOM RIGHT"
+                                    } else {
+                                        "RIGHT CLICK ME"
+                                    })
+                                    .render()
+                                })
+                                .anchor(AnchorCorner::BottomRight)
+                                .attach(AnchorCorner::TopRight)
+                                .menu(move |_, cx| build_menu(cx, "bottom right")),
+                        ),
+                )
         }
     }
 }

crates/ui2/src/components/icon.rs 🔗

@@ -29,6 +29,7 @@ pub enum Icon {
     ChevronRight,
     ChevronUp,
     Close,
+    Collab,
     Copilot,
     Dash,
     Envelope,
@@ -87,6 +88,7 @@ impl Icon {
             Icon::ChevronRight => "icons/chevron_right.svg",
             Icon::ChevronUp => "icons/chevron_up.svg",
             Icon::Close => "icons/x.svg",
+            Icon::Collab => "icons/user_group_16.svg",
             Icon::Copilot => "icons/copilot.svg",
             Icon::Dash => "icons/dash.svg",
             Icon::Envelope => "icons/feedback.svg",

crates/ui2/src/components/icon_button.rs 🔗

@@ -1,5 +1,5 @@
-use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement, TextTooltip};
-use gpui::{prelude::*, MouseButton, VisualContext};
+use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement};
+use gpui::{prelude::*, Action, AnyView, MouseButton};
 use std::sync::Arc;
 
 struct IconButtonHandlers<V: 'static> {
@@ -19,7 +19,8 @@ pub struct IconButton<V: 'static> {
     color: TextColor,
     variant: ButtonVariant,
     state: InteractionState,
-    tooltip: Option<SharedString>,
+    selected: bool,
+    tooltip: Option<Box<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>>,
     handlers: IconButtonHandlers<V>,
 }
 
@@ -31,6 +32,7 @@ impl<V: 'static> IconButton<V> {
             color: TextColor::default(),
             variant: ButtonVariant::default(),
             state: InteractionState::default(),
+            selected: false,
             tooltip: None,
             handlers: IconButtonHandlers::default(),
         }
@@ -56,26 +58,36 @@ impl<V: 'static> IconButton<V> {
         self
     }
 
-    pub fn tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
-        self.tooltip = Some(tooltip.into());
+    pub fn selected(mut self, selected: bool) -> Self {
+        self.selected = selected;
         self
     }
 
-    pub fn on_click(
+    pub fn tooltip(
         mut self,
-        handler: impl 'static + Fn(&mut V, &mut ViewContext<V>) + Send + Sync,
+        tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static,
     ) -> Self {
+        self.tooltip = Some(Box::new(tooltip));
+        self
+    }
+
+    pub fn on_click(mut self, handler: impl 'static + Fn(&mut V, &mut ViewContext<V>)) -> Self {
         self.handlers.click = Some(Arc::new(handler));
         self
     }
 
-    fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+    pub fn action(self, action: Box<dyn Action>) -> Self {
+        self.on_click(move |this, cx| cx.dispatch_action(action.boxed_clone()))
+    }
+
+    fn render(mut self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let icon_color = match (self.state, self.color) {
             (InteractionState::Disabled, _) => TextColor::Disabled,
+            (InteractionState::Active, _) => TextColor::Selected,
             _ => self.color,
         };
 
-        let (bg_color, bg_hover_color, bg_active_color) = match self.variant {
+        let (mut bg_color, bg_hover_color, bg_active_color) = match self.variant {
             ButtonVariant::Filled => (
                 cx.theme().colors().element_background,
                 cx.theme().colors().element_hover,
@@ -88,12 +100,17 @@ impl<V: 'static> IconButton<V> {
             ),
         };
 
+        if self.selected {
+            bg_color = bg_hover_color;
+        }
+
         let mut button = h_stack()
             .id(self.id.clone())
             .justify_center()
             .rounded_md()
             .p_1()
             .bg(bg_color)
+            .cursor_pointer()
             .hover(|style| style.bg(bg_hover_color))
             .active(|style| style.bg(bg_active_color))
             .child(IconElement::new(self.icon).color(icon_color));
@@ -102,12 +119,13 @@ impl<V: 'static> IconButton<V> {
             button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
                 cx.stop_propagation();
                 click_handler(state, cx);
-            });
+            })
         }
 
-        if let Some(tooltip) = self.tooltip.clone() {
-            button =
-                button.tooltip(move |_, cx| cx.build_view(|cx| TextTooltip::new(tooltip.clone())));
+        if let Some(tooltip) = self.tooltip.take() {
+            if !self.selected {
+                button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx))
+            }
         }
 
         button

crates/ui2/src/components/keybinding.rs 🔗

@@ -81,13 +81,12 @@ pub use stories::*;
 mod stories {
     use super::*;
     use crate::Story;
-    use gpui::{action, Div, Render};
+    use gpui::{actions, Div, Render};
     use itertools::Itertools;
 
     pub struct KeybindingStory;
 
-    #[action]
-    struct NoAction {}
+    actions!(NoAction);
 
     pub fn binding(key: &str) -> gpui::KeyBinding {
         gpui::KeyBinding::new(key, NoAction {}, None)

crates/ui2/src/components/label.rs 🔗

@@ -60,7 +60,7 @@ pub enum LineHeightStyle {
     UILabel,
 }
 
-#[derive(Component)]
+#[derive(Clone, Component)]
 pub struct Label {
     label: SharedString,
     size: LabelSize,

crates/ui2/src/components/list.rs 🔗

@@ -1,11 +1,10 @@
-use gpui::div;
+use gpui::{div, Action};
 
-use crate::prelude::*;
 use crate::settings::user_settings;
 use crate::{
-    disclosure_control, h_stack, v_stack, Avatar, GraphicSlot, Icon, IconElement, IconSize, Label,
-    TextColor, Toggle,
+    disclosure_control, h_stack, v_stack, Avatar, Icon, IconElement, IconSize, Label, Toggle,
 };
+use crate::{prelude::*, GraphicSlot};
 
 #[derive(Clone, Copy, Default, Debug, PartialEq)]
 pub enum ListItemVariant {
@@ -118,7 +117,7 @@ impl ListHeader {
     }
 }
 
-#[derive(Component)]
+#[derive(Component, Clone)]
 pub struct ListSubHeader {
     label: SharedString,
     left_icon: Option<Icon>,
@@ -173,7 +172,7 @@ pub enum ListEntrySize {
     Medium,
 }
 
-#[derive(Component)]
+#[derive(Component, Clone)]
 pub enum ListItem {
     Entry(ListEntry),
     Separator(ListSeparator),
@@ -232,6 +231,25 @@ pub struct ListEntry {
     size: ListEntrySize,
     toggle: Toggle,
     variant: ListItemVariant,
+    on_click: Option<Box<dyn Action>>,
+}
+
+impl Clone for ListEntry {
+    fn clone(&self) -> Self {
+        Self {
+            disabled: self.disabled,
+            // TODO: Reintroduce this
+            // disclosure_control_style: DisclosureControlVisibility,
+            indent_level: self.indent_level,
+            label: self.label.clone(),
+            left_slot: self.left_slot.clone(),
+            overflow: self.overflow,
+            size: self.size,
+            toggle: self.toggle,
+            variant: self.variant,
+            on_click: self.on_click.as_ref().map(|opt| opt.boxed_clone()),
+        }
+    }
 }
 
 impl ListEntry {
@@ -245,9 +263,15 @@ impl ListEntry {
             size: ListEntrySize::default(),
             toggle: Toggle::NotToggleable,
             variant: ListItemVariant::default(),
+            on_click: Default::default(),
         }
     }
 
+    pub fn action(mut self, action: impl Into<Box<dyn Action>>) -> Self {
+        self.on_click = Some(action.into());
+        self
+    }
+
     pub fn variant(mut self, variant: ListItemVariant) -> Self {
         self.variant = variant;
         self
@@ -303,9 +327,21 @@ impl ListEntry {
             ListEntrySize::Small => div().h_6(),
             ListEntrySize::Medium => div().h_7(),
         };
-
         div()
             .relative()
+            .hover(|mut style| {
+                style.background = Some(cx.theme().colors().editor_background.into());
+                style
+            })
+            .on_mouse_down(gpui::MouseButton::Left, {
+                let action = self.on_click.map(|action| action.boxed_clone());
+
+                move |entry: &mut V, event, cx| {
+                    if let Some(action) = action.as_ref() {
+                        cx.dispatch_action(action.boxed_clone());
+                    }
+                }
+            })
             .group("")
             .bg(cx.theme().colors().surface_background)
             // TODO: Add focus state
@@ -401,7 +437,7 @@ impl List {
         v_stack()
             .w_full()
             .py_1()
-            .children(self.header)
+            .children(self.header.map(|header| header))
             .child(list_content)
     }
 }

crates/ui2/src/components/tooltip.rs 🔗

@@ -1,17 +1,53 @@
-use gpui::{Div, Render};
+use gpui::{overlay, Action, AnyView, Overlay, Render, VisualContext};
 use settings2::Settings;
 use theme2::{ActiveTheme, ThemeSettings};
 
 use crate::prelude::*;
 use crate::{h_stack, v_stack, KeyBinding, Label, LabelSize, StyledExt, TextColor};
 
-pub struct TextTooltip {
+pub struct Tooltip {
     title: SharedString,
     meta: Option<SharedString>,
     key_binding: Option<KeyBinding>,
 }
 
-impl TextTooltip {
+impl Tooltip {
+    pub fn text(title: impl Into<SharedString>, cx: &mut WindowContext) -> AnyView {
+        cx.build_view(|cx| Self {
+            title: title.into(),
+            meta: None,
+            key_binding: None,
+        })
+        .into()
+    }
+
+    pub fn for_action(
+        title: impl Into<SharedString>,
+        action: &dyn Action,
+        cx: &mut WindowContext,
+    ) -> AnyView {
+        cx.build_view(|cx| Self {
+            title: title.into(),
+            meta: None,
+            key_binding: KeyBinding::for_action(action, cx),
+        })
+        .into()
+    }
+
+    pub fn with_meta(
+        title: impl Into<SharedString>,
+        action: Option<&dyn Action>,
+        meta: impl Into<SharedString>,
+        cx: &mut WindowContext,
+    ) -> AnyView {
+        cx.build_view(|cx| Self {
+            title: title.into(),
+            meta: Some(meta.into()),
+            key_binding: action.and_then(|action| KeyBinding::for_action(action, cx)),
+        })
+        .into()
+    }
+
     pub fn new(title: impl Into<SharedString>) -> Self {
         Self {
             title: title.into(),
@@ -31,31 +67,36 @@ impl TextTooltip {
     }
 }
 
-impl Render for TextTooltip {
-    type Element = Div<Self>;
+impl Render for Tooltip {
+    type Element = Overlay<Self>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
         let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
-        v_stack()
-            .elevation_2(cx)
-            .font(ui_font)
-            .text_ui_sm()
-            .text_color(cx.theme().colors().text)
-            .py_1()
-            .px_2()
-            .child(
-                h_stack()
-                    .child(self.title.clone())
-                    .when_some(self.key_binding.clone(), |this, key_binding| {
-                        this.justify_between().child(key_binding)
+        overlay().child(
+            // padding to avoid mouse cursor
+            div().pl_2().pt_2p5().child(
+                v_stack()
+                    .elevation_2(cx)
+                    .font(ui_font)
+                    .text_ui_sm()
+                    .text_color(cx.theme().colors().text)
+                    .py_1()
+                    .px_2()
+                    .child(
+                        h_stack()
+                            .child(self.title.clone())
+                            .when_some(self.key_binding.clone(), |this, key_binding| {
+                                this.justify_between().child(key_binding)
+                            }),
+                    )
+                    .when_some(self.meta.clone(), |this, meta| {
+                        this.child(
+                            Label::new(meta)
+                                .size(LabelSize::Small)
+                                .color(TextColor::Muted),
+                        )
                     }),
-            )
-            .when_some(self.meta.clone(), |this, meta| {
-                this.child(
-                    Label::new(meta)
-                        .size(LabelSize::Small)
-                        .color(TextColor::Muted),
-                )
-            })
+            ),
+        )
     }
 }

crates/ui2/src/story.rs 🔗

@@ -12,7 +12,6 @@ impl Story {
             .flex_col()
             .pt_2()
             .px_4()
-            .font("Zed Mono")
             .bg(cx.theme().colors().background)
     }
 

crates/ui2/src/styled_ext.rs 🔗

@@ -5,6 +5,7 @@ use crate::{ElevationIndex, UITextSize};
 
 fn elevated<E: Styled, V: 'static>(this: E, cx: &mut ViewContext<V>, index: ElevationIndex) -> E {
     this.bg(cx.theme().colors().elevated_surface_background)
+        .z_index(index.z_index())
         .rounded_lg()
         .border()
         .border_color(cx.theme().colors().border_variant)

crates/workspace2/src/dock.rs 🔗

@@ -1,12 +1,14 @@
 use crate::{status_bar::StatusItemView, Axis, Workspace};
 use gpui::{
-    div, px, Action, AnyView, AppContext, Component, Div, Entity, EntityId, EventEmitter,
-    FocusHandle, ParentComponent, Render, Styled, Subscription, View, ViewContext, WeakView,
-    WindowContext,
+    div, px, Action, AnchorCorner, AnyView, AppContext, Component, Div, Entity, EntityId,
+    EventEmitter, FocusHandle, FocusableView, ParentComponent, Render, SharedString, Styled,
+    Subscription, View, ViewContext, VisualContext, WeakView, WindowContext,
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use std::sync::Arc;
+use theme2::ActiveTheme;
+use ui::{h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Tooltip};
 
 pub enum PanelEvent {
     ChangePosition,
@@ -17,15 +19,15 @@ pub enum PanelEvent {
     Focus,
 }
 
-pub trait Panel: Render + EventEmitter<PanelEvent> {
-    fn persistent_name(&self) -> &'static str;
+pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
+    fn persistent_name() -> &'static str;
     fn position(&self, cx: &WindowContext) -> DockPosition;
     fn position_is_valid(&self, position: DockPosition) -> bool;
     fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
     fn size(&self, cx: &WindowContext) -> f32;
     fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>);
-    fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>;
-    fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>);
+    fn icon(&self, cx: &WindowContext) -> Option<ui::Icon>;
+    fn toggle_action(&self) -> Box<dyn Action>;
     fn icon_label(&self, _: &WindowContext) -> Option<String> {
         None
     }
@@ -35,12 +37,11 @@ pub trait Panel: Render + EventEmitter<PanelEvent> {
     fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {}
     fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
     fn has_focus(&self, cx: &WindowContext) -> bool;
-    fn focus_handle(&self, cx: &WindowContext) -> FocusHandle;
 }
 
 pub trait PanelHandle: Send + Sync {
     fn id(&self) -> EntityId;
-    fn persistent_name(&self, cx: &WindowContext) -> &'static str;
+    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);
@@ -49,11 +50,11 @@ pub trait PanelHandle: Send + Sync {
     fn set_active(&self, active: bool, cx: &mut WindowContext);
     fn size(&self, cx: &WindowContext) -> f32;
     fn set_size(&self, size: Option<f32>, cx: &mut WindowContext);
-    fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>;
-    fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>);
+    fn icon(&self, cx: &WindowContext) -> Option<ui::Icon>;
+    fn toggle_action(&self, cx: &WindowContext) -> Box<dyn Action>;
     fn icon_label(&self, cx: &WindowContext) -> Option<String>;
     fn has_focus(&self, cx: &WindowContext) -> bool;
-    fn focus_handle(&self, cx: &WindowContext) -> FocusHandle;
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
     fn to_any(&self) -> AnyView;
 }
 
@@ -65,8 +66,8 @@ where
         self.entity_id()
     }
 
-    fn persistent_name(&self, cx: &WindowContext) -> &'static str {
-        self.read(cx).persistent_name()
+    fn persistent_name(&self) -> &'static str {
+        T::persistent_name()
     }
 
     fn position(&self, cx: &WindowContext) -> DockPosition {
@@ -101,12 +102,12 @@ where
         self.update(cx, |this, cx| this.set_size(size, cx))
     }
 
-    fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> {
-        self.read(cx).icon_path(cx)
+    fn icon(&self, cx: &WindowContext) -> Option<ui::Icon> {
+        self.read(cx).icon(cx)
     }
 
-    fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>) {
-        self.read(cx).icon_tooltip()
+    fn toggle_action(&self, cx: &WindowContext) -> Box<dyn Action> {
+        self.read(cx).toggle_action()
     }
 
     fn icon_label(&self, cx: &WindowContext) -> Option<String> {
@@ -121,7 +122,7 @@ where
         self.clone().into()
     }
 
-    fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
         self.read(cx).focus_handle(cx).clone()
     }
 }
@@ -139,6 +140,14 @@ pub struct Dock {
     active_panel_index: usize,
 }
 
+impl FocusableView for Dock {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.panel_entries[self.active_panel_index]
+            .panel
+            .focus_handle(cx)
+    }
+}
+
 #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
 #[serde(rename_all = "lowercase")]
 pub enum DockPosition {
@@ -208,24 +217,26 @@ impl Dock {
     //             .map_or(false, |panel| panel.has_focus(cx))
     //     }
 
-    //     pub fn panel<T: Panel>(&self) -> Option<View<T>> {
-    //         self.panel_entries
-    //             .iter()
-    //             .find_map(|entry| entry.panel.as_any().clone().downcast())
-    //     }
+    pub fn panel<T: Panel>(&self) -> Option<View<T>> {
+        self.panel_entries
+            .iter()
+            .find_map(|entry| entry.panel.to_any().clone().downcast().ok())
+    }
 
-    //     pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
-    //         self.panel_entries
-    //             .iter()
-    //             .position(|entry| entry.panel.as_any().is::<T>())
-    //     }
+    pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
+        self.panel_entries
+            .iter()
+            .position(|entry| entry.panel.to_any().downcast::<T>().is_ok())
+    }
 
-    pub fn panel_index_for_ui_name(&self, _ui_name: &str, _cx: &AppContext) -> Option<usize> {
-        todo!()
-        // self.panel_entries.iter().position(|entry| {
-        //     let panel = entry.panel.as_any();
-        //     cx.view_ui_name(panel.window(), panel.id()) == Some(ui_name)
-        // })
+    pub fn panel_index_for_persistent_name(
+        &self,
+        ui_name: &str,
+        _cx: &AppContext,
+    ) -> Option<usize> {
+        self.panel_entries
+            .iter()
+            .position(|entry| entry.panel.persistent_name() == ui_name)
     }
 
     pub fn active_panel_index(&self) -> usize {
@@ -406,23 +417,13 @@ impl Dock {
         }
     }
 
-    //     pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement<Workspace> {
-    //         todo!()
-    // if let Some(active_entry) = self.visible_entry() {
-    //     Empty::new()
-    //         .into_any()
-    //         .contained()
-    //         .with_style(self.style(cx))
-    //         .resizable::<WorkspaceBounds>(
-    //             self.position.to_resize_handle_side(),
-    //             active_entry.panel.size(cx),
-    //             |_, _, _| {},
-    //         )
-    //         .into_any()
-    // } else {
-    //     Empty::new().into_any()
-    // }
-    //     }
+    pub fn toggle_action(&self) -> Box<dyn Action> {
+        match self.position {
+            DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
+            DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
+            DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
+        }
+    }
 }
 
 impl Render for Dock {
@@ -433,10 +434,16 @@ impl Render for Dock {
             let size = entry.panel.size(cx);
 
             div()
+                .border_color(cx.theme().colors().border)
                 .map(|this| match self.position().axis() {
                     Axis::Horizontal => this.w(px(size)).h_full(),
                     Axis::Vertical => this.h(px(size)).w_full(),
                 })
+                .map(|this| match self.position() {
+                    DockPosition::Left => this.border_r(),
+                    DockPosition::Right => this.border_l(),
+                    DockPosition::Bottom => this.border_t(),
+                })
                 .child(entry.panel.to_any())
         } else {
             div()
@@ -444,40 +451,6 @@ impl Render for Dock {
     }
 }
 
-// todo!()
-// impl View for Dock {
-//     fn ui_name() -> &'static str {
-//         "Dock"
-//     }
-
-//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-//         if let Some(active_entry) = self.visible_entry() {
-//             let style = self.style(cx);
-//             ChildView::new(active_entry.panel.as_any(), cx)
-//                 .contained()
-//                 .with_style(style)
-//                 .resizable::<WorkspaceBounds>(
-//                     self.position.to_resize_handle_side(),
-//                     active_entry.panel.size(cx),
-//                     |dock: &mut Self, size, cx| dock.resize_active_panel(size, cx),
-//                 )
-//                 .into_any()
-//         } else {
-//             Empty::new().into_any()
-//         }
-//     }
-
-//     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
-//         if cx.is_self_focused() {
-//             if let Some(active_entry) = self.visible_entry() {
-//                 cx.focus(active_entry.panel.as_any());
-//             } else {
-//                 cx.focus_parent();
-//             }
-//         }
-//     }
-// }
-
 impl PanelButtons {
     pub fn new(
         dock: View<Dock>,
@@ -638,17 +611,60 @@ impl PanelButtons {
 //     }
 // }
 
+// here be kittens
 impl Render for PanelButtons {
     type Element = Div<Self>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
         // todo!()
         let dock = self.dock.read(cx);
-        div().children(
-            dock.panel_entries
-                .iter()
-                .map(|panel| panel.panel.persistent_name(cx)),
-        )
+        let active_index = dock.active_panel_index;
+        let is_open = dock.is_open;
+
+        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, panel)| {
+                let icon = panel.panel.icon(cx)?;
+                let name = panel.panel.persistent_name();
+
+                let mut button: IconButton<Self> = if i == active_index && is_open {
+                    let action = dock.toggle_action();
+                    let tooltip: SharedString =
+                        format!("Close {} dock", dock.position.to_label()).into();
+                    IconButton::new(name, icon)
+                        .state(InteractionState::Active)
+                        .action(action.boxed_clone())
+                        .tooltip(move |_, cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
+                } else {
+                    let action = panel.panel.toggle_action(cx);
+
+                    IconButton::new(name, icon)
+                        .action(action.boxed_clone())
+                        .tooltip(move |_, cx| Tooltip::for_action(name, &*action, cx))
+                };
+
+                Some(
+                    menu_handle()
+                        .id(name)
+                        .menu(move |_, cx| {
+                            cx.build_view(|cx| ContextMenu::new(cx).header("SECTION"))
+                        })
+                        .anchor(menu_anchor)
+                        .attach(menu_attach)
+                        .child(|is_open| button.selected(is_open)),
+                )
+            });
+
+        h_stack().gap_0p5().children(buttons)
     }
 }
 
@@ -665,7 +681,7 @@ impl StatusItemView for PanelButtons {
 #[cfg(any(test, feature = "test-support"))]
 pub mod test {
     use super::*;
-    use gpui::{div, Div, ViewContext, WindowContext};
+    use gpui::{actions, div, Div, ViewContext, WindowContext};
 
     pub struct TestPanel {
         pub position: DockPosition,
@@ -674,6 +690,7 @@ pub mod test {
         pub has_focus: bool,
         pub size: f32,
     }
+    actions!(ToggleTestPanel);
 
     impl EventEmitter<PanelEvent> for TestPanel {}
 
@@ -698,7 +715,7 @@ pub mod test {
     }
 
     impl Panel for TestPanel {
-        fn persistent_name(&self) -> &'static str {
+        fn persistent_name() -> &'static str {
             "TestPanel"
         }
 
@@ -723,12 +740,12 @@ pub mod test {
             self.size = size.unwrap_or(300.);
         }
 
-        fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
-            Some("icons/test_panel.svg")
+        fn icon(&self, _: &WindowContext) -> Option<ui::Icon> {
+            None
         }
 
-        fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
-            ("Test Panel".into(), None)
+        fn toggle_action(&self) -> Box<dyn Action> {
+            ToggleTestPanel.boxed_clone()
         }
 
         fn is_zoomed(&self, _: &WindowContext) -> bool {
@@ -746,8 +763,10 @@ pub mod test {
         fn has_focus(&self, _cx: &WindowContext) -> bool {
             self.has_focus
         }
+    }
 
-        fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
+    impl FocusableView for TestPanel {
+        fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
             unimplemented!()
         }
     }

crates/workspace2/src/item.rs 🔗

@@ -12,8 +12,9 @@ use client2::{
     Client,
 };
 use gpui::{
-    AnyElement, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, HighlightStyle,
-    Model, Pixels, Point, Render, SharedString, Task, View, ViewContext, WeakView, WindowContext,
+    AnyElement, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, FocusableView,
+    HighlightStyle, Model, Pixels, Point, SharedString, Task, View, ViewContext, WeakView,
+    WindowContext,
 };
 use project2::{Project, ProjectEntryId, ProjectPath};
 use schemars::JsonSchema;
@@ -91,8 +92,7 @@ pub struct BreadcrumbText {
     pub highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
 }
 
-pub trait Item: Render + EventEmitter<ItemEvent> {
-    fn focus_handle(&self) -> FocusHandle;
+pub trait Item: FocusableView + EventEmitter<ItemEvent> {
     fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
     fn workspace_deactivated(&mut self, _: &mut ViewContext<Self>) {}
     fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
@@ -240,7 +240,7 @@ pub trait ItemHandle: 'static + Send {
     fn deactivated(&self, cx: &mut WindowContext);
     fn workspace_deactivated(&self, cx: &mut WindowContext);
     fn navigate(&self, data: Box<dyn Any>, cx: &mut WindowContext) -> bool;
-    fn id(&self) -> EntityId;
+    fn item_id(&self) -> EntityId;
     fn to_any(&self) -> AnyView;
     fn is_dirty(&self, cx: &AppContext) -> bool;
     fn has_conflict(&self, cx: &AppContext) -> bool;
@@ -286,7 +286,7 @@ impl dyn ItemHandle {
 
 impl<T: Item> ItemHandle for View<T> {
     fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
-        self.read(cx).focus_handle()
+        self.focus_handle(cx)
     }
 
     fn subscribe_to_item_events(
@@ -399,7 +399,7 @@ impl<T: Item> ItemHandle for View<T> {
 
         if workspace
             .panes_by_item
-            .insert(self.id(), pane.downgrade())
+            .insert(self.item_id(), pane.downgrade())
             .is_none()
         {
             let mut pending_autosave = DelayedDebouncedEditAction::new();
@@ -410,7 +410,7 @@ impl<T: Item> ItemHandle for View<T> {
                 Some(cx.subscribe(self, move |workspace, item, event, cx| {
                     let pane = if let Some(pane) = workspace
                         .panes_by_item
-                        .get(&item.id())
+                        .get(&item.item_id())
                         .and_then(|pane| pane.upgrade())
                     {
                         pane
@@ -463,7 +463,7 @@ impl<T: Item> ItemHandle for View<T> {
                     match event {
                         ItemEvent::CloseItem => {
                             pane.update(cx, |pane, cx| {
-                                pane.close_item_by_id(item.id(), crate::SaveIntent::Close, cx)
+                                pane.close_item_by_id(item.item_id(), crate::SaveIntent::Close, cx)
                             })
                             .detach_and_log_err(cx);
                             return;
@@ -502,7 +502,7 @@ impl<T: Item> ItemHandle for View<T> {
             // })
             // .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();
@@ -527,7 +527,7 @@ impl<T: Item> ItemHandle for View<T> {
         self.update(cx, |this, cx| this.navigate(data, cx))
     }
 
-    fn id(&self) -> EntityId {
+    fn item_id(&self) -> EntityId {
         self.entity_id()
     }
 
@@ -712,7 +712,7 @@ impl<T: FollowableItem> FollowableItemHandle for View<T> {
         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(),
             })
         })
     }

crates/workspace2/src/modal_layer.rs 🔗

@@ -1,6 +1,6 @@
 use gpui::{
-    div, prelude::*, px, AnyView, Div, EventEmitter, FocusHandle, Render, Subscription, View,
-    ViewContext, WindowContext,
+    div, prelude::*, px, AnyView, Div, FocusHandle, ManagedView, Render, Subscription, View,
+    ViewContext,
 };
 use ui::{h_stack, v_stack};
 
@@ -15,14 +15,6 @@ pub struct ModalLayer {
     active_modal: Option<ActiveModal>,
 }
 
-pub trait Modal: Render + EventEmitter<ModalEvent> {
-    fn focus(&self, cx: &mut WindowContext);
-}
-
-pub enum ModalEvent {
-    Dismissed,
-}
-
 impl ModalLayer {
     pub fn new() -> Self {
         Self { active_modal: None }
@@ -30,7 +22,7 @@ impl ModalLayer {
 
     pub fn toggle_modal<V, B>(&mut self, cx: &mut ViewContext<Self>, build_view: B)
     where
-        V: Modal,
+        V: ManagedView,
         B: FnOnce(&mut ViewContext<V>) -> V,
     {
         if let Some(active_modal) = &self.active_modal {
@@ -46,17 +38,15 @@ impl ModalLayer {
 
     pub fn show_modal<V>(&mut self, new_modal: View<V>, cx: &mut ViewContext<Self>)
     where
-        V: Modal,
+        V: ManagedView,
     {
         self.active_modal = Some(ActiveModal {
             modal: new_modal.clone().into(),
-            subscription: cx.subscribe(&new_modal, |this, modal, e, cx| match e {
-                ModalEvent::Dismissed => this.hide_modal(cx),
-            }),
+            subscription: cx.subscribe(&new_modal, |this, modal, e, cx| this.hide_modal(cx)),
             previous_focus_handle: cx.focused(),
             focus_handle: cx.focus_handle(),
         });
-        new_modal.update(cx, |modal, cx| modal.focus(cx));
+        cx.focus_view(&new_modal);
         cx.notify();
     }
 

crates/workspace2/src/pane.rs 🔗

@@ -7,9 +7,9 @@ use crate::{
 use anyhow::Result;
 use collections::{HashMap, HashSet, VecDeque};
 use gpui::{
-    actions, prelude::*, register_action, AppContext, AsyncWindowContext, Component, Div, EntityId,
-    EventEmitter, FocusHandle, Focusable, Model, PromptLevel, Render, Task, View, ViewContext,
-    VisualContext, WeakView, WindowContext,
+    actions, prelude::*, Action, AppContext, AsyncWindowContext, Component, Div, EntityId,
+    EventEmitter, FocusHandle, Focusable, FocusableView, Model, Pixels, Point, PromptLevel, Render,
+    Task, View, ViewContext, VisualContext, WeakView, WindowContext,
 };
 use parking_lot::Mutex;
 use project2::{Project, ProjectEntryId, ProjectPath};
@@ -25,7 +25,7 @@ use std::{
     },
 };
 use ui::v_stack;
-use ui::{prelude::*, Icon, IconButton, IconElement, TextColor, TextTooltip};
+use ui::{prelude::*, Icon, IconButton, IconElement, TextColor, Tooltip};
 use util::truncate_and_remove_front;
 
 #[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
@@ -70,15 +70,13 @@ pub struct ActivateItem(pub usize);
 //     pub pane: WeakView<Pane>,
 // }
 
-#[register_action]
-#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[derive(Clone, PartialEq, Debug, Deserialize, Default, Action)]
 #[serde(rename_all = "camelCase")]
 pub struct CloseActiveItem {
     pub save_intent: Option<SaveIntent>,
 }
 
-#[register_action]
-#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[derive(Clone, PartialEq, Debug, Deserialize, Default, Action)]
 #[serde(rename_all = "camelCase")]
 pub struct CloseAllItems {
     pub save_intent: Option<SaveIntent>,
@@ -104,33 +102,6 @@ actions!(
 
 const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 
-pub fn init(cx: &mut AppContext) {
-    // todo!()
-    //     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));
-}
-
 pub enum Event {
     AddItem { item: Box<dyn ItemHandle> },
     ActivateItem { local: bool },
@@ -146,7 +117,10 @@ pub enum Event {
 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.id()).finish(),
+            Event::AddItem { item } => f
+                .debug_struct("AddItem")
+                .field("item", &item.item_id())
+                .finish(),
             Event::ActivateItem { local } => f
                 .debug_struct("ActivateItem")
                 .field("local", local)
@@ -183,7 +157,7 @@ pub struct Pane {
     workspace: WeakView<Workspace>,
     project: Model<Project>,
     //     can_drop: Rc<dyn Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool>,
-    //     can_split: bool,
+    can_split: bool,
     //     render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>>,
 }
 
@@ -351,7 +325,7 @@ impl Pane {
             workspace,
             project,
             // can_drop: Rc::new(|_, _| true),
-            // can_split: true,
+            can_split: true,
             // render_tab_bar_buttons: Rc::new(move |pane, cx| {
             //     Flex::row()
             //         // New menu
@@ -431,17 +405,17 @@ impl Pane {
     //         self.can_drop = Rc::new(can_drop);
     //     }
 
-    //     pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
-    //         self.can_split = can_split;
-    //         cx.notify();
-    //     }
+    pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
+        self.can_split = can_split;
+        cx.notify();
+    }
 
-    //     pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
-    //         self.toolbar.update(cx, |toolbar, cx| {
-    //             toolbar.set_can_navigate(can_navigate, cx);
-    //         });
-    //         cx.notify();
-    //     }
+    pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
+        self.toolbar.update(cx, |toolbar, cx| {
+            toolbar.set_can_navigate(can_navigate, cx);
+        });
+        cx.notify();
+    }
 
     //     pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
     //     where
@@ -530,7 +504,7 @@ impl Pane {
                         .0
                         .lock()
                         .paths_by_item
-                        .insert(item.id(), (project_path, abs_path));
+                        .insert(item.item_id(), (project_path, abs_path));
                 }
             }
         }
@@ -554,7 +528,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
@@ -619,21 +593,21 @@ impl Pane {
         self.items.iter()
     }
 
-    pub fn items_of_type<T: 'static>(&self) -> impl '_ + Iterator<Item = View<T>> {
+    pub fn items_of_type<T: Render>(&self) -> impl '_ + Iterator<Item = View<T>> {
         self.items
             .iter()
-            .filter_map(|item| item.to_any().clone().downcast().ok())
+            .filter_map(|item| item.to_any().downcast().ok())
     }
 
     pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
         self.items.get(self.active_item_index).cloned()
     }
 
-    //     pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
-    //         self.items
-    //             .get(self.active_item_index)?
-    //             .pixel_position_of_cursor(cx)
-    //     }
+    pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
+        self.items
+            .get(self.active_item_index)?
+            .pixel_position_of_cursor(cx)
+    }
 
     pub fn item_for_entry(
         &self,
@@ -650,24 +624,26 @@ impl Pane {
     }
 
     pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
-        self.items.iter().position(|i| i.id() == item.id())
+        self.items
+            .iter()
+            .position(|i| i.item_id() == item.item_id())
     }
 
-    //     pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
-    //         // Potentially warn the user of the new keybinding
-    //         let workspace_handle = self.workspace().clone();
-    //         cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) })
-    //             .detach();
-
-    //         if self.zoomed {
-    //             cx.emit(Event::ZoomOut);
-    //         } else if !self.items.is_empty() {
-    //             if !self.has_focus {
-    //                 cx.focus_self();
-    //             }
-    //             cx.emit(Event::ZoomIn);
+    // pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
+    //     // Potentially warn the user of the new keybinding
+    //     let workspace_handle = self.workspace().clone();
+    //     cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) })
+    //         .detach();
+
+    //     if self.zoomed {
+    //         cx.emit(Event::ZoomOut);
+    //     } else if !self.items.is_empty() {
+    //         if !self.has_focus {
+    //             cx.focus_self();
     //         }
+    //         cx.emit(Event::ZoomIn);
     //     }
+    // }
 
     pub fn activate_item(
         &mut self,
@@ -695,9 +671,9 @@ 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);
@@ -711,25 +687,25 @@ impl Pane {
         }
     }
 
-    //     pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
-    //         let mut index = self.active_item_index;
-    //         if index > 0 {
-    //             index -= 1;
-    //         } else if !self.items.is_empty() {
-    //             index = self.items.len() - 1;
-    //         }
-    //         self.activate_item(index, activate_pane, activate_pane, cx);
-    //     }
+    pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
+        let mut index = self.active_item_index;
+        if index > 0 {
+            index -= 1;
+        } else if !self.items.is_empty() {
+            index = self.items.len() - 1;
+        }
+        self.activate_item(index, activate_pane, activate_pane, cx);
+    }
 
-    //     pub fn activate_next_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
-    //         let mut index = self.active_item_index;
-    //         if index + 1 < self.items.len() {
-    //             index += 1;
-    //         } else {
-    //             index = 0;
-    //         }
-    //         self.activate_item(index, activate_pane, activate_pane, cx);
-    //     }
+    pub fn activate_next_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
+        let mut index = self.active_item_index;
+        if index + 1 < self.items.len() {
+            index += 1;
+        } else {
+            index = 0;
+        }
+        self.activate_item(index, activate_pane, activate_pane, cx);
+    }
 
     pub fn close_active_item(
         &mut self,
@@ -739,7 +715,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),
@@ -756,106 +732,106 @@ impl Pane {
         self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close)
     }
 
-    // pub fn close_inactive_items(
-    //     &mut self,
-    //     _: &CloseInactiveItems,
-    //     cx: &mut ViewContext<Self>,
-    // ) -> Option<Task<Result<()>>> {
-    //     if self.items.is_empty() {
-    //         return None;
-    //     }
+    pub fn close_inactive_items(
+        &mut self,
+        _: &CloseInactiveItems,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        if self.items.is_empty() {
+            return None;
+        }
 
-    //     let active_item_id = self.items[self.active_item_index].id();
-    //     Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
-    //         item_id != active_item_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
+        }))
+    }
 
-    // pub fn close_clean_items(
-    //     &mut self,
-    //     _: &CloseCleanItems,
-    //     cx: &mut ViewContext<Self>,
-    // ) -> Option<Task<Result<()>>> {
-    //     let item_ids: Vec<_> = self
-    //         .items()
-    //         .filter(|item| !item.is_dirty(cx))
-    //         .map(|item| item.id())
-    //         .collect();
-    //     Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
-    //         item_ids.contains(&item_id)
-    //     }))
-    // }
+    pub fn close_clean_items(
+        &mut self,
+        _: &CloseCleanItems,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let item_ids: Vec<_> = self
+            .items()
+            .filter(|item| !item.is_dirty(cx))
+            .map(|item| item.item_id())
+            .collect();
+        Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
+            item_ids.contains(&item_id)
+        }))
+    }
 
-    // pub fn close_items_to_the_left(
-    //     &mut self,
-    //     _: &CloseItemsToTheLeft,
-    //     cx: &mut ViewContext<Self>,
-    // ) -> Option<Task<Result<()>>> {
-    //     if self.items.is_empty() {
-    //         return None;
-    //     }
-    //     let active_item_id = self.items[self.active_item_index].id();
-    //     Some(self.close_items_to_the_left_by_id(active_item_id, cx))
-    // }
+    pub fn close_items_to_the_left(
+        &mut self,
+        _: &CloseItemsToTheLeft,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        if self.items.is_empty() {
+            return None;
+        }
+        let active_item_id = self.items[self.active_item_index].item_id();
+        Some(self.close_items_to_the_left_by_id(active_item_id, cx))
+    }
 
-    // pub fn close_items_to_the_left_by_id(
-    //     &mut self,
-    //     item_id: usize,
-    //     cx: &mut ViewContext<Self>,
-    // ) -> Task<Result<()>> {
-    //     let item_ids: Vec<_> = self
-    //         .items()
-    //         .take_while(|item| item.id() != item_id)
-    //         .map(|item| item.id())
-    //         .collect();
-    //     self.close_items(cx, SaveIntent::Close, move |item_id| {
-    //         item_ids.contains(&item_id)
-    //     })
-    // }
+    pub fn close_items_to_the_left_by_id(
+        &mut self,
+        item_id: EntityId,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
+        let item_ids: Vec<_> = self
+            .items()
+            .take_while(|item| item.item_id() != item_id)
+            .map(|item| item.item_id())
+            .collect();
+        self.close_items(cx, SaveIntent::Close, move |item_id| {
+            item_ids.contains(&item_id)
+        })
+    }
 
-    // pub fn close_items_to_the_right(
-    //     &mut self,
-    //     _: &CloseItemsToTheRight,
-    //     cx: &mut ViewContext<Self>,
-    // ) -> Option<Task<Result<()>>> {
-    //     if self.items.is_empty() {
-    //         return None;
-    //     }
-    //     let active_item_id = self.items[self.active_item_index].id();
-    //     Some(self.close_items_to_the_right_by_id(active_item_id, cx))
-    // }
+    pub fn close_items_to_the_right(
+        &mut self,
+        _: &CloseItemsToTheRight,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        if self.items.is_empty() {
+            return None;
+        }
+        let active_item_id = self.items[self.active_item_index].item_id();
+        Some(self.close_items_to_the_right_by_id(active_item_id, cx))
+    }
 
-    // pub fn close_items_to_the_right_by_id(
-    //     &mut self,
-    //     item_id: usize,
-    //     cx: &mut ViewContext<Self>,
-    // ) -> Task<Result<()>> {
-    //     let item_ids: Vec<_> = self
-    //         .items()
-    //         .rev()
-    //         .take_while(|item| item.id() != item_id)
-    //         .map(|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_by_id(
+        &mut self,
+        item_id: EntityId,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
+        let item_ids: Vec<_> = self
+            .items()
+            .rev()
+            .take_while(|item| item.item_id() != item_id)
+            .map(|item| item.item_id())
+            .collect();
+        self.close_items(cx, SaveIntent::Close, move |item_id| {
+            item_ids.contains(&item_id)
+        })
+    }
 
-    // pub fn close_all_items(
-    //     &mut self,
-    //     action: &CloseAllItems,
-    //     cx: &mut ViewContext<Self>,
-    // ) -> Option<Task<Result<()>>> {
-    //     if self.items.is_empty() {
-    //         return None;
-    //     }
+    pub fn close_all_items(
+        &mut self,
+        action: &CloseAllItems,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        if self.items.is_empty() {
+            return None;
+        }
 
-    //     Some(
-    //         self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| {
-    //             true
-    //         }),
-    //     )
-    // }
+        Some(
+            self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| {
+                true
+            }),
+        )
+    }
 
     pub(super) fn file_names_for_prompt(
         items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
@@ -902,7 +878,7 @@ impl Pane {
         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());
@@ -955,7 +931,7 @@ impl Pane {
                     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));
@@ -983,7 +959,11 @@ 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);
                     }
                 })?;
@@ -1001,7 +981,7 @@ impl Pane {
         cx: &mut ViewContext<Self>,
     ) {
         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
@@ -1009,7 +989,7 @@ 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
@@ -1026,7 +1006,9 @@ impl Pane {
 
         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);
@@ -1047,16 +1029,20 @@ impl Pane {
                 .0
                 .lock()
                 .paths_by_item
-                .get(&item.id())
+                .get(&item.item_id())
                 .and_then(|(_, abs_path)| abs_path.clone());
 
             self.nav_history
                 .0
                 .lock()
                 .paths_by_item
-                .insert(item.id(), (path, abs_path));
+                .insert(item.item_id(), (path, abs_path));
         } else {
-            self.nav_history.0.lock().paths_by_item.remove(&item.id());
+            self.nav_history
+                .0
+                .lock()
+                .paths_by_item
+                .remove(&item.item_id());
         }
 
         if self.items.is_empty() && self.zoomed {
@@ -1195,9 +1181,9 @@ impl Pane {
         }
     }
 
-    //     pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
-    //         cx.emit(Event::Split(direction));
-    //     }
+    pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
+        cx.emit(Event::Split(direction));
+    }
 
     //     fn deploy_split_menu(&mut self, cx: &mut ViewContext<Self>) {
     //         self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
@@ -1329,7 +1315,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
             }
@@ -1360,10 +1346,10 @@ impl Pane {
     ) -> impl Component<Self> {
         let label = item.tab_content(Some(detail), cx);
         let close_icon = || {
-            let id = item.id();
+            let id = item.item_id();
 
             div()
-                .id(item.id())
+                .id(item.item_id())
                 .invisible()
                 .group_hover("", |style| style.visible())
                 .child(IconButton::new("close_tab", Icon::Close).on_click(
@@ -1393,11 +1379,12 @@ impl Pane {
 
         div()
             .group("")
-            .id(item.id())
+            .id(item.item_id())
             .cursor_pointer()
             .when_some(item.tab_tooltip_text(cx), |div, text| {
-                div.tooltip(move |_, cx| cx.build_view(|cx| TextTooltip::new(text.clone())))
+                div.tooltip(move |_, cx| cx.build_view(|cx| Tooltip::new(text.clone())).into())
             })
+            .on_click(move |v: &mut Self, e, cx| v.activate_item(ix, true, true, cx))
             // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx))
             // .drag_over::<DraggedTab>(|d| d.bg(cx.theme().colors().element_drop_target))
             // .on_drop(|_view, state: View<DraggedTab>, cx| {
@@ -1430,32 +1417,22 @@ impl Pane {
                     .items_center()
                     .gap_1()
                     .text_color(text_color)
-                    .children(if item.has_conflict(cx) {
-                        Some(
-                            IconElement::new(Icon::ExclamationTriangle)
-                                .size(ui::IconSize::Small)
-                                .color(TextColor::Warning),
-                        )
-                    } else if item.is_dirty(cx) {
-                        Some(
-                            IconElement::new(Icon::ExclamationTriangle)
-                                .size(ui::IconSize::Small)
-                                .color(TextColor::Info),
-                        )
-                    } else {
-                        None
-                    })
-                    .children(if !close_right {
-                        Some(close_icon())
-                    } else {
-                        None
-                    })
+                    .children(
+                        item.has_conflict(cx)
+                            .then(|| {
+                                IconElement::new(Icon::ExclamationTriangle)
+                                    .size(ui::IconSize::Small)
+                                    .color(TextColor::Warning)
+                            })
+                            .or(item.is_dirty(cx).then(|| {
+                                IconElement::new(Icon::ExclamationTriangle)
+                                    .size(ui::IconSize::Small)
+                                    .color(TextColor::Info)
+                            })),
+                    )
+                    .children((!close_right).then(|| close_icon()))
                     .child(label)
-                    .children(if close_right {
-                        Some(close_icon())
-                    } else {
-                        None
-                    }),
+                    .children(close_right.then(|| close_icon())),
             )
     }
 
@@ -1912,9 +1889,11 @@ impl Pane {
     }
 }
 
-// impl Entity for Pane {
-//     type Event = Event;
-// }
+impl FocusableView for Pane {
+    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
+        self.focus_handle.clone()
+    }
+}
 
 impl Render for Pane {
     type Element = Focusable<Self, Div<Self>>;
@@ -1923,8 +1902,31 @@ impl Render for Pane {
         v_stack()
             .key_context("Pane")
             .track_focus(&self.focus_handle)
+            .on_action(|pane: &mut Pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx))
+            .on_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx))
+            .on_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx))
+            .on_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx))
+            //     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);
             .size_full()
-            .on_action(|pane: &mut Self, action, cx| {
+            .on_action(|pane: &mut Self, action: &CloseActiveItem, cx| {
                 pane.close_active_item(action, cx)
                     .map(|task| task.detach_and_log_err(cx));
             })

crates/workspace2/src/pane_group.rs 🔗

@@ -148,6 +148,10 @@ impl PaneGroup {
         self.root.collect_panes(&mut panes);
         panes
     }
+
+    pub(crate) fn first_pane(&self) -> View<Pane> {
+        self.root.first_pane()
+    }
 }
 
 #[derive(Clone, PartialEq)]
@@ -181,6 +185,13 @@ impl Member {
         }
     }
 
+    fn first_pane(&self) -> View<Pane> {
+        match self {
+            Member::Axis(axis) => axis.members[0].first_pane(),
+            Member::Pane(pane) => pane.clone(),
+        }
+    }
+
     pub fn render(
         &self,
         project: &Model<Project>,
@@ -551,7 +562,32 @@ impl PaneAxis {
     ) -> AnyElement<Workspace> {
         debug_assert!(self.members.len() == self.flexes.lock().len());
 
-        todo!()
+        div()
+            .flex()
+            .flex_auto()
+            .map(|s| match self.axis {
+                Axis::Vertical => s.flex_col(),
+                Axis::Horizontal => s.flex_row(),
+            })
+            .children(self.members.iter().enumerate().map(|(ix, member)| {
+                match member {
+                    Member::Axis(axis) => axis
+                        .render(
+                            project,
+                            basis,
+                            follower_states,
+                            active_call,
+                            active_pane,
+                            zoomed,
+                            app_state,
+                            cx,
+                        )
+                        .render(),
+                    Member::Pane(pane) => pane.clone().render(),
+                }
+            }))
+            .render()
+
         // let mut pane_axis = PaneAxisElement::new(
         //     self.axis,
         //     basis,

crates/workspace2/src/persistence/model.rs 🔗

@@ -277,7 +277,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 {

crates/workspace2/src/searchable.rs 🔗

@@ -240,7 +240,7 @@ impl From<&Box<dyn SearchableItemHandle>> for AnyView {
 
 impl PartialEq for Box<dyn SearchableItemHandle> {
     fn eq(&self, other: &Self) -> bool {
-        self.id() == other.id()
+        self.item_id() == other.item_id()
     }
 }
 

crates/workspace2/src/status_bar.rs 🔗

@@ -6,6 +6,7 @@ use gpui::{
     WindowContext,
 };
 use theme2::ActiveTheme;
+use ui::h_stack;
 use util::ResultExt;
 
 pub trait StatusItemView: Render {
@@ -53,57 +54,20 @@ impl Render for StatusBar {
 
 impl StatusBar {
     fn render_left_tools(&self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
-        div()
-            .flex()
+        h_stack()
             .items_center()
             .gap_1()
             .children(self.left_items.iter().map(|item| item.to_any()))
     }
 
     fn render_right_tools(&self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
-        div()
-            .flex()
+        h_stack()
             .items_center()
             .gap_2()
             .children(self.right_items.iter().map(|item| item.to_any()))
     }
 }
 
-// todo!()
-// impl View for StatusBar {
-//     fn ui_name() -> &'static str {
-//         "StatusBar"
-//     }
-
-//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-//         let theme = &theme::current(cx).workspace.status_bar;
-
-//         StatusBarElement {
-//             left: Flex::row()
-//                 .with_children(self.left_items.iter().map(|i| {
-//                     ChildView::new(i.as_any(), cx)
-//                         .aligned()
-//                         .contained()
-//                         .with_margin_right(theme.item_spacing)
-//                 }))
-//                 .into_any(),
-//             right: Flex::row()
-//                 .with_children(self.right_items.iter().rev().map(|i| {
-//                     ChildView::new(i.as_any(), cx)
-//                         .aligned()
-//                         .contained()
-//                         .with_margin_left(theme.item_spacing)
-//                 }))
-//                 .into_any(),
-//         }
-//         .contained()
-//         .with_style(theme.container)
-//         .constrained()
-//         .with_height(theme.height)
-//         .into_any()
-//     }
-// }
-
 impl StatusBar {
     pub fn new(active_pane: &View<Pane>, cx: &mut ViewContext<Self>) -> Self {
         let mut this = Self {
@@ -223,80 +187,3 @@ impl From<&dyn StatusItemViewHandle> for AnyView {
         val.to_any().clone()
     }
 }
-
-// todo!()
-// struct StatusBarElement {
-//     left: AnyElement<StatusBar>,
-//     right: AnyElement<StatusBar>,
-// }
-
-// todo!()
-// impl Element<StatusBar> for StatusBarElement {
-//     type LayoutState = ();
-//     type PaintState = ();
-
-//     fn layout(
-//         &mut self,
-//         mut constraint: SizeConstraint,
-//         view: &mut StatusBar,
-//         cx: &mut ViewContext<StatusBar>,
-//     ) -> (Vector2F, Self::LayoutState) {
-//         let max_width = constraint.max.x();
-//         constraint.min = vec2f(0., constraint.min.y());
-
-//         let right_size = self.right.layout(constraint, view, cx);
-//         let constraint = SizeConstraint::new(
-//             vec2f(0., constraint.min.y()),
-//             vec2f(max_width - right_size.x(), constraint.max.y()),
-//         );
-
-//         self.left.layout(constraint, view, cx);
-
-//         (vec2f(max_width, right_size.y()), ())
-//     }
-
-//     fn paint(
-//         &mut self,
-//         bounds: RectF,
-//         visible_bounds: RectF,
-//         _: &mut Self::LayoutState,
-//         view: &mut StatusBar,
-//         cx: &mut ViewContext<StatusBar>,
-//     ) -> Self::PaintState {
-//         let origin_y = bounds.upper_right().y();
-//         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
-
-//         let left_origin = vec2f(bounds.lower_left().x(), origin_y);
-//         self.left.paint(left_origin, visible_bounds, view, cx);
-
-//         let right_origin = vec2f(bounds.upper_right().x() - self.right.size().x(), origin_y);
-//         self.right.paint(right_origin, visible_bounds, view, cx);
-//     }
-
-//     fn rect_for_text_range(
-//         &self,
-//         _: Range<usize>,
-//         _: RectF,
-//         _: RectF,
-//         _: &Self::LayoutState,
-//         _: &Self::PaintState,
-//         _: &StatusBar,
-//         _: &ViewContext<StatusBar>,
-//     ) -> Option<RectF> {
-//         None
-//     }
-
-//     fn debug(
-//         &self,
-//         bounds: RectF,
-//         _: &Self::LayoutState,
-//         _: &Self::PaintState,
-//         _: &StatusBar,
-//         _: &ViewContext<StatusBar>,
-//     ) -> serde_json::Value {
-//         json!({
-//             "type": "StatusBarElement",
-//             "bounds": bounds.to_json()
-//         })
-//     }
-// }

crates/workspace2/src/workspace2.rs 🔗

@@ -15,13 +15,6 @@ mod status_bar;
 mod toolbar;
 mod workspace_settings;
 
-pub use crate::persistence::{
-    model::{
-        DockData, DockStructure, ItemId, SerializedItem, SerializedPane, SerializedPaneGroup,
-        SerializedWorkspace,
-    },
-    WorkspaceDb,
-};
 use anyhow::{anyhow, Context as _, Result};
 use call2::ActiveCall;
 use client2::{
@@ -29,17 +22,18 @@ use client2::{
     Client, TypedEnvelope, UserStore,
 };
 use collections::{hash_map, HashMap, HashSet};
-use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle as _};
+use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
 use futures::{
     channel::{mpsc, oneshot},
     future::try_join_all,
     Future, FutureExt, StreamExt,
 };
 use gpui::{
-    actions, div, point, prelude::*, rems, size, Action, AnyModel, AnyView, AnyWeakView,
-    AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId,
-    EventEmitter, GlobalPixels, KeyContext, Model, ModelContext, ParentComponent, Point, Render,
-    Size, Styled, Subscription, Task, View, ViewContext, WeakView, WindowBounds, WindowContext,
+    actions, div, point, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext,
+    AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter, FocusHandle,
+    FocusableView, GlobalPixels, InteractiveComponent, KeyContext, ManagedView, Model,
+    ModelContext, ParentComponent, PathPromptOptions, Point, PromptLevel, Render, Size, Styled,
+    Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext,
     WindowHandle, WindowOptions,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
@@ -51,9 +45,12 @@ use node_runtime::NodeRuntime;
 use notifications::{simple_message_notification::MessageNotification, NotificationHandle};
 pub use pane::*;
 pub use pane_group::*;
-use persistence::{model::WorkspaceLocation, DB};
+pub use persistence::{
+    model::{ItemId, SerializedWorkspace, WorkspaceLocation},
+    WorkspaceDb, DB,
+};
 use postage::stream::Stream;
-use project2::{Project, ProjectEntryId, ProjectPath, Worktree};
+use project2::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
 use serde::Deserialize;
 use settings2::Settings;
 use status_bar::StatusBar;
@@ -61,19 +58,22 @@ pub use status_bar::StatusItemView;
 use std::{
     any::TypeId,
     borrow::Cow,
-    env,
+    cmp, env,
     path::{Path, PathBuf},
     sync::{atomic::AtomicUsize, Arc},
     time::Duration,
 };
 use theme2::{ActiveTheme, ThemeSettings};
 pub use toolbar::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
-use ui::TextColor;
-use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextTooltip};
+pub use ui;
 use util::ResultExt;
 use uuid::Uuid;
 pub use workspace_settings::{AutosaveSetting, WorkspaceSettings};
 
+use crate::persistence::model::{
+    DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup,
+};
+
 lazy_static! {
     static ref ZED_WINDOW_SIZE: Option<Size<GlobalPixels>> = env::var("ZED_WINDOW_SIZE")
         .ok()
@@ -85,8 +85,8 @@ lazy_static! {
         .and_then(parse_pixel_position_env_var);
 }
 
-// #[derive(Clone, PartialEq)]
-// pub struct RemoveWorktreeFromProject(pub WorktreeId);
+#[derive(Clone, PartialEq)]
+pub struct RemoveWorktreeFromProject(pub WorktreeId);
 
 actions!(
     Open,
@@ -115,40 +115,40 @@ actions!(
     CloseAllDocks,
 );
 
-// #[derive(Clone, PartialEq)]
-// pub struct OpenPaths {
-//     pub paths: Vec<PathBuf>,
-// }
+#[derive(Clone, PartialEq)]
+pub struct OpenPaths {
+    pub paths: Vec<PathBuf>,
+}
 
-// #[derive(Clone, Deserialize, PartialEq)]
-// pub struct ActivatePane(pub usize);
+#[derive(Clone, Deserialize, PartialEq, Action)]
+pub struct ActivatePane(pub usize);
 
-// #[derive(Clone, Deserialize, PartialEq)]
-// pub struct ActivatePaneInDirection(pub SplitDirection);
+#[derive(Clone, Deserialize, PartialEq, Action)]
+pub struct ActivatePaneInDirection(pub SplitDirection);
 
-// #[derive(Clone, Deserialize, PartialEq)]
-// pub struct SwapPaneInDirection(pub SplitDirection);
+#[derive(Clone, Deserialize, PartialEq, Action)]
+pub struct SwapPaneInDirection(pub SplitDirection);
 
-// #[derive(Clone, Deserialize, PartialEq)]
-// pub struct NewFileInDirection(pub SplitDirection);
+#[derive(Clone, Deserialize, PartialEq, Action)]
+pub struct NewFileInDirection(pub SplitDirection);
 
-// #[derive(Clone, PartialEq, Debug, Deserialize)]
-// #[serde(rename_all = "camelCase")]
-// pub struct SaveAll {
-//     pub save_intent: Option<SaveIntent>,
-// }
+#[derive(Clone, PartialEq, Debug, Deserialize, Action)]
+#[serde(rename_all = "camelCase")]
+pub struct SaveAll {
+    pub save_intent: Option<SaveIntent>,
+}
 
-// #[derive(Clone, PartialEq, Debug, Deserialize)]
-// #[serde(rename_all = "camelCase")]
-// pub struct Save {
-//     pub save_intent: Option<SaveIntent>,
-// }
+#[derive(Clone, PartialEq, Debug, Deserialize, Action)]
+#[serde(rename_all = "camelCase")]
+pub struct Save {
+    pub save_intent: Option<SaveIntent>,
+}
 
-// #[derive(Clone, PartialEq, Debug, Deserialize, Default)]
-// #[serde(rename_all = "camelCase")]
-// pub struct CloseAllItemsAndPanes {
-//     pub save_intent: Option<SaveIntent>,
-// }
+#[derive(Clone, PartialEq, Debug, Deserialize, Default, Action)]
+#[serde(rename_all = "camelCase")]
+pub struct CloseAllItemsAndPanes {
+    pub save_intent: Option<SaveIntent>,
+}
 
 #[derive(Deserialize)]
 pub struct Toast {
@@ -195,25 +195,10 @@ impl Clone for Toast {
     }
 }
 
-// #[derive(Clone, Deserialize, PartialEq)]
-// pub struct OpenTerminal {
-//     pub working_directory: PathBuf,
-// }
-
-// impl_actions!(
-//     workspace,
-//     [
-//         ActivatePane,
-//         ActivatePaneInDirection,
-//         SwapPaneInDirection,
-//         NewFileInDirection,
-//         Toast,
-//         OpenTerminal,
-//         SaveAll,
-//         Save,
-//         CloseAllItemsAndPanes,
-//     ]
-// );
+#[derive(Debug, Default, Clone, Deserialize, PartialEq, Action)]
+pub struct OpenTerminal {
+    pub working_directory: PathBuf,
+}
 
 pub type WorkspaceId = i64;
 
@@ -224,7 +209,6 @@ pub fn init_settings(cx: &mut AppContext) {
 
 pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
     init_settings(cx);
-    pane::init(cx);
     notifications::init(cx);
 
     //     cx.add_global_action({
@@ -322,12 +306,6 @@ pub struct AppState {
     pub fs: Arc<dyn fs2::Fs>,
     pub build_window_options:
         fn(Option<WindowBounds>, Option<Uuid>, &mut AppContext) -> WindowOptions,
-    pub initialize_workspace: fn(
-        WeakView<Workspace>,
-        bool,
-        Arc<AppState>,
-        AsyncWindowContext,
-    ) -> Task<anyhow::Result<()>>,
     pub node_runtime: Arc<dyn NodeRuntime>,
 }
 
@@ -362,7 +340,7 @@ impl AppState {
         let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http_client, cx));
         let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx));
 
-        theme2::init(cx);
+        theme2::init(theme2::LoadThemes::JustBase, cx);
         client2::init(&client, cx);
         crate::init_settings(cx);
 
@@ -373,7 +351,6 @@ impl AppState {
             user_store,
             workspace_store,
             node_runtime: FakeNodeRuntime::new(),
-            initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
             build_window_options: |_, _, _| Default::default(),
         })
     }
@@ -432,6 +409,7 @@ pub enum Event {
 }
 
 pub struct Workspace {
+    window_self: WindowHandle<Self>,
     weak_self: WeakView<Self>,
     workspace_actions: Vec<Box<dyn Fn(Div<Workspace>) -> Div<Workspace>>>,
     zoomed: Option<AnyWeakView>,
@@ -447,7 +425,7 @@ pub struct Workspace {
     last_active_view_id: Option<proto::ViewId>,
     status_bar: View<StatusBar>,
     modal_layer: View<ModalLayer>,
-    //     titlebar_item: Option<AnyViewHandle>,
+    titlebar_item: Option<AnyView>,
     notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
     project: Model<Project>,
     follower_states: HashMap<View<Pane>, FollowerState>,
@@ -464,6 +442,8 @@ pub struct Workspace {
     pane_history_timestamp: Arc<AtomicUsize>,
 }
 
+impl EventEmitter<Event> for Workspace {}
+
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
 pub struct ViewId {
     pub creator: PeerId,
@@ -541,8 +521,8 @@ impl Workspace {
             )
         });
         cx.subscribe(&center_pane, Self::handle_pane_event).detach();
-        // todo!()
-        // cx.focus(&center_pane);
+
+        cx.focus_view(&center_pane);
         cx.emit(Event::PaneAdded(center_pane.clone()));
 
         let window_handle = cx.window_handle().downcast::<Workspace>().unwrap();
@@ -645,10 +625,16 @@ impl Workspace {
                 this.serialize_workspace(cx);
                 cx.notify();
             }),
+            cx.on_release(|this, cx| {
+                this.app_state.workspace_store.update(cx, |store, _| {
+                    store.workspaces.remove(&this.window_self);
+                })
+            }),
         ];
 
         cx.defer(|this, cx| this.update_window_title(cx));
         Workspace {
+            window_self: window_handle,
             weak_self: weak_handle.clone(),
             zoomed: None,
             zoomed_position: None,
@@ -660,7 +646,7 @@ impl Workspace {
             last_active_view_id: None,
             status_bar,
             modal_layer,
-            // titlebar_item: None,
+            titlebar_item: None,
             notifications: Default::default(),
             left_dock,
             bottom_dock,
@@ -685,7 +671,7 @@ impl Workspace {
     fn new_local(
         abs_paths: Vec<PathBuf>,
         app_state: Arc<AppState>,
-        _requesting_window: Option<WindowHandle<Workspace>>,
+        requesting_window: Option<WindowHandle<Workspace>>,
         cx: &mut AppContext,
     ) -> Task<
         anyhow::Result<(
@@ -703,7 +689,8 @@ impl Workspace {
         );
 
         cx.spawn(|mut cx| async move {
-            let serialized_workspace: Option<SerializedWorkspace> = None; //persistence::DB.workspace_for_roots(&abs_paths.as_slice());
+            let serialized_workspace: Option<SerializedWorkspace> =
+                persistence::DB.workspace_for_roots(&abs_paths.as_slice());
 
             let paths_to_open = Arc::new(abs_paths);
 
@@ -732,15 +719,14 @@ impl Workspace {
                 DB.next_id().await.unwrap_or(0)
             };
 
-            // todo!()
-            let window = /*if let Some(window) = requesting_window {
+            let window = if let Some(window) = requesting_window {
                 cx.update_window(window.into(), |old_workspace, cx| {
                     cx.replace_root_view(|cx| {
                         Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
                     });
-                });
+                })?;
                 window
-                } else */ {
+            } else {
                 let window_bounds_override = window_bounds_env_override(&cx);
                 let (bounds, display) = if let Some(bounds) = window_bounds_override {
                     (Some(bounds), None)
@@ -754,12 +740,13 @@ impl Workspace {
                             // 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 = 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;
@@ -787,19 +774,6 @@ impl Workspace {
                 })?
             };
 
-            // todo!() Ask how to do this
-            let weak_view = window.update(&mut cx, |_, cx| cx.view().downgrade())?;
-            let async_cx = window.update(&mut cx, |_, cx| cx.to_async())?;
-
-            (app_state.initialize_workspace)(
-                weak_view,
-                serialized_workspace.is_some(),
-                app_state.clone(),
-                async_cx,
-            )
-            .await
-            .log_err();
-
             window
                 .update(&mut cx, |_, cx| cx.activate_window())
                 .log_err();
@@ -807,12 +781,7 @@ impl Workspace {
             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,
-                    )
+                    open_items(serialized_workspace, project_paths, app_state, cx)
                 })?
                 .await
                 .unwrap_or_default();
@@ -977,12 +946,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);
@@ -1033,15 +1002,14 @@ impl Workspace {
         &self.app_state.client
     }
 
-    // todo!()
-    // pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext<Self>) {
-    //     self.titlebar_item = Some(item);
-    //     cx.notify();
-    // }
+    pub fn set_titlebar_item(&mut self, item: AnyView, cx: &mut ViewContext<Self>) {
+        self.titlebar_item = Some(item);
+        cx.notify();
+    }
 
-    // pub fn titlebar_item(&self) -> Option<AnyViewHandle> {
-    //     self.titlebar_item.clone()
-    // }
+    pub fn titlebar_item(&self) -> Option<AnyView> {
+        self.titlebar_item.clone()
+    }
 
     /// Call the given callback with a workspace whose project is local.
     ///
@@ -1091,35 +1059,40 @@ 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();
-    //     }
+    // todo!(Non-window-actions)
+    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(
-    //         &mut self,
-    //         _: &CloseWindow,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Option<Task<Result<()>>> {
-    //         let window = cx.window();
-    //         let prepare = self.prepare_to_close(false, cx);
-    //         Some(cx.spawn(|_, mut cx| async move {
-    //             if prepare.await? {
-    //                 window.remove(&mut cx);
-    //             }
-    //             Ok(())
-    //         }))
-    //     }
+    pub fn close(
+        &mut self,
+        _: &CloseWindow,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let window = cx.window_handle();
+        let prepare = self.prepare_to_close(false, cx);
+        Some(cx.spawn(|_, mut cx| async move {
+            if prepare.await? {
+                window.update(&mut cx, |_, cx| {
+                    cx.remove_window();
+                })?;
+            }
+            Ok(())
+        }))
+    }
 
     pub fn prepare_to_close(
         &mut self,
@@ -1127,184 +1100,177 @@ impl Workspace {
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<bool>> {
         //todo!(saveing)
-        // let active_call = self.active_call().cloned();
-        // let window = cx.window();
+        let active_call = self.active_call().cloned();
+        let window = cx.window_handle();
 
         cx.spawn(|this, mut cx| async move {
-            // let workspace_count = cx
-            //     .windows()
-            //     .into_iter()
-            //     .filter(|window| window.root_is::<Workspace>())
-            //     .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.prompt(
-            //             PromptLevel::Warning,
-            //             "Do you want to leave the current call?",
-            //             &["Close window and hang up", "Cancel"],
-            //             &mut cx,
-            //         );
-
-            //         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();
-            //             }
-            //         }
-            //     }
-            // }
+            let workspace_count = cx.update(|_, cx| {
+                cx.windows()
+                    .iter()
+                    .filter(|window| window.downcast::<Workspace>().is_some())
+                    .count()
+            })?;
 
-            Ok(
-                false, // this
-                      // .update(&mut cx, |this, cx| {
-                      //     this.save_all_internal(SaveIntent::Close, cx)
-                      // })?
-                      // .await?
-            )
+            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>,
-    //     ) -> Option<Task<Result<()>>> {
-    //         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>) {
+        let save_all = self
+            .save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx)
+            .detach_and_log_err(cx);
+    }
 
-    //     fn save_all_internal(
-    //         &mut self,
-    //         mut save_intent: SaveIntent,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Task<Result<bool>> {
-    //         if self.project.read(cx).is_read_only() {
-    //             return Task::ready(Ok(true));
-    //         }
-    //         let dirty_items = self
-    //             .panes
-    //             .iter()
-    //             .flat_map(|pane| {
-    //                 pane.read(cx).items().filter_map(|item| {
-    //                     if item.is_dirty(cx) {
-    //                         Some((pane.downgrade(), item.boxed_clone()))
-    //                     } else {
-    //                         None
-    //                     }
-    //                 })
-    //             })
-    //             .collect::<Vec<_>>();
-
-    //         let project = self.project.clone();
-    //         cx.spawn(|workspace, mut cx| async move {
-    //             // Override save mode and display "Save all files" prompt
-    //             if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
-    //                 let mut 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.next().await {
-    //                     Some(0) => save_intent = SaveIntent::SaveAll,
-    //                     Some(1) => save_intent = SaveIntent::Skip,
-    //                     _ => {}
-    //                 }
-    //             }
-    //             for (pane, item) in dirty_items {
-    //                 let (singleton, project_entry_ids) =
-    //                     cx.read(|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()))?
-    //                     {
-    //                         if !Pane::save_item(
-    //                             project.clone(),
-    //                             &pane,
-    //                             ix,
-    //                             &*item,
-    //                             save_intent,
-    //                             &mut cx,
-    //                         )
-    //                         .await?
-    //                         {
-    //                             return Ok(false);
-    //                         }
-    //                     }
-    //                 }
-    //             }
-    //             Ok(true)
-    //         })
-    //     }
+    fn save_all_internal(
+        &mut self,
+        mut save_intent: SaveIntent,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<bool>> {
+        if self.project.read(cx).is_read_only() {
+            return Task::ready(Ok(true));
+        }
+        let dirty_items = self
+            .panes
+            .iter()
+            .flat_map(|pane| {
+                pane.read(cx).items().filter_map(|item| {
+                    if item.is_dirty(cx) {
+                        Some((pane.downgrade(), item.boxed_clone()))
+                    } else {
+                        None
+                    }
+                })
+            })
+            .collect::<Vec<_>>();
 
-    //     pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
-    //         let mut paths = cx.prompt_for_paths(PathPromptOptions {
-    //             files: true,
-    //             directories: true,
-    //             multiple: true,
-    //         });
+        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 mut 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)
+        })
+    }
 
-    //         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?
-    //                 }
-    //             }
-    //             Ok(())
-    //         }))
-    //     }
+    pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
+        let mut paths = cx.prompt_for_paths(PathPromptOptions {
+            files: true,
+            directories: true,
+            multiple: true,
+        });
 
-    //     pub fn open_workspace_for_paths(
-    //         &mut self,
-    //         paths: Vec<PathBuf>,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Task<Result<()>> {
-    //         let window = cx.window().downcast::<Self>();
-    //         let is_remote = self.project.read(cx).is_remote();
-    //         let has_worktree = self.project.read(cx).worktrees(cx).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(|this, mut cx| async move {
+            let Some(paths) = paths.await.log_err().flatten() else {
+                return;
+            };
 
-    //         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(())
-    //         })
-    //     }
+            if let Some(task) = this
+                .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx))
+                .log_err()
+            {
+                task.await.log_err();
+            }
+        })
+        .detach()
+    }
+
+    pub fn open_workspace_for_paths(
+        &mut self,
+        paths: Vec<PathBuf>,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
+        let window = cx.window_handle().downcast::<Self>();
+        let is_remote = self.project.read(cx).is_remote();
+        let has_worktree = self.project.read(cx).worktrees().next().is_some();
+        let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
+        let close_task = if is_remote || has_worktree || has_dirty_items {
+            None
+        } else {
+            Some(self.prepare_to_close(false, cx))
+        };
+        let app_state = self.app_state.clone();
+
+        cx.spawn(|_, mut cx| async move {
+            let window_to_replace = if let Some(close_task) = close_task {
+                if !close_task.await? {
+                    return Ok(());
+                }
+                window
+            } else {
+                None
+            };
+            cx.update(|_, cx| open_paths(&paths, &app_state, window_to_replace, cx))?
+                .await?;
+            Ok(())
+        })
+    }
 
     #[allow(clippy::type_complexity)]
     pub fn open_paths(
@@ -1382,25 +1348,25 @@ impl Workspace {
         })
     }
 
-    //     fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
-    //         let mut 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() {
-    //                 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 add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
+        let mut paths = cx.prompt_for_paths(PathPromptOptions {
+            files: false,
+            directories: true,
+            multiple: true,
+        });
+        cx.spawn(|this, mut cx| async move {
+            if let Some(paths) = paths.await.log_err().flatten() {
+                let results = this
+                    .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))?
+                    .await;
+                for result in results.into_iter().flatten() {
+                    result.log_err();
+                }
+            }
+            anyhow::Ok(())
+        })
+        .detach_and_log_err(cx);
+    }
 
     fn project_path_for_path(
         project: Model<Project>,
@@ -1432,7 +1398,7 @@ impl Workspace {
     }
 
     pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<View<T>> {
-        self.items_of_type(cx).max_by_key(|item| item.entity_id())
+        self.items_of_type(cx).max_by_key(|item| item.item_id())
     }
 
     pub fn items_of_type<'a, T: Item>(
@@ -1479,68 +1445,70 @@ impl Workspace {
         })
     }
 
-    //     pub fn close_inactive_items_and_panes(
-    //         &mut self,
-    //         _: &CloseInactiveTabsAndPanes,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Option<Task<Result<()>>> {
-    //         self.close_all_internal(true, SaveIntent::Close, cx)
-    //     }
+    pub fn close_inactive_items_and_panes(
+        &mut self,
+        _: &CloseInactiveTabsAndPanes,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.close_all_internal(true, SaveIntent::Close, cx)
+            .map(|task| task.detach_and_log_err(cx));
+    }
 
-    //     pub fn close_all_items_and_panes(
-    //         &mut self,
-    //         action: &CloseAllItemsAndPanes,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Option<Task<Result<()>>> {
-    //         self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx)
-    //     }
+    pub fn close_all_items_and_panes(
+        &mut self,
+        action: &CloseAllItemsAndPanes,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx)
+            .map(|task| task.detach_and_log_err(cx));
+    }
 
-    //     fn close_all_internal(
-    //         &mut self,
-    //         retain_active_pane: bool,
-    //         save_intent: SaveIntent,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Option<Task<Result<()>>> {
-    //         let current_pane = self.active_pane();
+    fn close_all_internal(
+        &mut self,
+        retain_active_pane: bool,
+        save_intent: SaveIntent,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let current_pane = self.active_pane();
 
-    //         let mut tasks = Vec::new();
+        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);
-    //             };
-    //         }
+        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.id() == current_pane.id() {
-    //                 continue;
-    //             }
+        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 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(())
-    //             }))
-    //         }
-    //     }
+        if tasks.is_empty() {
+            None
+        } else {
+            Some(cx.spawn(|_, _| async move {
+                for task in tasks {
+                    task.await?
+                }
+                Ok(())
+            }))
+        }
+    }
 
     pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
         let dock = match dock_side {

crates/zed2/Cargo.toml 🔗

@@ -11,7 +11,7 @@ path = "src/zed2.rs"
 doctest = false
 
 [[bin]]
-name = "Zed"
+name = "Zed2"
 path = "src/main.rs"
 
 [dependencies]
@@ -23,11 +23,10 @@ ai = { package = "ai2", path = "../ai2"}
 call = { package = "call2", path = "../call2" }
 # channel = { path = "../channel" }
 cli = { path = "../cli" }
-# collab_ui = { path = "../collab_ui" }
+collab_ui = { package = "collab_ui2", path = "../collab_ui2" }
 collections = { path = "../collections" }
 command_palette = { package="command_palette2", path = "../command_palette2" }
 # component_test = { path = "../component_test" }
-# context_menu = { path = "../context_menu" }
 client = { package = "client2", path = "../client2" }
 # clock = { path = "../clock" }
 copilot = { package = "copilot2", path = "../copilot2" }
@@ -66,7 +65,7 @@ feature_flags = { package = "feature_flags2", path = "../feature_flags2" }
 sum_tree = { path = "../sum_tree" }
 shellexpand = "2.1.0"
 text = { package = "text2", path = "../text2" }
-# terminal_view = { path = "../terminal_view" }
+terminal_view = { package = "terminal_view2", path = "../terminal_view2" }
 theme = { package = "theme2", path = "../theme2" }
 # theme_selector = { path = "../theme_selector" }
 util = { path = "../util" }

crates/zed2/src/languages/json.rs 🔗

@@ -107,7 +107,7 @@ impl LspAdapter for JsonLspAdapter {
         &self,
         cx: &mut AppContext,
     ) -> BoxFuture<'static, serde_json::Value> {
-        let action_names = gpui::all_action_names();
+        let action_names = cx.all_action_names();
         let staff_mode = cx.is_staff();
         let language_names = &self.languages.language_names();
         let settings_schema = cx.global::<SettingsStore>().json_schema(

crates/zed2/src/main.rs 🔗

@@ -50,8 +50,8 @@ use util::{
 use uuid::Uuid;
 use workspace::{AppState, WorkspaceStore};
 use zed2::{
-    build_window_options, ensure_only_instance, handle_cli_connection, init_zed_actions,
-    initialize_workspace, languages, Assets, IsOnlyInstance, OpenListener, OpenRequest,
+    build_window_options, ensure_only_instance, handle_cli_connection, initialize_workspace,
+    languages, Assets, IsOnlyInstance, OpenListener, OpenRequest,
 };
 
 mod open_listener;
@@ -140,8 +140,7 @@ fn main() {
 
         cx.set_global(client.clone());
 
-        theme::init(cx);
-        // context_menu::init(cx);
+        theme::init(theme::LoadThemes::All, cx);
         project::Project::init(&client, cx);
         client::init(&client, cx);
         command_palette::init(cx);
@@ -177,7 +176,6 @@ fn main() {
             user_store,
             fs,
             build_window_options,
-            initialize_workspace,
             // background_actions: todo!("ask Mikayla"),
             workspace_store,
             node_runtime,
@@ -200,7 +198,7 @@ fn main() {
         // search::init(cx);
         // semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);
         // vim::init(cx);
-        // terminal_view::init(cx);
+        terminal_view::init(cx);
 
         // journal2::init(app_state.clone(), cx);
         // language_selector::init(cx);
@@ -208,13 +206,13 @@ fn main() {
         // activity_indicator::init(cx);
         // language_tools::init(cx);
         call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
-        // collab_ui::init(&app_state, cx);
+        collab_ui::init(&app_state, cx);
         // feedback::init(cx);
         // welcome::init(cx);
         // zed::init(&app_state, cx);
 
         // cx.set_menus(menus::menus());
-        init_zed_actions(app_state.clone(), cx);
+        initialize_workspace(app_state.clone(), cx);
 
         if stdout_is_a_pty() {
             cx.activate(true);

crates/zed2/src/zed2.rs 🔗

@@ -10,16 +10,17 @@ pub use assets::*;
 use collections::VecDeque;
 use editor::{Editor, MultiBuffer};
 use gpui::{
-    actions, point, px, AppContext, AsyncWindowContext, Context, PromptLevel, Task,
-    TitlebarOptions, ViewContext, VisualContext, WeakView, WindowBounds, WindowKind, WindowOptions,
+    actions, point, px, AppContext, Context, PromptLevel, TitlebarOptions, ViewContext,
+    VisualContext, WindowBounds, WindowKind, WindowOptions,
 };
 pub use only_instance::*;
 pub use open_listener::*;
 
-use anyhow::{anyhow, Context as _, Result};
+use anyhow::{anyhow, Context as _};
 use project_panel::ProjectPanel;
 use settings::{initial_local_settings_content, Settings};
 use std::{borrow::Cow, ops::Deref, sync::Arc};
+use terminal_view::terminal_panel::TerminalPanel;
 use util::{
     asset_str,
     channel::ReleaseChannel,
@@ -86,8 +87,147 @@ pub fn build_window_options(
     }
 }
 
-pub fn init_zed_actions(app_state: Arc<AppState>, cx: &mut AppContext) {
-    cx.observe_new_views(move |workspace: &mut Workspace, _cx| {
+pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
+    cx.observe_new_views(move |workspace: &mut Workspace, cx| {
+        let workspace_handle = cx.view().clone();
+        cx.subscribe(&workspace_handle, {
+            move |workspace, _, event, cx| {
+                if let workspace::Event::PaneAdded(pane) = event {
+                    pane.update(cx, |pane, cx| {
+                        pane.toolbar().update(cx, |toolbar, cx| {
+                            // todo!()
+                            //     let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
+                            //     toolbar.add_item(breadcrumbs, cx);
+                            //     let buffer_search_bar = cx.add_view(BufferSearchBar::new);
+                            //     toolbar.add_item(buffer_search_bar.clone(), cx);
+                            //     let quick_action_bar = cx.add_view(|_| {
+                            //         QuickActionBar::new(buffer_search_bar, workspace)
+                            //     });
+                            //     toolbar.add_item(quick_action_bar, cx);
+                            let diagnostic_editor_controls =
+                                cx.build_view(|_| diagnostics::ToolbarControls::new());
+                            //     toolbar.add_item(diagnostic_editor_controls, cx);
+                            //     let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
+                            //     toolbar.add_item(project_search_bar, cx);
+                            //     let submit_feedback_button =
+                            //         cx.add_view(|_| SubmitFeedbackButton::new());
+                            //     toolbar.add_item(submit_feedback_button, cx);
+                            //     let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new());
+                            //     toolbar.add_item(feedback_info_text, cx);
+                            //     let lsp_log_item =
+                            //         cx.add_view(|_| language_tools::LspLogToolbarItemView::new());
+                            //     toolbar.add_item(lsp_log_item, cx);
+                            //     let syntax_tree_item = cx
+                            //         .add_view(|_| language_tools::SyntaxTreeToolbarItemView::new());
+                            //     toolbar.add_item(syntax_tree_item, cx);
+                        })
+                    });
+                }
+            }
+        })
+        .detach();
+
+        //     cx.emit(workspace2::Event::PaneAdded(
+        //         workspace.active_pane().clone(),
+        //     ));
+
+        //     let collab_titlebar_item =
+        //         cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
+        //     workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
+
+        //     let copilot =
+        //         cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
+        let diagnostic_summary =
+            cx.build_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
+        //     let activity_indicator = activity_indicator::ActivityIndicator::new(
+        //         workspace,
+        //         app_state.languages.clone(),
+        //         cx,
+        //     );
+        //     let active_buffer_language =
+        //         cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
+        //     let vim_mode_indicator = cx.add_view(|cx| vim::ModeIndicator::new(cx));
+        //     let feedback_button = cx.add_view(|_| {
+        //         feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace)
+        //     });
+        //     let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
+        workspace.status_bar().update(cx, |status_bar, cx| {
+            // status_bar.add_left_item(diagnostic_summary, cx);
+            // status_bar.add_left_item(activity_indicator, cx);
+
+            // status_bar.add_right_item(feedback_button, cx);
+            // status_bar.add_right_item(copilot, cx);
+            // status_bar.add_right_item(active_buffer_language, cx);
+            // status_bar.add_right_item(vim_mode_indicator, cx);
+            // status_bar.add_right_item(cursor_position, cx);
+        });
+
+        //     auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
+
+        //     vim::observe_keystrokes(cx);
+
+        //     cx.on_window_should_close(|workspace, cx| {
+        //         if let Some(task) = workspace.close(&Default::default(), cx) {
+        //             task.detach_and_log_err(cx);
+        //         }
+        //         false
+        //     });
+
+        cx.spawn(|workspace_handle, mut cx| async move {
+            let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
+            let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
+            // let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
+            let channels_panel =
+                collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
+            // let chat_panel =
+            //     collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
+            // let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
+            //     workspace_handle.clone(),
+            //     cx.clone(),
+            // );
+            let (
+                project_panel,
+                terminal_panel,
+                //     assistant_panel,
+                channels_panel,
+                //     chat_panel,
+                //     notification_panel,
+            ) = futures::try_join!(
+                project_panel,
+                terminal_panel,
+                //     assistant_panel,
+                channels_panel,
+                //     chat_panel,
+                //     notification_panel,
+            )?;
+
+            workspace_handle.update(&mut cx, |workspace, cx| {
+                let project_panel_position = project_panel.position(cx);
+                workspace.add_panel(project_panel, cx);
+                workspace.add_panel(terminal_panel, cx);
+                //     workspace.add_panel(assistant_panel, cx);
+                workspace.add_panel(channels_panel, cx);
+                //     workspace.add_panel(chat_panel, cx);
+                //     workspace.add_panel(notification_panel, cx);
+
+                //     if !was_deserialized
+                //         && workspace
+                //             .project()
+                //             .read(cx)
+                //             .visible_worktrees(cx)
+                //             .any(|tree| {
+                //                 tree.read(cx)
+                //                     .root_entry()
+                //                     .map_or(false, |entry| entry.is_dir())
+                //             })
+                //     {
+                // workspace.toggle_dock(project_panel_position, cx);
+                //     }
+                // cx.focus_self();
+            })
+        })
+        .detach();
+
         workspace
             .register_action(about)
             .register_action(|_, _: &Hide, cx| {
@@ -291,154 +431,6 @@ pub fn init_zed_actions(app_state: Arc<AppState>, cx: &mut AppContext) {
     .detach();
 }
 
-pub fn initialize_workspace(
-    workspace_handle: WeakView<Workspace>,
-    was_deserialized: bool,
-    app_state: Arc<AppState>,
-    cx: AsyncWindowContext,
-) -> Task<Result<()>> {
-    cx.spawn(|mut cx| async move {
-        workspace_handle.update(&mut cx, |workspace, cx| {
-            let workspace_handle = cx.view().clone();
-            cx.subscribe(&workspace_handle, {
-                move |workspace, _, event, cx| {
-                    if let workspace::Event::PaneAdded(pane) = event {
-                        pane.update(cx, |pane, cx| {
-                            pane.toolbar().update(cx, |toolbar, cx| {
-                                // todo!()
-                                //     let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
-                                //     toolbar.add_item(breadcrumbs, cx);
-                                //     let buffer_search_bar = cx.add_view(BufferSearchBar::new);
-                                //     toolbar.add_item(buffer_search_bar.clone(), cx);
-                                //     let quick_action_bar = cx.add_view(|_| {
-                                //         QuickActionBar::new(buffer_search_bar, workspace)
-                                //     });
-                                //     toolbar.add_item(quick_action_bar, cx);
-                                let diagnostic_editor_controls =
-                                    cx.build_view(|_| diagnostics::ToolbarControls::new());
-                                //     toolbar.add_item(diagnostic_editor_controls, cx);
-                                //     let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
-                                //     toolbar.add_item(project_search_bar, cx);
-                                //     let submit_feedback_button =
-                                //         cx.add_view(|_| SubmitFeedbackButton::new());
-                                //     toolbar.add_item(submit_feedback_button, cx);
-                                //     let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new());
-                                //     toolbar.add_item(feedback_info_text, cx);
-                                //     let lsp_log_item =
-                                //         cx.add_view(|_| language_tools::LspLogToolbarItemView::new());
-                                //     toolbar.add_item(lsp_log_item, cx);
-                                //     let syntax_tree_item = cx
-                                //         .add_view(|_| language_tools::SyntaxTreeToolbarItemView::new());
-                                //     toolbar.add_item(syntax_tree_item, cx);
-                            })
-                        });
-                    }
-                }
-            })
-            .detach();
-
-            //     cx.emit(workspace2::Event::PaneAdded(
-            //         workspace.active_pane().clone(),
-            //     ));
-
-            //     let collab_titlebar_item =
-            //         cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
-            //     workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
-
-            //     let copilot =
-            //         cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
-            let diagnostic_summary =
-                cx.build_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
-            //     let activity_indicator = activity_indicator::ActivityIndicator::new(
-            //         workspace,
-            //         app_state.languages.clone(),
-            //         cx,
-            //     );
-            //     let active_buffer_language =
-            //         cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
-            //     let vim_mode_indicator = cx.add_view(|cx| vim::ModeIndicator::new(cx));
-            //     let feedback_button = cx.add_view(|_| {
-            //         feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace)
-            //     });
-            //     let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
-            workspace.status_bar().update(cx, |status_bar, cx| {
-                // status_bar.add_left_item(diagnostic_summary, cx);
-                // status_bar.add_left_item(activity_indicator, cx);
-
-                // status_bar.add_right_item(feedback_button, cx);
-                // status_bar.add_right_item(copilot, cx);
-                // status_bar.add_right_item(active_buffer_language, cx);
-                // status_bar.add_right_item(vim_mode_indicator, cx);
-                // status_bar.add_right_item(cursor_position, cx);
-            });
-
-            //     auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
-
-            //     vim::observe_keystrokes(cx);
-
-            //     cx.on_window_should_close(|workspace, cx| {
-            //         if let Some(task) = workspace.close(&Default::default(), cx) {
-            //             task.detach_and_log_err(cx);
-            //         }
-            //         false
-            //     });
-        })?;
-
-        let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
-        // let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
-        // let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
-        // let channels_panel =
-        //     collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
-        // let chat_panel =
-        //     collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
-        // let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
-        //     workspace_handle.clone(),
-        //     cx.clone(),
-        // );
-        let (
-            project_panel,
-            //     terminal_panel,
-            //     assistant_panel,
-            //     channels_panel,
-            //     chat_panel,
-            //     notification_panel,
-        ) = futures::try_join!(
-            project_panel,
-            //     terminal_panel,
-            //     assistant_panel,
-            //     channels_panel,
-            //     chat_panel,
-            //     notification_panel,
-        )?;
-
-        workspace_handle.update(&mut cx, |workspace, cx| {
-            let project_panel_position = project_panel.position(cx);
-            workspace.add_panel(project_panel, cx);
-            //     workspace.add_panel(terminal_panel, cx);
-            //     workspace.add_panel(assistant_panel, cx);
-            //     workspace.add_panel(channels_panel, cx);
-            //     workspace.add_panel(chat_panel, cx);
-            //     workspace.add_panel(notification_panel, cx);
-
-            //     if !was_deserialized
-            //         && workspace
-            //             .project()
-            //             .read(cx)
-            //             .visible_worktrees(cx)
-            //             .any(|tree| {
-            //                 tree.read(cx)
-            //                     .root_entry()
-            //                     .map_or(false, |entry| entry.is_dir())
-            //             })
-            //     {
-            workspace.toggle_dock(project_panel_position, cx);
-            //     }
-            // cx.focus_self();
-        })?;
-        Ok(())
-    })
-}
-
 fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
     let app_name = cx.global::<ReleaseChannel>().display_name();
     let version = env!("CARGO_PKG_VERSION");

crates/zed_actions2/src/lib.rs 🔗

@@ -1,4 +1,5 @@
-use gpui::action;
+use gpui::Action;
+use serde::Deserialize;
 
 // If the zed binary doesn't use anything in this crate, it will be optimized away
 // and the actions won't initialize. So we just provide an empty initialization function
@@ -9,12 +10,12 @@ use gpui::action;
 // https://github.com/mmastrac/rust-ctor/issues/280
 pub fn init() {}
 
-#[action]
+#[derive(Clone, PartialEq, Deserialize, Action)]
 pub struct OpenBrowser {
     pub url: String,
 }
 
-#[action]
+#[derive(Clone, PartialEq, Deserialize, Action)]
 pub struct OpenZedURL {
     pub url: String,
 }