Merge branch 'main' into user-timeline

Antonio Scandurra created

Change summary

Cargo.lock                                          | 224 ++++
assets/keymaps/default.json                         | 123 ++
assets/keymaps/vim.json                             |  22 
crates/activity_indicator/Cargo.toml                |   5 
crates/activity_indicator/src/activity_indicator.rs | 185 ++-
crates/auto_update/src/auto_update.rs               | 104 -
crates/client/src/client.rs                         |  17 
crates/collab/src/db.rs                             |  11 
crates/collab/src/integration_tests.rs              |  39 
crates/collab/src/rpc.rs                            | 169 +-
crates/collab/src/rpc/store.rs                      |   3 
crates/contacts_panel/src/contacts_panel.rs         |   3 
crates/editor/src/element.rs                        |  42 
crates/gpui/src/app.rs                              |   8 
crates/gpui/src/presenter.rs                        |  14 
crates/gpui/src/text_layout.rs                      |   2 
crates/lsp/src/lsp.rs                               |  29 
crates/project/src/lsp_command.rs                   |  14 
crates/project/src/project.rs                       | 707 ++++++++------
crates/project/src/worktree.rs                      | 503 ++++------
crates/project_panel/src/project_panel.rs           |   3 
crates/rpc/proto/zed.proto                          |  12 
crates/rpc/src/proto.rs                             |  26 
crates/search/src/buffer_search.rs                  |   2 
crates/terminal/Cargo.toml                          |  27 
crates/terminal/print256color.sh                    |  96 ++
crates/terminal/src/terminal.rs                     | 494 ++++++++++
crates/terminal/src/terminal_element.rs             | 621 +++++++++++++
crates/terminal/truecolor.sh                        |  19 
crates/theme/src/theme.rs                           |  34 
crates/vim/Cargo.toml                               |   1 
crates/vim/src/editor_events.rs                     |  17 
crates/vim/src/normal.rs                            |   2 
crates/vim/src/normal/delete.rs                     |  40 
crates/vim/src/state.rs                             |  27 
crates/vim/src/vim.rs                               | 135 ++
crates/vim/src/vim_test_context.rs                  |  26 
crates/workspace/src/pane.rs                        |  14 
crates/workspace/src/sidebar.rs                     |  54 
crates/workspace/src/waiting_room.rs                |  13 
crates/workspace/src/workspace.rs                   |  70 +
crates/zed/Cargo.toml                               |   3 
crates/zed/src/main.rs                              |   2 
crates/zed/src/menus.rs                             |  41 
crates/zed/src/zed.rs                               |  24 
styles/src/styleTree/app.ts                         |   2 
styles/src/styleTree/terminal.ts                    |  35 
styles/src/themes/cave.ts                           |   2 
styles/src/themes/common/base16.ts                  |  22 
styles/src/themes/common/theme.ts                   |   3 
50 files changed, 3,016 insertions(+), 1,075 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -2,6 +2,22 @@
 # It is not intended for manual editing.
 version = 3
 
+[[package]]
+name = "activity_indicator"
+version = "0.1.0"
+dependencies = [
+ "auto_update",
+ "editor",
+ "futures",
+ "gpui",
+ "language",
+ "project",
+ "settings",
+ "smallvec",
+ "util",
+ "workspace",
+]
+
 [[package]]
 name = "addr2line"
 version = "0.17.0"
@@ -43,6 +59,45 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "alacritty_config_derive"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77044c45bdb871e501b5789ad16293ecb619e5733b60f4bb01d1cb31c463c336"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "alacritty_terminal"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02fb5d4af84e39f9754d039ff6de2233c8996dbae0af74910156e559e5766e2f"
+dependencies = [
+ "alacritty_config_derive",
+ "base64 0.13.0",
+ "bitflags",
+ "dirs 3.0.2",
+ "libc",
+ "log",
+ "mio 0.6.23",
+ "mio-anonymous-pipes",
+ "mio-extras",
+ "miow 0.3.7",
+ "nix",
+ "parking_lot 0.11.2",
+ "regex-automata",
+ "serde",
+ "serde_yaml",
+ "signal-hook",
+ "signal-hook-mio",
+ "unicode-width",
+ "vte",
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "ansi_term"
 version = "0.12.1"
@@ -2500,6 +2555,12 @@ dependencies = [
  "safemem",
 ]
 
+[[package]]
+name = "linked-hash-map"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
+
 [[package]]
 name = "lipsum"
 version = "0.8.2"
@@ -2566,21 +2627,6 @@ dependencies = [
  "url",
 ]
 
-[[package]]
-name = "lsp_status"
-version = "0.1.0"
-dependencies = [
- "editor",
- "futures",
- "gpui",
- "language",
- "project",
- "settings",
- "smallvec",
- "util",
- "workspace",
-]
-
 [[package]]
 name = "malloc_buf"
 version = "0.0.6"
@@ -2724,7 +2770,7 @@ dependencies = [
  "kernel32-sys",
  "libc",
  "log",
- "miow",
+ "miow 0.2.2",
  "net2",
  "slab",
  "winapi 0.2.8",
@@ -2742,6 +2788,42 @@ dependencies = [
  "windows-sys",
 ]
 
+[[package]]
+name = "mio-anonymous-pipes"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bc513025fe5005a3aa561b50fdb2cda5a150b84800ae02acd8aa9ed62ca1a6b"
+dependencies = [
+ "mio 0.6.23",
+ "miow 0.3.7",
+ "parking_lot 0.11.2",
+ "spsc-buffer",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "mio-extras"
+version = "2.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
+dependencies = [
+ "lazycell",
+ "log",
+ "mio 0.6.23",
+ "slab",
+]
+
+[[package]]
+name = "mio-uds"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0"
+dependencies = [
+ "iovec",
+ "libc",
+ "mio 0.6.23",
+]
+
 [[package]]
 name = "miow"
 version = "0.2.2"
@@ -2754,6 +2836,15 @@ dependencies = [
  "ws2_32-sys",
 ]
 
+[[package]]
+name = "miow"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
+dependencies = [
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "multimap"
 version = "0.8.3"
@@ -2798,6 +2889,19 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "nix"
+version = "0.22.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4916f159ed8e5de0082076562152a76b7a1f64a01fd9d1e0fea002c37624faf"
+dependencies = [
+ "bitflags",
+ "cc",
+ "cfg-if 1.0.0",
+ "libc",
+ "memoffset",
+]
+
 [[package]]
 name = "nom"
 version = "7.1.1"
@@ -4252,6 +4356,18 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "serde_yaml"
+version = "0.8.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "707d15895415db6628332b737c838b88c598522e4dc70647e59b72312924aebc"
+dependencies = [
+ "indexmap",
+ "ryu",
+ "serde",
+ "yaml-rust",
+]
+
 [[package]]
 name = "servo-fontconfig"
 version = "0.5.1"
@@ -4364,6 +4480,18 @@ dependencies = [
  "signal-hook-registry",
 ]
 
+[[package]]
+name = "signal-hook-mio"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
+dependencies = [
+ "libc",
+ "mio 0.6.23",
+ "mio-uds",
+ "signal-hook",
+]
+
 [[package]]
 name = "signal-hook-registry"
 version = "1.4.0"
@@ -4492,6 +4620,12 @@ version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
 
+[[package]]
+name = "spsc-buffer"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be6c3f39c37a4283ee4b43d1311c828f2e1fb0541e76ea0cb1a2abd9ef2f5b3b"
+
 [[package]]
 name = "sqlformat"
 version = "0.1.8"
@@ -4739,6 +4873,25 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "terminal"
+version = "0.1.0"
+dependencies = [
+ "alacritty_terminal",
+ "editor",
+ "futures",
+ "gpui",
+ "itertools",
+ "mio-extras",
+ "ordered-float",
+ "project",
+ "settings",
+ "smallvec",
+ "theme",
+ "util",
+ "workspace",
+]
+
 [[package]]
 name = "text"
 version = "0.1.0"
@@ -5531,6 +5684,12 @@ version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
 
+[[package]]
+name = "utf8parse"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372"
+
 [[package]]
 name = "util"
 version = "0.1.0"
@@ -5610,12 +5769,33 @@ dependencies = [
  "language",
  "log",
  "project",
+ "search",
  "serde",
  "settings",
  "util",
  "workspace",
 ]
 
+[[package]]
+name = "vte"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983"
+dependencies = [
+ "utf8parse",
+ "vte_generate_state_changes",
+]
+
+[[package]]
+name = "vte_generate_state_changes"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
+
 [[package]]
 name = "waker-fn"
 version = "1.1.0"
@@ -5967,10 +6147,20 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
 
+[[package]]
+name = "yaml-rust"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
+dependencies = [
+ "linked-hash-map",
+]
+
 [[package]]
 name = "zed"
 version = "0.42.0"
 dependencies = [
+ "activity_indicator",
  "anyhow",
  "assets",
  "async-compression",
@@ -6011,7 +6201,6 @@ dependencies = [
  "libc",
  "log",
  "lsp",
- "lsp_status",
  "num_cpus",
  "outline",
  "parking_lot 0.11.2",
@@ -6034,6 +6223,7 @@ dependencies = [
  "smol",
  "sum_tree",
  "tempdir",
+ "terminal",
  "text",
  "theme",
  "theme_selector",

assets/keymaps/default.json πŸ”—

@@ -13,6 +13,8 @@
             "ctrl-c": "menu::Cancel",
             "shift-cmd-{": "pane::ActivatePrevItem",
             "shift-cmd-}": "pane::ActivateNextItem",
+            "alt-cmd-left": "pane::ActivatePrevItem",
+            "alt-cmd-right": "pane::ActivateNextItem",
             "cmd-w": "pane::CloseActiveItem",
             "cmd-shift-W": "workspace::CloseWindow",
             "alt-cmd-t": "pane::CloseInactiveItems",
@@ -210,6 +212,43 @@
     {
         "context": "Pane",
         "bindings": {
+            "ctrl-1": [
+                "pane::ActivateItem",
+                0
+            ],
+            "ctrl-2": [
+                "pane::ActivateItem",
+                1
+            ],
+            "ctrl-3": [
+                "pane::ActivateItem",
+                2
+            ],
+            "ctrl-4": [
+                "pane::ActivateItem",
+                3
+            ],
+            "ctrl-5": [
+                "pane::ActivateItem",
+                4
+            ],
+            "ctrl-6": [
+                "pane::ActivateItem",
+                5
+            ],
+            "ctrl-7": [
+                "pane::ActivateItem",
+                6
+            ],
+            "ctrl-8": [
+                "pane::ActivateItem",
+                7
+            ],
+            "ctrl-9": [
+                "pane::ActivateItem",
+                8
+            ],
+            "ctrl-0": "pane::ActivateLastItem",
             "ctrl--": "pane::GoBack",
             "shift-ctrl-_": "pane::GoForward",
             "cmd-shift-T": "pane::ReopenClosedItem",
@@ -219,6 +258,43 @@
     {
         "context": "Workspace",
         "bindings": {
+            "cmd-1": [
+                "workspace::ActivatePane",
+                0
+            ],
+            "cmd-2": [
+                "workspace::ActivatePane",
+                1
+            ],
+            "cmd-3": [
+                "workspace::ActivatePane",
+                2
+            ],
+            "cmd-4": [
+                "workspace::ActivatePane",
+                3
+            ],
+            "cmd-5": [
+                "workspace::ActivatePane",
+                4
+            ],
+            "cmd-6": [
+                "workspace::ActivatePane",
+                5
+            ],
+            "cmd-7": [
+                "workspace::ActivatePane",
+                6
+            ],
+            "cmd-8": [
+                "workspace::ActivatePane",
+                7
+            ],
+            "cmd-9": [
+                "workspace::ActivatePane",
+                8
+            ],
+            "cmd-b": "workspace::ToggleLeftSidebar",
             "cmd-shift-F": "project_search::Deploy",
             "cmd-k cmd-t": "theme_selector::Toggle",
             "cmd-k cmd-s": "zed::OpenKeymap",
@@ -226,6 +302,7 @@
             "cmd-p": "file_finder::Toggle",
             "cmd-shift-P": "command_palette::Toggle",
             "cmd-shift-M": "diagnostics::Deploy",
+            "cmd-shift-E": "project_panel::Toggle",
             "cmd-alt-s": "workspace::SaveAll"
         }
     },
@@ -310,34 +387,8 @@
     {
         "context": "Workspace",
         "bindings": {
-            "cmd-1": [
-                "workspace::ToggleSidebarItemFocus",
-                {
-                    "side": "Left",
-                    "item_index": 0
-                }
-            ],
-            "cmd-shift-!": [
-                "workspace::ToggleSidebarItem",
-                {
-                    "side": "Left",
-                    "item_index": 0
-                }
-            ],
-            "cmd-9": [
-                "workspace::ToggleSidebarItemFocus",
-                {
-                    "side": "Right",
-                    "item_index": 0
-                }
-            ],
-            "cmd-shift-(": [
-                "workspace::ToggleSidebarItem",
-                {
-                    "side": "Right",
-                    "item_index": 0
-                }
-            ]
+            "cmd-shift-C": "contacts_panel::Toggle",
+            "cmd-shift-B": "workspace::ToggleRightSidebar"
         }
     },
     {
@@ -352,5 +403,21 @@
             "f2": "project_panel::Rename",
             "backspace": "project_panel::Delete"
         }
+    },
+    {
+        "context": "Terminal",
+        "bindings": {
+            "ctrl-c": "terminal::Sigint",
+            "escape": "terminal::Escape",
+            "ctrl-d": "terminal::Quit",
+            "backspace": "terminal::Del",
+            "enter": "terminal::Return",
+            "left": "terminal::Left",
+            "right": "terminal::Right",
+            "up": "terminal::Up",
+            "down": "terminal::Down",
+            "tab": "terminal::Tab",
+            "cmd-v": "terminal::Paste"
+        }
     }
 ]

assets/keymaps/vim.json πŸ”—

@@ -37,16 +37,12 @@
                     "ignorePunctuation": true
                 }
             ],
-            "escape": [
-                "vim::SwitchMode",
-                "Normal"
-            ]
+            "escape": "editor::Cancel"
         }
     },
     {
-        "context": "Editor && vim_mode == normal",
+        "context": "Editor && vim_mode == normal && vim_operator == none",
         "bindings": {
-            "escape": "editor::Cancel",
             "c": [
                 "vim::PushOperator",
                 "Change"
@@ -92,7 +88,13 @@
             "p": "vim::Paste",
             "u": "editor::Undo",
             "ctrl-r": "editor::Redo",
-            "ctrl-o": "pane::GoBack"
+            "ctrl-o": "pane::GoBack",
+            "/": [
+                "buffer_search::Deploy",
+                {
+                    "focus": true
+                }
+            ]
         }
     },
     {
@@ -146,11 +148,5 @@
             "escape": "vim::NormalBefore",
             "ctrl-c": "vim::NormalBefore"
         }
-    },
-    {
-        "context": "Editor && mode == singleline",
-        "bindings": {
-            "escape": "editor::Cancel"
-        }
     }
 ]

crates/lsp_status/Cargo.toml β†’ crates/activity_indicator/Cargo.toml πŸ”—

@@ -1,13 +1,14 @@
 [package]
-name = "lsp_status"
+name = "activity_indicator"
 version = "0.1.0"
 edition = "2021"
 
 [lib]
-path = "src/lsp_status.rs"
+path = "src/activity_indicator.rs"
 doctest = false
 
 [dependencies]
+auto_update = { path = "../auto_update" }
 editor = { path = "../editor" }
 language = { path = "../language" }
 gpui = { path = "../gpui" }

crates/lsp_status/src/lsp_status.rs β†’ crates/activity_indicator/src/activity_indicator.rs πŸ”—

@@ -1,7 +1,8 @@
+use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
 use editor::Editor;
 use futures::StreamExt;
 use gpui::{
-    actions, elements::*, platform::CursorStyle, AppContext, Entity, EventContext, ModelHandle,
+    actions, elements::*, platform::CursorStyle, Action, AppContext, Entity, ModelHandle,
     MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
 };
 use language::{LanguageRegistry, LanguageServerBinaryStatus};
@@ -14,13 +15,18 @@ use workspace::{ItemHandle, StatusItemView, Workspace};
 
 actions!(lsp_status, [ShowErrorMessage]);
 
+const DOWNLOAD_ICON: &'static str = "icons/download-solid-14.svg";
+const WARNING_ICON: &'static str = "icons/warning-solid-14.svg";
+const DONE_ICON: &'static str = "icons/accept.svg";
+
 pub enum Event {
     ShowError { lsp_name: Arc<str>, error: String },
 }
 
-pub struct LspStatusItem {
+pub struct ActivityIndicator {
     statuses: Vec<LspStatus>,
     project: ModelHandle<Project>,
+    auto_updater: Option<ModelHandle<AutoUpdater>>,
 }
 
 struct LspStatus {
@@ -29,15 +35,16 @@ struct LspStatus {
 }
 
 pub fn init(cx: &mut MutableAppContext) {
-    cx.add_action(LspStatusItem::show_error_message);
+    cx.add_action(ActivityIndicator::show_error_message);
+    cx.add_action(ActivityIndicator::dismiss_error_message);
 }
 
-impl LspStatusItem {
+impl ActivityIndicator {
     pub fn new(
         workspace: &mut Workspace,
         languages: Arc<LanguageRegistry>,
         cx: &mut ViewContext<Workspace>,
-    ) -> ViewHandle<LspStatusItem> {
+    ) -> ViewHandle<ActivityIndicator> {
         let project = workspace.project().clone();
         let this = cx.add_view(|cx: &mut ViewContext<Self>| {
             let mut status_events = languages.language_server_binary_statuses();
@@ -63,6 +70,7 @@ impl LspStatusItem {
             Self {
                 statuses: Default::default(),
                 project: project.clone(),
+                auto_updater: AutoUpdater::get(cx),
             }
         });
         cx.subscribe(&this, move |workspace, _, event, cx| match event {
@@ -106,6 +114,15 @@ impl LspStatusItem {
         cx.notify();
     }
 
+    fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
+        if let Some(updater) = &self.auto_updater {
+            updater.update(cx, |updater, cx| {
+                updater.dismiss_error(cx);
+            });
+        }
+        cx.notify();
+    }
+
     fn pending_language_server_work<'a>(
         &self,
         cx: &'a AppContext,
@@ -129,25 +146,15 @@ impl LspStatusItem {
             })
             .flatten()
     }
-}
-
-impl Entity for LspStatusItem {
-    type Event = Event;
-}
-
-impl View for LspStatusItem {
-    fn ui_name() -> &'static str {
-        "LspStatus"
-    }
-
-    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let mut message;
-        let mut icon = None;
-        let mut handler = None;
 
+    fn content_to_render(
+        &mut self,
+        cx: &mut RenderContext<Self>,
+    ) -> (Option<&'static str>, String, Option<Box<dyn Action>>) {
+        // Show any language server has pending activity.
         let mut pending_work = self.pending_language_server_work(cx);
         if let Some((lang_server_name, progress_token, progress)) = pending_work.next() {
-            message = lang_server_name.to_string();
+            let mut message = lang_server_name.to_string();
 
             message.push_str(": ");
             if let Some(progress_message) = progress.message.as_ref() {
@@ -164,38 +171,43 @@ impl View for LspStatusItem {
             if additional_work_count > 0 {
                 write!(&mut message, " + {} more", additional_work_count).unwrap();
             }
-        } else {
-            drop(pending_work);
 
-            let mut downloading = SmallVec::<[_; 3]>::new();
-            let mut checking_for_update = SmallVec::<[_; 3]>::new();
-            let mut failed = SmallVec::<[_; 3]>::new();
-            for status in &self.statuses {
-                match status.status {
-                    LanguageServerBinaryStatus::CheckingForUpdate => {
-                        checking_for_update.push(status.name.clone());
-                    }
-                    LanguageServerBinaryStatus::Downloading => {
-                        downloading.push(status.name.clone());
-                    }
-                    LanguageServerBinaryStatus::Failed { .. } => {
-                        failed.push(status.name.clone());
-                    }
-                    LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {
-                    }
+            return (None, message, None);
+        }
+
+        // Show any language server installation info.
+        let mut downloading = SmallVec::<[_; 3]>::new();
+        let mut checking_for_update = SmallVec::<[_; 3]>::new();
+        let mut failed = SmallVec::<[_; 3]>::new();
+        for status in &self.statuses {
+            match status.status {
+                LanguageServerBinaryStatus::CheckingForUpdate => {
+                    checking_for_update.push(status.name.clone());
+                }
+                LanguageServerBinaryStatus::Downloading => {
+                    downloading.push(status.name.clone());
+                }
+                LanguageServerBinaryStatus::Failed { .. } => {
+                    failed.push(status.name.clone());
                 }
+                LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {}
             }
+        }
 
-            if !downloading.is_empty() {
-                icon = Some("icons/download-solid-14.svg");
-                message = format!(
+        if !downloading.is_empty() {
+            return (
+                Some(DOWNLOAD_ICON),
+                format!(
                     "Downloading {} language server{}...",
                     downloading.join(", "),
                     if downloading.len() > 1 { "s" } else { "" }
-                );
-            } else if !checking_for_update.is_empty() {
-                icon = Some("icons/download-solid-14.svg");
-                message = format!(
+                ),
+                None,
+            );
+        } else if !checking_for_update.is_empty() {
+            return (
+                Some(DOWNLOAD_ICON),
+                format!(
                     "Checking for updates to {} language server{}...",
                     checking_for_update.join(", "),
                     if checking_for_update.len() > 1 {
@@ -203,19 +215,67 @@ impl View for LspStatusItem {
                     } else {
                         ""
                     }
-                );
-            } else if !failed.is_empty() {
-                icon = Some("icons/warning-solid-14.svg");
-                message = format!(
+                ),
+                None,
+            );
+        } else if !failed.is_empty() {
+            return (
+                Some(WARNING_ICON),
+                format!(
                     "Failed to download {} language server{}. Click to show error.",
                     failed.join(", "),
                     if failed.len() > 1 { "s" } else { "" }
-                );
-                handler = Some(|_, _, cx: &mut EventContext| cx.dispatch_action(ShowErrorMessage));
-            } else {
-                return Empty::new().boxed();
+                ),
+                Some(Box::new(ShowErrorMessage)),
+            );
+        }
+
+        // Show any application auto-update info.
+        if let Some(updater) = &self.auto_updater {
+            // let theme = &cx.global::<Settings>().theme.workspace.status_bar;
+            match &updater.read(cx).status() {
+                AutoUpdateStatus::Checking => (
+                    Some(DOWNLOAD_ICON),
+                    "Checking for Zed updates…".to_string(),
+                    None,
+                ),
+                AutoUpdateStatus::Downloading => (
+                    Some(DOWNLOAD_ICON),
+                    "Downloading Zed update…".to_string(),
+                    None,
+                ),
+                AutoUpdateStatus::Installing => (
+                    Some(DOWNLOAD_ICON),
+                    "Installing Zed update…".to_string(),
+                    None,
+                ),
+                AutoUpdateStatus::Updated => {
+                    (Some(DONE_ICON), "Restart to update Zed".to_string(), None)
+                }
+                AutoUpdateStatus::Errored => (
+                    Some(WARNING_ICON),
+                    "Auto update failed".to_string(),
+                    Some(Box::new(DismissErrorMessage)),
+                ),
+                AutoUpdateStatus::Idle => Default::default(),
             }
+        } else {
+            Default::default()
         }
+    }
+}
+
+impl Entity for ActivityIndicator {
+    type Event = Event;
+}
+
+impl View for ActivityIndicator {
+    fn ui_name() -> &'static str {
+        "ActivityIndicator"
+    }
+
+    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        let (icon, message, action) = self.content_to_render(cx);
 
         let mut element = MouseEventHandler::new::<Self, _, _>(0, cx, |state, cx| {
             let theme = &cx
@@ -224,7 +284,7 @@ impl View for LspStatusItem {
                 .workspace
                 .status_bar
                 .lsp_status;
-            let style = if state.hovered && handler.is_some() {
+            let style = if state.hovered && action.is_some() {
                 theme.hover.as_ref().unwrap_or(&theme.default)
             } else {
                 &theme.default
@@ -238,9 +298,14 @@ impl View for LspStatusItem {
                         .contained()
                         .with_margin_right(style.icon_spacing)
                         .aligned()
-                        .named("warning-icon")
+                        .named("activity-icon")
                 }))
-                .with_child(Label::new(message, style.message.clone()).aligned().boxed())
+                .with_child(
+                    Text::new(message, style.message.clone())
+                        .with_soft_wrap(false)
+                        .aligned()
+                        .boxed(),
+                )
                 .constrained()
                 .with_height(style.height)
                 .contained()
@@ -249,16 +314,16 @@ impl View for LspStatusItem {
                 .boxed()
         });
 
-        if let Some(handler) = handler {
+        if let Some(action) = action {
             element = element
                 .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(handler);
+                .on_click(move |_, _, cx| cx.dispatch_any_action(action.boxed_clone()));
         }
 
         element.boxed()
     }
 }
 
-impl StatusItemView for LspStatusItem {
+impl StatusItemView for ActivityIndicator {
     fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
 }

crates/auto_update/src/auto_update.rs πŸ”—

@@ -3,19 +3,15 @@ mod update_notification;
 use anyhow::{anyhow, Context, Result};
 use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN};
 use gpui::{
-    actions,
-    elements::{Empty, MouseEventHandler, Text},
-    platform::AppVersion,
-    AppContext, AsyncAppContext, Element, Entity, ModelContext, ModelHandle, MutableAppContext,
-    Task, View, ViewContext, WeakViewHandle,
+    actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
+    MutableAppContext, Task, WeakViewHandle,
 };
 use lazy_static::lazy_static;
 use serde::Deserialize;
-use settings::Settings;
 use smol::{fs::File, io::AsyncReadExt, process::Command};
 use std::{env, ffi::OsString, path::PathBuf, sync::Arc, time::Duration};
 use update_notification::UpdateNotification;
-use workspace::{ItemHandle, StatusItemView, Workspace};
+use workspace::Workspace;
 
 const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &'static str =
     "auto-updater-should-show-updated-notification";
@@ -30,7 +26,7 @@ lazy_static! {
 
 actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]);
 
-#[derive(Clone, PartialEq, Eq)]
+#[derive(Clone, Copy, PartialEq, Eq)]
 pub enum AutoUpdateStatus {
     Idle,
     Checking,
@@ -49,10 +45,6 @@ pub struct AutoUpdater {
     server_url: String,
 }
 
-pub struct AutoUpdateIndicator {
-    updater: Option<ModelHandle<AutoUpdater>>,
-}
-
 #[derive(Deserialize)]
 struct JsonRelease {
     version: String,
@@ -84,7 +76,6 @@ pub fn init(
         cx.add_global_action(move |_: &ViewReleaseNotes, cx| {
             cx.platform().open_url(&format!("{server_url}/releases"));
         });
-        cx.add_action(AutoUpdateIndicator::dismiss_error_message);
         cx.add_action(UpdateNotification::dismiss);
     }
 }
@@ -120,7 +111,7 @@ pub fn notify_of_any_new_update(
 }
 
 impl AutoUpdater {
-    fn get(cx: &mut MutableAppContext) -> Option<ModelHandle<Self>> {
+    pub fn get(cx: &mut MutableAppContext) -> Option<ModelHandle<Self>> {
         cx.default_global::<Option<ModelHandle<Self>>>().clone()
     }
 
@@ -170,6 +161,15 @@ impl AutoUpdater {
         }));
     }
 
+    pub fn status(&self) -> AutoUpdateStatus {
+        self.status
+    }
+
+    pub fn dismiss_error(&mut self, cx: &mut ModelContext<Self>) {
+        self.status = AutoUpdateStatus::Idle;
+        cx.notify();
+    }
+
     async fn update(this: ModelHandle<Self>, mut cx: AsyncAppContext) -> Result<()> {
         let (client, server_url, current_version) = this.read_with(&cx, |this, _| {
             (
@@ -299,79 +299,3 @@ impl AutoUpdater {
         })
     }
 }
-
-impl Entity for AutoUpdateIndicator {
-    type Event = ();
-}
-
-impl View for AutoUpdateIndicator {
-    fn ui_name() -> &'static str {
-        "AutoUpdateIndicator"
-    }
-
-    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
-        if let Some(updater) = &self.updater {
-            let theme = &cx.global::<Settings>().theme.workspace.status_bar;
-            match &updater.read(cx).status {
-                AutoUpdateStatus::Checking => Text::new(
-                    "Checking for updates…".to_string(),
-                    theme.auto_update_progress_message.clone(),
-                )
-                .boxed(),
-                AutoUpdateStatus::Downloading => Text::new(
-                    "Downloading update…".to_string(),
-                    theme.auto_update_progress_message.clone(),
-                )
-                .boxed(),
-                AutoUpdateStatus::Installing => Text::new(
-                    "Installing update…".to_string(),
-                    theme.auto_update_progress_message.clone(),
-                )
-                .boxed(),
-                AutoUpdateStatus::Updated => Text::new(
-                    "Restart to update Zed".to_string(),
-                    theme.auto_update_done_message.clone(),
-                )
-                .boxed(),
-                AutoUpdateStatus::Errored => {
-                    MouseEventHandler::new::<Self, _, _>(0, cx, |_, cx| {
-                        let theme = &cx.global::<Settings>().theme.workspace.status_bar;
-                        Text::new(
-                            "Auto update failed".to_string(),
-                            theme.auto_update_done_message.clone(),
-                        )
-                        .boxed()
-                    })
-                    .on_click(|_, _, cx| cx.dispatch_action(DismissErrorMessage))
-                    .boxed()
-                }
-                AutoUpdateStatus::Idle => Empty::new().boxed(),
-            }
-        } else {
-            Empty::new().boxed()
-        }
-    }
-}
-
-impl StatusItemView for AutoUpdateIndicator {
-    fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
-}
-
-impl AutoUpdateIndicator {
-    pub fn new(cx: &mut ViewContext<Self>) -> Self {
-        let updater = AutoUpdater::get(cx);
-        if let Some(updater) = &updater {
-            cx.observe(updater, |_, _, cx| cx.notify()).detach();
-        }
-        Self { updater }
-    }
-
-    fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
-        if let Some(updater) = &self.updater {
-            updater.update(cx, |updater, cx| {
-                updater.status = AutoUpdateStatus::Idle;
-                cx.notify();
-            });
-        }
-    }
-}

crates/client/src/client.rs πŸ”—

@@ -28,10 +28,7 @@ use std::{
     convert::TryFrom,
     fmt::Write as _,
     future::Future,
-    sync::{
-        atomic::{AtomicUsize, Ordering},
-        Arc, Weak,
-    },
+    sync::{Arc, Weak},
     time::{Duration, Instant},
 };
 use thiserror::Error;
@@ -232,12 +229,8 @@ impl Drop for Subscription {
 
 impl Client {
     pub fn new(http: Arc<dyn HttpClient>) -> Arc<Self> {
-        lazy_static! {
-            static ref NEXT_CLIENT_ID: AtomicUsize = AtomicUsize::default();
-        }
-
         Arc::new(Self {
-            id: NEXT_CLIENT_ID.fetch_add(1, Ordering::SeqCst),
+            id: 0,
             peer: Peer::new(),
             http,
             state: Default::default(),
@@ -257,6 +250,12 @@ impl Client {
         self.http.clone()
     }
 
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn set_id(&mut self, id: usize) -> &Self {
+        self.id = id;
+        self
+    }
+
     #[cfg(any(test, feature = "test-support"))]
     pub fn tear_down(&self) {
         let mut state = self.state.write();

crates/collab/src/db.rs πŸ”—

@@ -2282,7 +2282,7 @@ pub mod tests {
             Self {
                 background,
                 users: Default::default(),
-                next_user_id: Mutex::new(1),
+                next_user_id: Mutex::new(0),
                 projects: Default::default(),
                 worktree_extensions: Default::default(),
                 next_project_id: Mutex::new(1),
@@ -2346,6 +2346,7 @@ pub mod tests {
         }
 
         async fn get_user_by_id(&self, id: UserId) -> Result<Option<User>> {
+            self.background.simulate_random_delay().await;
             Ok(self.get_users_by_ids(vec![id]).await?.into_iter().next())
         }
 
@@ -2360,6 +2361,7 @@ pub mod tests {
         }
 
         async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>> {
+            self.background.simulate_random_delay().await;
             Ok(self
                 .users
                 .lock()
@@ -2393,6 +2395,7 @@ pub mod tests {
         }
 
         async fn get_invite_code_for_user(&self, _id: UserId) -> Result<Option<(String, u32)>> {
+            self.background.simulate_random_delay().await;
             Ok(None)
         }
 
@@ -2430,6 +2433,7 @@ pub mod tests {
         }
 
         async fn unregister_project(&self, project_id: ProjectId) -> Result<()> {
+            self.background.simulate_random_delay().await;
             self.projects
                 .lock()
                 .get_mut(&project_id)
@@ -2543,6 +2547,7 @@ pub mod tests {
             requester_id: UserId,
             responder_id: UserId,
         ) -> Result<()> {
+            self.background.simulate_random_delay().await;
             let mut contacts = self.contacts.lock();
             for contact in contacts.iter_mut() {
                 if contact.requester_id == requester_id && contact.responder_id == responder_id {
@@ -2572,6 +2577,7 @@ pub mod tests {
         }
 
         async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<()> {
+            self.background.simulate_random_delay().await;
             self.contacts.lock().retain(|contact| {
                 !(contact.requester_id == requester_id && contact.responder_id == responder_id)
             });
@@ -2583,6 +2589,7 @@ pub mod tests {
             user_id: UserId,
             contact_user_id: UserId,
         ) -> Result<()> {
+            self.background.simulate_random_delay().await;
             let mut contacts = self.contacts.lock();
             for contact in contacts.iter_mut() {
                 if contact.requester_id == contact_user_id
@@ -2609,6 +2616,7 @@ pub mod tests {
             requester_id: UserId,
             accept: bool,
         ) -> Result<()> {
+            self.background.simulate_random_delay().await;
             let mut contacts = self.contacts.lock();
             for (ix, contact) in contacts.iter_mut().enumerate() {
                 if contact.requester_id == requester_id && contact.responder_id == responder_id {
@@ -2804,6 +2812,7 @@ pub mod tests {
             count: usize,
             before_id: Option<MessageId>,
         ) -> Result<Vec<ChannelMessage>> {
+            self.background.simulate_random_delay().await;
             let mut messages = self
                 .channel_messages
                 .lock()

crates/collab/src/integration_tests.rs πŸ”—

@@ -50,7 +50,6 @@ use std::{
     time::Duration,
 };
 use theme::ThemeRegistry;
-use tokio::sync::RwLockReadGuard;
 use workspace::{Item, SplitDirection, ToggleFollow, Workspace};
 
 #[ctor::ctor]
@@ -596,7 +595,7 @@ async fn test_offline_projects(
     deterministic.run_until_parked();
     assert!(server
         .store
-        .read()
+        .lock()
         .await
         .project_metadata_for_user(user_a)
         .is_empty());
@@ -630,7 +629,7 @@ async fn test_offline_projects(
     cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
     assert!(server
         .store
-        .read()
+        .lock()
         .await
         .project_metadata_for_user(user_a)
         .is_empty());
@@ -1491,7 +1490,7 @@ async fn test_collaborating_with_diagnostics(
     // Wait for server to see the diagnostics update.
     deterministic.run_until_parked();
     {
-        let store = server.store.read().await;
+        let store = server.store.lock().await;
         let project = store.project(ProjectId::from_proto(project_id)).unwrap();
         let worktree = project.worktrees.get(&worktree_id.to_proto()).unwrap();
         assert!(!worktree.diagnostic_summaries.is_empty());
@@ -1517,6 +1516,7 @@ async fn test_collaborating_with_diagnostics(
 
     // Join project as client C and observe the diagnostics.
     let project_c = client_c.build_remote_project(&project_a, cx_a, cx_c).await;
+    deterministic.run_until_parked();
     project_c.read_with(cx_c, |project, cx| {
         assert_eq!(
             project.diagnostic_summaries(cx).collect::<Vec<_>>(),
@@ -3216,7 +3216,7 @@ async fn test_basic_chat(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
 
     assert_eq!(
         server
-            .state()
+            .store()
             .await
             .channel(channel_id)
             .unwrap()
@@ -4470,8 +4470,16 @@ async fn test_random_collaboration(
     let mut server = TestServer::start(cx.foreground(), cx.background()).await;
     let db = server.app_state.db.clone();
     let host_user_id = db.create_user("host", None, false).await.unwrap();
-    for username in ["guest-1", "guest-2", "guest-3", "guest-4"] {
+    let mut available_guests = vec![
+        "guest-1".to_string(),
+        "guest-2".to_string(),
+        "guest-3".to_string(),
+        "guest-4".to_string(),
+    ];
+
+    for username in &available_guests {
         let guest_user_id = db.create_user(username, None, false).await.unwrap();
+        assert_eq!(*username, format!("guest-{}", guest_user_id));
         server
             .app_state
             .db
@@ -4665,12 +4673,7 @@ async fn test_random_collaboration(
     } else {
         max_operations
     };
-    let mut available_guests = vec![
-        "guest-1".to_string(),
-        "guest-2".to_string(),
-        "guest-3".to_string(),
-        "guest-4".to_string(),
-    ];
+
     let mut operations = 0;
     while operations < max_operations {
         if operations == disconnect_host_at {
@@ -4701,7 +4704,7 @@ async fn test_random_collaboration(
                     .unwrap();
                 let contacts = server
                     .store
-                    .read()
+                    .lock()
                     .await
                     .build_initial_contacts_update(contacts)
                     .contacts;
@@ -4773,6 +4776,7 @@ async fn test_random_collaboration(
                 server.disconnect_client(removed_guest_id);
                 deterministic.advance_clock(RECEIVE_TIMEOUT);
                 deterministic.start_waiting();
+                log::info!("Waiting for guest {} to exit...", removed_guest_id);
                 let (guest, guest_project, mut guest_cx, guest_err) = guest.await;
                 deterministic.finish_waiting();
                 server.allow_connections();
@@ -4785,7 +4789,7 @@ async fn test_random_collaboration(
                     let contacts = server.app_state.db.get_contacts(*user_id).await.unwrap();
                     let contacts = server
                         .store
-                        .read()
+                        .lock()
                         .await
                         .build_initial_contacts_update(contacts)
                         .contacts;
@@ -4989,6 +4993,7 @@ impl TestServer {
 
         Arc::get_mut(&mut client)
             .unwrap()
+            .set_id(user_id.0 as usize)
             .override_authenticate(move |cx| {
                 cx.spawn(|_| async move {
                     let access_token = "the-token".to_string();
@@ -5116,10 +5121,6 @@ impl TestServer {
         })
     }
 
-    async fn state<'a>(&'a self) -> RwLockReadGuard<'a, Store> {
-        self.server.store.read().await
-    }
-
     async fn condition<F>(&mut self, mut predicate: F)
     where
         F: FnMut(&Store) -> bool,
@@ -5128,7 +5129,7 @@ impl TestServer {
             self.foreground.parking_forbidden(),
             "you must call forbid_parking to use server conditions so we don't block indefinitely"
         );
-        while !(predicate)(&*self.server.store.read().await) {
+        while !(predicate)(&*self.server.store.lock().await) {
             self.foreground.start_waiting();
             self.notifications.next().await;
             self.foreground.finish_waiting();

crates/collab/src/rpc.rs πŸ”—

@@ -51,7 +51,7 @@ use std::{
 };
 use time::OffsetDateTime;
 use tokio::{
-    sync::{RwLock, RwLockReadGuard, RwLockWriteGuard},
+    sync::{Mutex, MutexGuard},
     time::Sleep,
 };
 use tower::ServiceBuilder;
@@ -97,7 +97,7 @@ impl<R: RequestMessage> Response<R> {
 
 pub struct Server {
     peer: Arc<Peer>,
-    pub(crate) store: RwLock<Store>,
+    pub(crate) store: Mutex<Store>,
     app_state: Arc<AppState>,
     handlers: HashMap<TypeId, MessageHandler>,
     notifications: Option<mpsc::UnboundedSender<()>>,
@@ -115,13 +115,8 @@ pub struct RealExecutor;
 const MESSAGE_COUNT_PER_PAGE: usize = 100;
 const MAX_MESSAGE_LEN: usize = 1024;
 
-struct StoreReadGuard<'a> {
-    guard: RwLockReadGuard<'a, Store>,
-    _not_send: PhantomData<Rc<()>>,
-}
-
-struct StoreWriteGuard<'a> {
-    guard: RwLockWriteGuard<'a, Store>,
+pub(crate) struct StoreGuard<'a> {
+    guard: MutexGuard<'a, Store>,
     _not_send: PhantomData<Rc<()>>,
 }
 
@@ -129,7 +124,7 @@ struct StoreWriteGuard<'a> {
 pub struct ServerSnapshot<'a> {
     peer: &'a Peer,
     #[serde(serialize_with = "serialize_deref")]
-    store: RwLockReadGuard<'a, Store>,
+    store: StoreGuard<'a>,
 }
 
 pub fn serialize_deref<S, T, U>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
@@ -385,7 +380,7 @@ impl Server {
             ).await?;
 
             {
-                let mut store = this.store_mut().await;
+                let mut store = this.store().await;
                 store.add_connection(connection_id, user_id, user.admin);
                 this.peer.send(connection_id, store.build_initial_contacts_update(contacts))?;
 
@@ -472,7 +467,7 @@ impl Server {
         let mut projects_to_unregister = Vec::new();
         let removed_user_id;
         {
-            let mut store = self.store_mut().await;
+            let mut store = self.store().await;
             let removed_connection = store.remove_connection(connection_id)?;
 
             for (project_id, project) in removed_connection.hosted_projects {
@@ -606,7 +601,7 @@ impl Server {
             .await
             .user_id_for_connection(request.sender_id)?;
         let project_id = self.app_state.db.register_project(user_id).await?;
-        self.store_mut().await.register_project(
+        self.store().await.register_project(
             request.sender_id,
             project_id,
             request.payload.online,
@@ -626,7 +621,7 @@ impl Server {
     ) -> Result<()> {
         let project_id = ProjectId::from_proto(request.payload.project_id);
         let (user_id, project) = {
-            let mut state = self.store_mut().await;
+            let mut state = self.store().await;
             let project = state.unregister_project(project_id, request.sender_id)?;
             (state.user_id_for_connection(request.sender_id)?, project)
         };
@@ -728,7 +723,7 @@ impl Server {
             return Err(anyhow!("no such project"))?;
         }
 
-        self.store_mut().await.request_join_project(
+        self.store().await.request_join_project(
             guest_user_id,
             project_id,
             response.into_receipt(),
@@ -750,7 +745,7 @@ impl Server {
         let host_user_id;
 
         {
-            let mut state = self.store_mut().await;
+            let mut state = self.store().await;
             let project_id = ProjectId::from_proto(request.payload.project_id);
             let project = state.project(project_id)?;
             if project.host_connection_id != request.sender_id {
@@ -794,20 +789,10 @@ impl Server {
             let worktrees = project
                 .worktrees
                 .iter()
-                .filter_map(|(id, shared_worktree)| {
-                    let worktree = project.worktrees.get(&id)?;
-                    Some(proto::Worktree {
-                        id: *id,
-                        root_name: worktree.root_name.clone(),
-                        entries: shared_worktree.entries.values().cloned().collect(),
-                        diagnostic_summaries: shared_worktree
-                            .diagnostic_summaries
-                            .values()
-                            .cloned()
-                            .collect(),
-                        visible: worktree.visible,
-                        scan_id: shared_worktree.scan_id,
-                    })
+                .map(|(id, worktree)| proto::WorktreeMetadata {
+                    id: *id,
+                    root_name: worktree.root_name.clone(),
+                    visible: worktree.visible,
                 })
                 .collect::<Vec<_>>();
 
@@ -843,14 +828,15 @@ impl Server {
                 }
             }
 
-            for (receipt, replica_id) in receipts_with_replica_ids {
+            // First, we send the metadata associated with each worktree.
+            for (receipt, replica_id) in &receipts_with_replica_ids {
                 self.peer.respond(
-                    receipt,
+                    receipt.clone(),
                     proto::JoinProjectResponse {
                         variant: Some(proto::join_project_response::Variant::Accept(
                             proto::join_project_response::Accept {
                                 worktrees: worktrees.clone(),
-                                replica_id: replica_id as u32,
+                                replica_id: *replica_id as u32,
                                 collaborators: collaborators.clone(),
                                 language_servers: project.language_servers.clone(),
                             },
@@ -858,6 +844,43 @@ impl Server {
                     },
                 )?;
             }
+
+            for (worktree_id, worktree) in &project.worktrees {
+                #[cfg(any(test, feature = "test-support"))]
+                const MAX_CHUNK_SIZE: usize = 2;
+                #[cfg(not(any(test, feature = "test-support")))]
+                const MAX_CHUNK_SIZE: usize = 256;
+
+                // Stream this worktree's entries.
+                let message = proto::UpdateWorktree {
+                    project_id: project_id.to_proto(),
+                    worktree_id: *worktree_id,
+                    root_name: worktree.root_name.clone(),
+                    updated_entries: worktree.entries.values().cloned().collect(),
+                    removed_entries: Default::default(),
+                    scan_id: worktree.scan_id,
+                    is_last_update: worktree.is_complete,
+                };
+                for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
+                    for (receipt, _) in &receipts_with_replica_ids {
+                        self.peer.send(receipt.sender_id, update.clone())?;
+                    }
+                }
+
+                // Stream this worktree's diagnostics.
+                for summary in worktree.diagnostic_summaries.values() {
+                    for (receipt, _) in &receipts_with_replica_ids {
+                        self.peer.send(
+                            receipt.sender_id,
+                            proto::UpdateDiagnosticSummary {
+                                project_id: project_id.to_proto(),
+                                worktree_id: *worktree_id,
+                                summary: Some(summary.clone()),
+                            },
+                        )?;
+                    }
+                }
+            }
         }
 
         self.update_user_contacts(host_user_id).await?;
@@ -872,7 +895,7 @@ impl Server {
         let project_id = ProjectId::from_proto(request.payload.project_id);
         let project;
         {
-            let mut store = self.store_mut().await;
+            let mut store = self.store().await;
             project = store.leave_project(sender_id, project_id)?;
             tracing::info!(
                 %project_id,
@@ -923,7 +946,7 @@ impl Server {
         let project_id = ProjectId::from_proto(request.payload.project_id);
         let user_id;
         {
-            let mut state = self.store_mut().await;
+            let mut state = self.store().await;
             user_id = state.user_id_for_connection(request.sender_id)?;
             let guest_connection_ids = state
                 .read_project(project_id, request.sender_id)?
@@ -983,7 +1006,7 @@ impl Server {
         self: Arc<Server>,
         request: TypedEnvelope<proto::RegisterProjectActivity>,
     ) -> Result<()> {
-        self.store_mut().await.register_project_activity(
+        self.store().await.register_project_activity(
             ProjectId::from_proto(request.payload.project_id),
             request.sender_id,
         )?;
@@ -998,7 +1021,7 @@ impl Server {
         let project_id = ProjectId::from_proto(request.payload.project_id);
         let worktree_id = request.payload.worktree_id;
         let (connection_ids, metadata_changed) = {
-            let mut store = self.store_mut().await;
+            let mut store = self.store().await;
             let (connection_ids, metadata_changed) = store.update_worktree(
                 request.sender_id,
                 project_id,
@@ -1007,6 +1030,7 @@ impl Server {
                 &request.payload.removed_entries,
                 &request.payload.updated_entries,
                 request.payload.scan_id,
+                request.payload.is_last_update,
             )?;
             (connection_ids, metadata_changed)
         };
@@ -1054,7 +1078,7 @@ impl Server {
             .summary
             .clone()
             .ok_or_else(|| anyhow!("invalid summary"))?;
-        let receiver_ids = self.store_mut().await.update_diagnostic_summary(
+        let receiver_ids = self.store().await.update_diagnostic_summary(
             ProjectId::from_proto(request.payload.project_id),
             request.payload.worktree_id,
             request.sender_id,
@@ -1072,7 +1096,7 @@ impl Server {
         self: Arc<Server>,
         request: TypedEnvelope<proto::StartLanguageServer>,
     ) -> Result<()> {
-        let receiver_ids = self.store_mut().await.start_language_server(
+        let receiver_ids = self.store().await.start_language_server(
             ProjectId::from_proto(request.payload.project_id),
             request.sender_id,
             request
@@ -1111,20 +1135,23 @@ impl Server {
     where
         T: EntityMessage + RequestMessage,
     {
+        let project_id = ProjectId::from_proto(request.payload.remote_entity_id());
         let host_connection_id = self
             .store()
             .await
-            .read_project(
-                ProjectId::from_proto(request.payload.remote_entity_id()),
-                request.sender_id,
-            )?
+            .read_project(project_id, request.sender_id)?
             .host_connection_id;
+        let payload = self
+            .peer
+            .forward_request(request.sender_id, host_connection_id, request.payload)
+            .await?;
 
-        response.send(
-            self.peer
-                .forward_request(request.sender_id, host_connection_id, request.payload)
-                .await?,
-        )?;
+        // Ensure project still exists by the time we get the response from the host.
+        self.store()
+            .await
+            .read_project(project_id, request.sender_id)?;
+
+        response.send(payload)?;
         Ok(())
     }
 
@@ -1165,7 +1192,7 @@ impl Server {
     ) -> Result<()> {
         let project_id = ProjectId::from_proto(request.payload.project_id);
         let receiver_ids = {
-            let mut store = self.store_mut().await;
+            let mut store = self.store().await;
             store.register_project_activity(project_id, request.sender_id)?;
             store.project_connection_ids(project_id, request.sender_id)?
         };
@@ -1232,7 +1259,7 @@ impl Server {
         let leader_id = ConnectionId(request.payload.leader_id);
         let follower_id = request.sender_id;
         {
-            let mut store = self.store_mut().await;
+            let mut store = self.store().await;
             if !store
                 .project_connection_ids(project_id, follower_id)?
                 .contains(&leader_id)
@@ -1257,7 +1284,7 @@ impl Server {
     async fn unfollow(self: Arc<Self>, request: TypedEnvelope<proto::Unfollow>) -> Result<()> {
         let project_id = ProjectId::from_proto(request.payload.project_id);
         let leader_id = ConnectionId(request.payload.leader_id);
-        let mut store = self.store_mut().await;
+        let mut store = self.store().await;
         if !store
             .project_connection_ids(project_id, request.sender_id)?
             .contains(&leader_id)
@@ -1275,7 +1302,7 @@ impl Server {
         request: TypedEnvelope<proto::UpdateFollowers>,
     ) -> Result<()> {
         let project_id = ProjectId::from_proto(request.payload.project_id);
-        let mut store = self.store_mut().await;
+        let mut store = self.store().await;
         store.register_project_activity(project_id, request.sender_id)?;
         let connection_ids = store.project_connection_ids(project_id, request.sender_id)?;
         let leader_id = request
@@ -1533,7 +1560,7 @@ impl Server {
             Err(anyhow!("access denied"))?;
         }
 
-        self.store_mut()
+        self.store()
             .await
             .join_channel(request.sender_id, channel_id);
         let messages = self
@@ -1575,7 +1602,7 @@ impl Server {
             Err(anyhow!("access denied"))?;
         }
 
-        self.store_mut()
+        self.store()
             .await
             .leave_channel(request.sender_id, channel_id);
 
@@ -1683,25 +1710,13 @@ impl Server {
         Ok(())
     }
 
-    async fn store<'a>(self: &'a Arc<Self>) -> StoreReadGuard<'a> {
-        #[cfg(test)]
-        tokio::task::yield_now().await;
-        let guard = self.store.read().await;
-        #[cfg(test)]
-        tokio::task::yield_now().await;
-        StoreReadGuard {
-            guard,
-            _not_send: PhantomData,
-        }
-    }
-
-    async fn store_mut<'a>(self: &'a Arc<Self>) -> StoreWriteGuard<'a> {
+    pub(crate) async fn store<'a>(&'a self) -> StoreGuard<'a> {
         #[cfg(test)]
         tokio::task::yield_now().await;
-        let guard = self.store.write().await;
+        let guard = self.store.lock().await;
         #[cfg(test)]
         tokio::task::yield_now().await;
-        StoreWriteGuard {
+        StoreGuard {
             guard,
             _not_send: PhantomData,
         }
@@ -1709,21 +1724,13 @@ impl Server {
 
     pub async fn snapshot<'a>(self: &'a Arc<Self>) -> ServerSnapshot<'a> {
         ServerSnapshot {
-            store: self.store.read().await,
+            store: self.store().await,
             peer: &self.peer,
         }
     }
 }
 
-impl<'a> Deref for StoreReadGuard<'a> {
-    type Target = Store;
-
-    fn deref(&self) -> &Self::Target {
-        &*self.guard
-    }
-}
-
-impl<'a> Deref for StoreWriteGuard<'a> {
+impl<'a> Deref for StoreGuard<'a> {
     type Target = Store;
 
     fn deref(&self) -> &Self::Target {
@@ -1731,13 +1738,13 @@ impl<'a> Deref for StoreWriteGuard<'a> {
     }
 }
 
-impl<'a> DerefMut for StoreWriteGuard<'a> {
+impl<'a> DerefMut for StoreGuard<'a> {
     fn deref_mut(&mut self) -> &mut Self::Target {
         &mut *self.guard
     }
 }
 
-impl<'a> Drop for StoreWriteGuard<'a> {
+impl<'a> Drop for StoreGuard<'a> {
     fn drop(&mut self) {
         #[cfg(test)]
         self.check_invariants();

crates/collab/src/rpc/store.rs πŸ”—

@@ -56,6 +56,7 @@ pub struct Worktree {
     #[serde(skip)]
     pub diagnostic_summaries: BTreeMap<PathBuf, proto::DiagnosticSummary>,
     pub scan_id: u64,
+    pub is_complete: bool,
 }
 
 #[derive(Default)]
@@ -646,6 +647,7 @@ impl Store {
         removed_entries: &[u64],
         updated_entries: &[proto::Entry],
         scan_id: u64,
+        is_last_update: bool,
     ) -> Result<(Vec<ConnectionId>, bool)> {
         let project = self.write_project(project_id, connection_id)?;
         if !project.online {
@@ -666,6 +668,7 @@ impl Store {
         }
 
         worktree.scan_id = scan_id;
+        worktree.is_complete = is_last_update;
         Ok((connection_ids, metadata_changed))
     }
 

crates/contacts_panel/src/contacts_panel.rs πŸ”—

@@ -8,6 +8,7 @@ use contact_notification::ContactNotification;
 use editor::{Cancel, Editor};
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
+    actions,
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
     impl_actions, impl_internal_actions,
@@ -24,6 +25,8 @@ use std::{ops::DerefMut, sync::Arc};
 use theme::IconButton;
 use workspace::{sidebar::SidebarItem, JoinProject, ToggleProjectOnline, Workspace};
 
+actions!(contacts_panel, [Toggle]);
+
 impl_actions!(
     contacts_panel,
     [RequestContact, RemoveContact, RespondToContactRequest]

crates/editor/src/element.rs πŸ”—

@@ -490,7 +490,7 @@ impl EditorElement {
                         }
 
                         let block_text =
-                            if matches!(self.cursor_shape, CursorShape::Block) {
+                            if let CursorShape::Block = self.cursor_shape {
                                 layout.snapshot.chars_at(cursor_position).next().and_then(
                                     |character| {
                                         let font_id =
@@ -520,7 +520,7 @@ impl EditorElement {
                         cursors.push(Cursor {
                             color: selection_style.cursor,
                             block_width,
-                            origin: content_origin + vec2f(x, y),
+                            origin: vec2f(x, y),
                             line_height: layout.line_height,
                             shape: self.cursor_shape,
                             block_text,
@@ -546,13 +546,12 @@ impl EditorElement {
 
         cx.scene.push_layer(Some(bounds));
         for cursor in cursors {
-            cursor.paint(cx);
+            cursor.paint(content_origin, cx);
         }
         cx.scene.pop_layer();
 
         if let Some((position, context_menu)) = layout.context_menu.as_mut() {
             cx.scene.push_stacking_context(None);
-
             let cursor_row_layout = &layout.line_layouts[(position.row() - start_row) as usize];
             let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left;
             let y = (position.row() + 1) as f32 * layout.line_height - scroll_top;
@@ -1630,7 +1629,7 @@ impl Default for CursorShape {
     }
 }
 
-struct Cursor {
+pub struct Cursor {
     origin: Vector2F,
     block_width: f32,
     line_height: f32,
@@ -1640,14 +1639,33 @@ struct Cursor {
 }
 
 impl Cursor {
-    fn paint(&self, cx: &mut PaintContext) {
+    pub fn new(
+        origin: Vector2F,
+        block_width: f32,
+        line_height: f32,
+        color: Color,
+        shape: CursorShape,
+        block_text: Option<Line>,
+    ) -> Cursor {
+        Cursor {
+            origin,
+            block_width,
+            line_height,
+            color,
+            shape,
+            block_text,
+        }
+    }
+
+    pub fn paint(&self, origin: Vector2F, cx: &mut PaintContext) {
         let bounds = match self.shape {
-            CursorShape::Bar => RectF::new(self.origin, vec2f(2.0, self.line_height)),
-            CursorShape::Block => {
-                RectF::new(self.origin, vec2f(self.block_width, self.line_height))
-            }
+            CursorShape::Bar => RectF::new(self.origin + origin, vec2f(2.0, self.line_height)),
+            CursorShape::Block => RectF::new(
+                self.origin + origin,
+                vec2f(self.block_width, self.line_height),
+            ),
             CursorShape::Underscore => RectF::new(
-                self.origin + Vector2F::new(0.0, self.line_height - 2.0),
+                self.origin + origin + Vector2F::new(0.0, self.line_height - 2.0),
                 vec2f(self.block_width, 2.0),
             ),
         };
@@ -1660,7 +1678,7 @@ impl Cursor {
         });
 
         if let Some(block_text) = &self.block_text {
-            block_text.paint(self.origin, bounds, self.line_height, cx);
+            block_text.paint(self.origin + origin, bounds, self.line_height, cx);
         }
     }
 }

crates/gpui/src/app.rs πŸ”—

@@ -1634,14 +1634,10 @@ impl MutableAppContext {
     pub fn default_global<T: 'static + Default>(&mut self) -> &T {
         let type_id = TypeId::of::<T>();
         self.update(|this| {
-            if !this.globals.contains_key(&type_id) {
+            if let Entry::Vacant(entry) = this.cx.globals.entry(type_id) {
+                entry.insert(Box::new(T::default()));
                 this.notify_global(type_id);
             }
-
-            this.cx
-                .globals
-                .entry(type_id)
-                .or_insert_with(|| Box::new(T::default()));
         });
         self.globals.get(&type_id).unwrap().downcast_ref().unwrap()
     }

crates/gpui/src/presenter.rs πŸ”—

@@ -703,6 +703,20 @@ impl<'a> EventContext<'a> {
         self.view_stack.last().copied()
     }
 
+    pub fn is_parent_view_focused(&self) -> bool {
+        if let Some(parent_view_id) = self.view_stack.last() {
+            self.app.focused_view_id(self.window_id) == Some(*parent_view_id)
+        } else {
+            false
+        }
+    }
+
+    pub fn focus_parent_view(&mut self) {
+        if let Some(parent_view_id) = self.view_stack.last() {
+            self.app.focus(self.window_id, Some(*parent_view_id))
+        }
+    }
+
     pub fn dispatch_any_action(&mut self, action: Box<dyn Action>) {
         self.dispatched_actions.push(DispatchDirective {
             dispatcher_view_id: self.view_stack.last().copied(),

crates/gpui/src/text_layout.rs πŸ”—

@@ -164,7 +164,7 @@ impl<'a> Hash for CacheKeyRef<'a> {
     }
 }
 
-#[derive(Default, Debug)]
+#[derive(Default, Debug, Clone)]
 pub struct Line {
     layout: Arc<LineLayout>,
     style_runs: SmallVec<[(u32, Color, Underline); 32]>,

crates/lsp/src/lsp.rs πŸ”—

@@ -11,7 +11,7 @@ use serde_json::{json, value::RawValue, Value};
 use smol::{
     channel,
     io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
-    process,
+    process::{self, Child},
 };
 use std::{
     future::Future,
@@ -44,6 +44,7 @@ pub struct LanguageServer {
     io_tasks: Mutex<Option<(Task<Option<()>>, Task<Option<()>>)>>,
     output_done_rx: Mutex<Option<barrier::Receiver>>,
     root_path: PathBuf,
+    _server: Option<Child>,
 }
 
 pub struct Subscription {
@@ -118,11 +119,20 @@ impl LanguageServer {
             .stdin(Stdio::piped())
             .stdout(Stdio::piped())
             .stderr(Stdio::inherit())
+            .kill_on_drop(true)
             .spawn()?;
+
         let stdin = server.stdin.take().unwrap();
-        let stdout = server.stdout.take().unwrap();
-        let mut server =
-            Self::new_internal(server_id, stdin, stdout, root_path, cx, |notification| {
+        let stout = server.stdout.take().unwrap();
+
+        let mut server = Self::new_internal(
+            server_id,
+            stdin,
+            stout,
+            Some(server),
+            root_path,
+            cx,
+            |notification| {
                 log::info!(
                     "unhandled notification {}:\n{}",
                     notification.method,
@@ -131,7 +141,8 @@ impl LanguageServer {
                     )
                     .unwrap()
                 );
-            });
+            },
+        );
         if let Some(name) = binary_path.file_name() {
             server.name = name.to_string_lossy().to_string();
         }
@@ -142,6 +153,7 @@ impl LanguageServer {
         server_id: usize,
         stdin: Stdin,
         stdout: Stdout,
+        server: Option<Child>,
         root_path: &Path,
         cx: AsyncAppContext,
         mut on_unhandled_notification: F,
@@ -242,6 +254,7 @@ impl LanguageServer {
             io_tasks: Mutex::new(Some((input_task, output_task))),
             output_done_rx: Mutex::new(Some(output_done_rx)),
             root_path: root_path.to_path_buf(),
+            _server: server,
         }
     }
 
@@ -480,6 +493,10 @@ impl LanguageServer {
         self.server_id
     }
 
+    pub fn root_path(&self) -> &PathBuf {
+        &self.root_path
+    }
+
     pub fn request<T: request::Request>(
         &self,
         params: T::Params,
@@ -608,6 +625,7 @@ impl LanguageServer {
             0,
             stdin_writer,
             stdout_reader,
+            None,
             Path::new("/"),
             cx.clone(),
             |_| {},
@@ -617,6 +635,7 @@ impl LanguageServer {
                 0,
                 stdout_writer,
                 stdin_reader,
+                None,
                 Path::new("/"),
                 cx.clone(),
                 move |msg| {

crates/project/src/lsp_command.rs πŸ”—

@@ -242,7 +242,7 @@ impl LspCommand for PerformRename {
                 .read_with(&cx, |project, cx| {
                     project
                         .language_server_for_buffer(buffer.read(cx), cx)
-                        .cloned()
+                        .map(|(adapter, server)| (adapter.clone(), server.clone()))
                 })
                 .ok_or_else(|| anyhow!("no language server found for buffer"))?;
             Project::deserialize_workspace_edit(
@@ -359,7 +359,7 @@ impl LspCommand for GetDefinition {
             .read_with(&cx, |project, cx| {
                 project
                     .language_server_for_buffer(buffer.read(cx), cx)
-                    .cloned()
+                    .map(|(adapter, server)| (adapter.clone(), server.clone()))
             })
             .ok_or_else(|| anyhow!("no language server found for buffer"))?;
 
@@ -388,8 +388,8 @@ impl LspCommand for GetDefinition {
                     .update(&mut cx, |this, cx| {
                         this.open_local_buffer_via_lsp(
                             target_uri,
-                            lsp_adapter.clone(),
-                            language_server.clone(),
+                            language_server.server_id(),
+                            lsp_adapter.name(),
                             cx,
                         )
                     })
@@ -599,7 +599,7 @@ impl LspCommand for GetReferences {
             .read_with(&cx, |project, cx| {
                 project
                     .language_server_for_buffer(buffer.read(cx), cx)
-                    .cloned()
+                    .map(|(adapter, server)| (adapter.clone(), server.clone()))
             })
             .ok_or_else(|| anyhow!("no language server found for buffer"))?;
 
@@ -609,8 +609,8 @@ impl LspCommand for GetReferences {
                     .update(&mut cx, |this, cx| {
                         this.open_local_buffer_via_lsp(
                             lsp_location.uri,
-                            lsp_adapter.clone(),
-                            language_server.clone(),
+                            language_server.server_id(),
+                            lsp_adapter.name(),
                             cx,
                         )
                     })

crates/project/src/project.rs πŸ”—

@@ -70,14 +70,26 @@ pub struct ProjectStore {
     projects: Vec<WeakModelHandle<Project>>,
 }
 
+// Language server state is stored across 3 collections:
+//     language_servers =>
+//         a mapping from unique server id to LanguageServerState which can either be a task for a
+//         server in the process of starting, or a running server with adapter and language server arcs
+//     language_server_ids => a mapping from worktreeId and server name to the unique server id
+//     language_server_statuses => a mapping from unique server id to the current server status
+//
+// Multiple worktrees can map to the same language server for example when you jump to the definition
+// of a file in the standard library. So language_server_ids is used to look up which server is active
+// for a given worktree and language server name
+//
+// When starting a language server, first the id map is checked to make sure a server isn't already available
+// for that worktree. If there is one, it finishes early. Otherwise, a new id is allocated and and
+// the Starting variant of LanguageServerState is stored in the language_servers map.
 pub struct Project {
     worktrees: Vec<WorktreeHandle>,
     active_entry: Option<ProjectEntryId>,
     languages: Arc<LanguageRegistry>,
-    language_servers:
-        HashMap<(WorktreeId, LanguageServerName), (Arc<dyn LspAdapter>, Arc<LanguageServer>)>,
-    started_language_servers:
-        HashMap<(WorktreeId, LanguageServerName), Task<Option<Arc<LanguageServer>>>>,
+    language_servers: HashMap<usize, LanguageServerState>,
+    language_server_ids: HashMap<(WorktreeId, LanguageServerName), usize>,
     language_server_statuses: BTreeMap<usize, LanguageServerStatus>,
     language_server_settings: Arc<Mutex<serde_json::Value>>,
     last_workspace_edits_by_language_server: HashMap<usize, ProjectTransaction>,
@@ -175,6 +187,14 @@ pub enum Event {
     ContactCancelledJoinRequest(Arc<User>),
 }
 
+pub enum LanguageServerState {
+    Starting(Task<Option<Arc<LanguageServer>>>),
+    Running {
+        adapter: Arc<dyn LspAdapter>,
+        server: Arc<LanguageServer>,
+    },
+}
+
 #[derive(Serialize)]
 pub struct LanguageServerStatus {
     pub name: String,
@@ -452,7 +472,7 @@ impl Project {
                 next_entry_id: Default::default(),
                 next_diagnostic_group_id: Default::default(),
                 language_servers: Default::default(),
-                started_language_servers: Default::default(),
+                language_server_ids: Default::default(),
                 language_server_statuses: Default::default(),
                 last_workspace_edits_by_language_server: Default::default(),
                 language_server_settings: Default::default(),
@@ -502,10 +522,9 @@ impl Project {
 
         let mut worktrees = Vec::new();
         for worktree in response.worktrees {
-            let (worktree, load_task) = cx
+            let worktree = cx
                 .update(|cx| Worktree::remote(remote_id, replica_id, worktree, client.clone(), cx));
             worktrees.push(worktree);
-            load_task.detach();
         }
 
         let (opened_buffer_tx, opened_buffer_rx) = watch::channel();
@@ -551,7 +570,7 @@ impl Project {
                     }),
                 },
                 language_servers: Default::default(),
-                started_language_servers: Default::default(),
+                language_server_ids: Default::default(),
                 language_server_settings: Default::default(),
                 language_server_statuses: response
                     .language_servers
@@ -706,7 +725,7 @@ impl Project {
             if let Some(lsp_adapter) = language.lsp_adapter() {
                 if !settings.enable_language_server(Some(&language.name())) {
                     let lsp_name = lsp_adapter.name();
-                    for (worktree_id, started_lsp_name) in self.started_language_servers.keys() {
+                    for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
                         if lsp_name == *started_lsp_name {
                             language_servers_to_stop.push((*worktree_id, started_lsp_name.clone()));
                         }
@@ -1135,7 +1154,7 @@ impl Project {
                     .ok_or_else(|| anyhow!("missing entry in response"))?;
                 worktree
                     .update(&mut cx, |worktree, cx| {
-                        worktree.as_remote().unwrap().insert_entry(
+                        worktree.as_remote_mut().unwrap().insert_entry(
                             entry,
                             response.worktree_scan_id as usize,
                             cx,
@@ -1178,7 +1197,7 @@ impl Project {
                     .ok_or_else(|| anyhow!("missing entry in response"))?;
                 worktree
                     .update(&mut cx, |worktree, cx| {
-                        worktree.as_remote().unwrap().insert_entry(
+                        worktree.as_remote_mut().unwrap().insert_entry(
                             entry,
                             response.worktree_scan_id as usize,
                             cx,
@@ -1221,7 +1240,7 @@ impl Project {
                     .ok_or_else(|| anyhow!("missing entry in response"))?;
                 worktree
                     .update(&mut cx, |worktree, cx| {
-                        worktree.as_remote().unwrap().insert_entry(
+                        worktree.as_remote_mut().unwrap().insert_entry(
                             entry,
                             response.worktree_scan_id as usize,
                             cx,
@@ -1254,7 +1273,7 @@ impl Project {
                     .await?;
                 worktree
                     .update(&mut cx, move |worktree, cx| {
-                        worktree.as_remote().unwrap().delete_entry(
+                        worktree.as_remote_mut().unwrap().delete_entry(
                             entry_id,
                             response.worktree_scan_id as usize,
                             cx,
@@ -1393,14 +1412,15 @@ impl Project {
             let client = self.client.clone();
             cx.foreground()
                 .spawn(async move {
-                    if let Some(share) = share {
-                        share.await?;
-                    }
                     client.send(proto::RespondToJoinProjectRequest {
                         requester_id,
                         project_id,
                         allow,
-                    })
+                    })?;
+                    if let Some(share) = share {
+                        share.await?;
+                    }
+                    anyhow::Ok(())
                 })
                 .detach_and_log_err(cx);
         }
@@ -1601,8 +1621,8 @@ impl Project {
     fn open_local_buffer_via_lsp(
         &mut self,
         abs_path: lsp::Url,
-        lsp_adapter: Arc<dyn LspAdapter>,
-        lsp_server: Arc<LanguageServer>,
+        language_server_id: usize,
+        language_server_name: LanguageServerName,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<ModelHandle<Buffer>>> {
         cx.spawn(|this, mut cx| async move {
@@ -1620,9 +1640,9 @@ impl Project {
                     })
                     .await?;
                 this.update(&mut cx, |this, cx| {
-                    this.language_servers.insert(
-                        (worktree.read(cx).id(), lsp_adapter.name()),
-                        (lsp_adapter, lsp_server),
+                    this.language_server_ids.insert(
+                        (worktree.read(cx).id(), language_server_name),
+                        language_server_id,
                     );
                 });
                 (worktree, PathBuf::new())
@@ -1789,9 +1809,16 @@ impl Project {
                     if let Some(adapter) = language.lsp_adapter() {
                         language_id = adapter.id_for_language(language.name().as_ref());
                         language_server = self
-                            .language_servers
+                            .language_server_ids
                             .get(&(worktree_id, adapter.name()))
-                            .cloned();
+                            .and_then(|id| self.language_servers.get(&id))
+                            .and_then(|server_state| {
+                                if let LanguageServerState::Running { server, .. } = server_state {
+                                    Some(server.clone())
+                                } else {
+                                    None
+                                }
+                            });
                     }
                 }
 
@@ -1802,7 +1829,7 @@ impl Project {
                     }
                 }
 
-                if let Some((_, server)) = language_server {
+                if let Some(server) = language_server {
                     server
                         .notify::<lsp::notification::DidOpenTextDocument>(
                             lsp::DidOpenTextDocumentParams {
@@ -1879,9 +1906,9 @@ impl Project {
                 }
             }
             BufferEvent::Edited { .. } => {
-                let (_, language_server) = self
-                    .language_server_for_buffer(buffer.read(cx), cx)?
-                    .clone();
+                let language_server = self
+                    .language_server_for_buffer(buffer.read(cx), cx)
+                    .map(|(_, server)| server.clone())?;
                 let buffer = buffer.read(cx);
                 let file = File::from_dyn(buffer.file())?;
                 let abs_path = file.as_local()?.abs_path(cx);
@@ -1970,16 +1997,19 @@ impl Project {
     fn language_servers_for_worktree(
         &self,
         worktree_id: WorktreeId,
-    ) -> impl Iterator<Item = &(Arc<dyn LspAdapter>, Arc<LanguageServer>)> {
-        self.language_servers.iter().filter_map(
-            move |((language_server_worktree_id, _), server)| {
+    ) -> impl Iterator<Item = (&Arc<dyn LspAdapter>, &Arc<LanguageServer>)> {
+        self.language_server_ids
+            .iter()
+            .filter_map(move |((language_server_worktree_id, _), id)| {
                 if *language_server_worktree_id == worktree_id {
-                    Some(server)
-                } else {
-                    None
+                    if let Some(LanguageServerState::Running { adapter, server }) =
+                        self.language_servers.get(&id)
+                    {
+                        return Some((adapter, server));
+                    }
                 }
-            },
-        )
+                None
+            })
     }
 
     fn assign_language_to_buffer(
@@ -2023,7 +2053,8 @@ impl Project {
             return;
         };
         let key = (worktree_id, adapter.name());
-        self.started_language_servers
+
+        self.language_server_ids
             .entry(key.clone())
             .or_insert_with(|| {
                 let server_id = post_inc(&mut self.next_language_server_id);
@@ -2034,252 +2065,298 @@ impl Project {
                     self.client.http_client(),
                     cx,
                 );
-                cx.spawn_weak(|this, mut cx| async move {
-                    let language_server = language_server?.await.log_err()?;
-                    let language_server = language_server
-                        .initialize(adapter.initialization_options())
-                        .await
-                        .log_err()?;
-                    let this = this.upgrade(&cx)?;
-                    let disk_based_diagnostics_progress_token =
-                        adapter.disk_based_diagnostics_progress_token();
+                self.language_servers.insert(
+                    server_id,
+                    LanguageServerState::Starting(cx.spawn_weak(|this, mut cx| async move {
+                        let language_server = language_server?.await.log_err()?;
+                        let language_server = language_server
+                            .initialize(adapter.initialization_options())
+                            .await
+                            .log_err()?;
+                        let this = this.upgrade(&cx)?;
+                        let disk_based_diagnostics_progress_token =
+                            adapter.disk_based_diagnostics_progress_token();
 
-                    language_server
-                        .on_notification::<lsp::notification::PublishDiagnostics, _>({
-                            let this = this.downgrade();
-                            let adapter = adapter.clone();
-                            move |params, mut cx| {
-                                if let Some(this) = this.upgrade(&cx) {
-                                    this.update(&mut cx, |this, cx| {
-                                        this.on_lsp_diagnostics_published(
-                                            server_id, params, &adapter, cx,
-                                        );
-                                    });
+                        language_server
+                            .on_notification::<lsp::notification::PublishDiagnostics, _>({
+                                let this = this.downgrade();
+                                let adapter = adapter.clone();
+                                move |params, mut cx| {
+                                    if let Some(this) = this.upgrade(&cx) {
+                                        this.update(&mut cx, |this, cx| {
+                                            this.on_lsp_diagnostics_published(
+                                                server_id, params, &adapter, cx,
+                                            );
+                                        });
+                                    }
                                 }
-                            }
-                        })
-                        .detach();
+                            })
+                            .detach();
 
-                    language_server
-                        .on_request::<lsp::request::WorkspaceConfiguration, _, _>({
-                            let settings = this
-                                .read_with(&cx, |this, _| this.language_server_settings.clone());
-                            move |params, _| {
-                                let settings = settings.lock().clone();
-                                async move {
-                                    Ok(params
-                                        .items
-                                        .into_iter()
-                                        .map(|item| {
-                                            if let Some(section) = &item.section {
-                                                settings
-                                                    .get(section)
-                                                    .cloned()
-                                                    .unwrap_or(serde_json::Value::Null)
-                                            } else {
-                                                settings.clone()
-                                            }
-                                        })
-                                        .collect())
+                        language_server
+                            .on_request::<lsp::request::WorkspaceConfiguration, _, _>({
+                                let settings = this.read_with(&cx, |this, _| {
+                                    this.language_server_settings.clone()
+                                });
+                                move |params, _| {
+                                    let settings = settings.lock().clone();
+                                    async move {
+                                        Ok(params
+                                            .items
+                                            .into_iter()
+                                            .map(|item| {
+                                                if let Some(section) = &item.section {
+                                                    settings
+                                                        .get(section)
+                                                        .cloned()
+                                                        .unwrap_or(serde_json::Value::Null)
+                                                } else {
+                                                    settings.clone()
+                                                }
+                                            })
+                                            .collect())
+                                    }
                                 }
-                            }
-                        })
-                        .detach();
+                            })
+                            .detach();
 
-                    // Even though we don't have handling for these requests, respond to them to
-                    // avoid stalling any language server like `gopls` which waits for a response
-                    // to these requests when initializing.
-                    language_server
-                        .on_request::<lsp::request::WorkDoneProgressCreate, _, _>({
-                            let this = this.downgrade();
-                            move |params, mut cx| async move {
-                                if let Some(this) = this.upgrade(&cx) {
-                                    this.update(&mut cx, |this, _| {
-                                        if let Some(status) =
-                                            this.language_server_statuses.get_mut(&server_id)
-                                        {
-                                            if let lsp::NumberOrString::String(token) = params.token
+                        // Even though we don't have handling for these requests, respond to them to
+                        // avoid stalling any language server like `gopls` which waits for a response
+                        // to these requests when initializing.
+                        language_server
+                            .on_request::<lsp::request::WorkDoneProgressCreate, _, _>({
+                                let this = this.downgrade();
+                                move |params, mut cx| async move {
+                                    if let Some(this) = this.upgrade(&cx) {
+                                        this.update(&mut cx, |this, _| {
+                                            if let Some(status) =
+                                                this.language_server_statuses.get_mut(&server_id)
                                             {
-                                                status.progress_tokens.insert(token);
+                                                if let lsp::NumberOrString::String(token) =
+                                                    params.token
+                                                {
+                                                    status.progress_tokens.insert(token);
+                                                }
                                             }
-                                        }
-                                    });
+                                        });
+                                    }
+                                    Ok(())
                                 }
+                            })
+                            .detach();
+                        language_server
+                            .on_request::<lsp::request::RegisterCapability, _, _>(|_, _| async {
                                 Ok(())
-                            }
-                        })
-                        .detach();
-                    language_server
-                        .on_request::<lsp::request::RegisterCapability, _, _>(|_, _| async {
-                            Ok(())
-                        })
-                        .detach();
+                            })
+                            .detach();
 
-                    language_server
-                        .on_request::<lsp::request::ApplyWorkspaceEdit, _, _>({
-                            let this = this.downgrade();
-                            let adapter = adapter.clone();
-                            let language_server = language_server.clone();
-                            move |params, cx| {
-                                Self::on_lsp_workspace_edit(
-                                    this,
-                                    params,
-                                    server_id,
-                                    adapter.clone(),
-                                    language_server.clone(),
-                                    cx,
-                                )
-                            }
-                        })
-                        .detach();
+                        language_server
+                            .on_request::<lsp::request::ApplyWorkspaceEdit, _, _>({
+                                let this = this.downgrade();
+                                let adapter = adapter.clone();
+                                let language_server = language_server.clone();
+                                move |params, cx| {
+                                    Self::on_lsp_workspace_edit(
+                                        this,
+                                        params,
+                                        server_id,
+                                        adapter.clone(),
+                                        language_server.clone(),
+                                        cx,
+                                    )
+                                }
+                            })
+                            .detach();
 
-                    language_server
-                        .on_notification::<lsp::notification::Progress, _>({
-                            let this = this.downgrade();
-                            move |params, mut cx| {
-                                if let Some(this) = this.upgrade(&cx) {
-                                    this.update(&mut cx, |this, cx| {
-                                        this.on_lsp_progress(
-                                            params,
-                                            server_id,
-                                            disk_based_diagnostics_progress_token,
-                                            cx,
-                                        );
-                                    });
+                        language_server
+                            .on_notification::<lsp::notification::Progress, _>({
+                                let this = this.downgrade();
+                                move |params, mut cx| {
+                                    if let Some(this) = this.upgrade(&cx) {
+                                        this.update(&mut cx, |this, cx| {
+                                            this.on_lsp_progress(
+                                                params,
+                                                server_id,
+                                                disk_based_diagnostics_progress_token,
+                                                cx,
+                                            );
+                                        });
+                                    }
                                 }
+                            })
+                            .detach();
+
+                        this.update(&mut cx, |this, cx| {
+                            // If the language server for this key doesn't match the server id, don't store the
+                            // server. Which will cause it to be dropped, killing the process
+                            if this
+                                .language_server_ids
+                                .get(&key)
+                                .map(|id| id != &server_id)
+                                .unwrap_or(false)
+                            {
+                                return None;
                             }
-                        })
-                        .detach();
 
-                    this.update(&mut cx, |this, cx| {
-                        this.language_servers
-                            .insert(key.clone(), (adapter.clone(), language_server.clone()));
-                        this.language_server_statuses.insert(
-                            server_id,
-                            LanguageServerStatus {
-                                name: language_server.name().to_string(),
-                                pending_work: Default::default(),
-                                has_pending_diagnostic_updates: false,
-                                progress_tokens: Default::default(),
-                            },
-                        );
-                        language_server
-                            .notify::<lsp::notification::DidChangeConfiguration>(
-                                lsp::DidChangeConfigurationParams {
-                                    settings: this.language_server_settings.lock().clone(),
+                            // Update language_servers collection with Running variant of LanguageServerState
+                            // indicating that the server is up and running and ready
+                            this.language_servers.insert(
+                                server_id,
+                                LanguageServerState::Running {
+                                    adapter: adapter.clone(),
+                                    server: language_server.clone(),
                                 },
-                            )
-                            .ok();
+                            );
+                            this.language_server_statuses.insert(
+                                server_id,
+                                LanguageServerStatus {
+                                    name: language_server.name().to_string(),
+                                    pending_work: Default::default(),
+                                    has_pending_diagnostic_updates: false,
+                                    progress_tokens: Default::default(),
+                                },
+                            );
+                            language_server
+                                .notify::<lsp::notification::DidChangeConfiguration>(
+                                    lsp::DidChangeConfigurationParams {
+                                        settings: this.language_server_settings.lock().clone(),
+                                    },
+                                )
+                                .ok();
+
+                            if let Some(project_id) = this.shared_remote_id() {
+                                this.client
+                                    .send(proto::StartLanguageServer {
+                                        project_id,
+                                        server: Some(proto::LanguageServer {
+                                            id: server_id as u64,
+                                            name: language_server.name().to_string(),
+                                        }),
+                                    })
+                                    .log_err();
+                            }
 
-                        if let Some(project_id) = this.shared_remote_id() {
-                            this.client
-                                .send(proto::StartLanguageServer {
-                                    project_id,
-                                    server: Some(proto::LanguageServer {
-                                        id: server_id as u64,
-                                        name: language_server.name().to_string(),
-                                    }),
-                                })
-                                .log_err();
-                        }
+                            // Tell the language server about every open buffer in the worktree that matches the language.
+                            for buffer in this.opened_buffers.values() {
+                                if let Some(buffer_handle) = buffer.upgrade(cx) {
+                                    let buffer = buffer_handle.read(cx);
+                                    let file = if let Some(file) = File::from_dyn(buffer.file()) {
+                                        file
+                                    } else {
+                                        continue;
+                                    };
+                                    let language = if let Some(language) = buffer.language() {
+                                        language
+                                    } else {
+                                        continue;
+                                    };
+                                    if file.worktree.read(cx).id() != key.0
+                                        || language.lsp_adapter().map(|a| a.name())
+                                            != Some(key.1.clone())
+                                    {
+                                        continue;
+                                    }
 
-                        // Tell the language server about every open buffer in the worktree that matches the language.
-                        for buffer in this.opened_buffers.values() {
-                            if let Some(buffer_handle) = buffer.upgrade(cx) {
-                                let buffer = buffer_handle.read(cx);
-                                let file = if let Some(file) = File::from_dyn(buffer.file()) {
-                                    file
-                                } else {
-                                    continue;
-                                };
-                                let language = if let Some(language) = buffer.language() {
-                                    language
-                                } else {
-                                    continue;
-                                };
-                                if file.worktree.read(cx).id() != key.0
-                                    || language.lsp_adapter().map(|a| a.name())
-                                        != Some(key.1.clone())
-                                {
-                                    continue;
+                                    let file = file.as_local()?;
+                                    let versions = this
+                                        .buffer_snapshots
+                                        .entry(buffer.remote_id())
+                                        .or_insert_with(|| vec![(0, buffer.text_snapshot())]);
+                                    let (version, initial_snapshot) = versions.last().unwrap();
+                                    let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
+                                    let language_id =
+                                        adapter.id_for_language(language.name().as_ref());
+                                    language_server
+                                        .notify::<lsp::notification::DidOpenTextDocument>(
+                                            lsp::DidOpenTextDocumentParams {
+                                                text_document: lsp::TextDocumentItem::new(
+                                                    uri,
+                                                    language_id.unwrap_or_default(),
+                                                    *version,
+                                                    initial_snapshot.text(),
+                                                ),
+                                            },
+                                        )
+                                        .log_err()?;
+                                    buffer_handle.update(cx, |buffer, cx| {
+                                        buffer.set_completion_triggers(
+                                            language_server
+                                                .capabilities()
+                                                .completion_provider
+                                                .as_ref()
+                                                .and_then(|provider| {
+                                                    provider.trigger_characters.clone()
+                                                })
+                                                .unwrap_or(Vec::new()),
+                                            cx,
+                                        )
+                                    });
                                 }
-
-                                let file = file.as_local()?;
-                                let versions = this
-                                    .buffer_snapshots
-                                    .entry(buffer.remote_id())
-                                    .or_insert_with(|| vec![(0, buffer.text_snapshot())]);
-                                let (version, initial_snapshot) = versions.last().unwrap();
-                                let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
-                                let language_id = adapter.id_for_language(language.name().as_ref());
-                                language_server
-                                    .notify::<lsp::notification::DidOpenTextDocument>(
-                                        lsp::DidOpenTextDocumentParams {
-                                            text_document: lsp::TextDocumentItem::new(
-                                                uri,
-                                                language_id.unwrap_or_default(),
-                                                *version,
-                                                initial_snapshot.text(),
-                                            ),
-                                        },
-                                    )
-                                    .log_err()?;
-                                buffer_handle.update(cx, |buffer, cx| {
-                                    buffer.set_completion_triggers(
-                                        language_server
-                                            .capabilities()
-                                            .completion_provider
-                                            .as_ref()
-                                            .and_then(|provider| {
-                                                provider.trigger_characters.clone()
-                                            })
-                                            .unwrap_or(Vec::new()),
-                                        cx,
-                                    )
-                                });
                             }
-                        }
 
-                        cx.notify();
-                        Some(())
-                    });
+                            cx.notify();
+                            Some(language_server)
+                        })
+                    })),
+                );
 
-                    Some(language_server)
-                })
+                server_id
             });
     }
 
+    // Returns a list of all of the worktrees which no longer have a language server and the root path
+    // for the stopped server
     fn stop_language_server(
         &mut self,
         worktree_id: WorktreeId,
         adapter_name: LanguageServerName,
         cx: &mut ModelContext<Self>,
-    ) -> Task<()> {
+    ) -> Task<(Option<PathBuf>, Vec<WorktreeId>)> {
         let key = (worktree_id, adapter_name);
-        if let Some((_, language_server)) = self.language_servers.remove(&key) {
-            self.language_server_statuses
-                .remove(&language_server.server_id());
+        if let Some(server_id) = self.language_server_ids.remove(&key) {
+            // Remove other entries for this language server as well
+            let mut orphaned_worktrees = vec![worktree_id];
+            let other_keys = self.language_server_ids.keys().cloned().collect::<Vec<_>>();
+            for other_key in other_keys {
+                if self.language_server_ids.get(&other_key) == Some(&server_id) {
+                    self.language_server_ids.remove(&other_key);
+                    orphaned_worktrees.push(other_key.0);
+                }
+            }
+
+            self.language_server_statuses.remove(&server_id);
             cx.notify();
-        }
 
-        if let Some(started_language_server) = self.started_language_servers.remove(&key) {
+            let server_state = self.language_servers.remove(&server_id);
             cx.spawn_weak(|this, mut cx| async move {
-                if let Some(language_server) = started_language_server.await {
-                    if let Some(shutdown) = language_server.shutdown() {
-                        shutdown.await;
+                let mut root_path = None;
+
+                let server = match server_state {
+                    Some(LanguageServerState::Starting(started_language_server)) => {
+                        started_language_server.await
                     }
+                    Some(LanguageServerState::Running { server, .. }) => Some(server),
+                    None => None,
+                };
 
-                    if let Some(this) = this.upgrade(&cx) {
-                        this.update(&mut cx, |this, cx| {
-                            this.language_server_statuses
-                                .remove(&language_server.server_id());
-                            cx.notify();
-                        });
+                if let Some(server) = server {
+                    root_path = Some(server.root_path().clone());
+                    if let Some(shutdown) = server.shutdown() {
+                        shutdown.await;
                     }
                 }
+
+                if let Some(this) = this.upgrade(&cx) {
+                    this.update(&mut cx, |this, cx| {
+                        this.language_server_statuses.remove(&server_id);
+                        cx.notify();
+                    });
+                }
+
+                (root_path, orphaned_worktrees)
             })
         } else {
-            Task::ready(())
+            Task::ready((None, Vec::new()))
         }
     }
 
@@ -2310,7 +2387,7 @@ impl Project {
     fn restart_language_server(
         &mut self,
         worktree_id: WorktreeId,
-        worktree_path: Arc<Path>,
+        fallback_path: Arc<Path>,
         language: Arc<Language>,
         cx: &mut ModelContext<Self>,
     ) {
@@ -2320,12 +2397,33 @@ impl Project {
             return;
         };
 
-        let stop = self.stop_language_server(worktree_id, adapter.name(), cx);
+        let server_name = adapter.name();
+        let stop = self.stop_language_server(worktree_id, server_name.clone(), cx);
         cx.spawn_weak(|this, mut cx| async move {
-            stop.await;
+            let (original_root_path, orphaned_worktrees) = stop.await;
             if let Some(this) = this.upgrade(&cx) {
                 this.update(&mut cx, |this, cx| {
-                    this.start_language_server(worktree_id, worktree_path, language, cx);
+                    // Attempt to restart using original server path. Fallback to passed in
+                    // path if we could not retrieve the root path
+                    let root_path = original_root_path
+                        .map(|path_buf| Arc::from(path_buf.as_path()))
+                        .unwrap_or(fallback_path);
+
+                    this.start_language_server(worktree_id, root_path, language, cx);
+
+                    // Lookup new server id and set it for each of the orphaned worktrees
+                    if let Some(new_server_id) = this
+                        .language_server_ids
+                        .get(&(worktree_id, server_name.clone()))
+                        .cloned()
+                    {
+                        for orphaned_worktree in orphaned_worktrees {
+                            this.language_server_ids.insert(
+                                (orphaned_worktree, server_name.clone()),
+                                new_server_id.clone(),
+                            );
+                        }
+                    }
                 });
             }
         })
@@ -2561,14 +2659,16 @@ impl Project {
     }
 
     pub fn set_language_server_settings(&mut self, settings: serde_json::Value) {
-        for (_, server) in self.language_servers.values() {
-            server
-                .notify::<lsp::notification::DidChangeConfiguration>(
-                    lsp::DidChangeConfigurationParams {
-                        settings: settings.clone(),
-                    },
-                )
-                .ok();
+        for server_state in self.language_servers.values() {
+            if let LanguageServerState::Running { server, .. } = server_state {
+                server
+                    .notify::<lsp::notification::DidChangeConfiguration>(
+                        lsp::DidChangeConfigurationParams {
+                            settings: settings.clone(),
+                        },
+                    )
+                    .ok();
+            }
         }
         *self.language_server_settings.lock() = settings;
     }
@@ -3031,30 +3131,36 @@ impl Project {
     pub fn symbols(&self, query: &str, cx: &mut ModelContext<Self>) -> Task<Result<Vec<Symbol>>> {
         if self.is_local() {
             let mut requests = Vec::new();
-            for ((worktree_id, _), (lsp_adapter, language_server)) in self.language_servers.iter() {
+            for ((worktree_id, _), server_id) in self.language_server_ids.iter() {
                 let worktree_id = *worktree_id;
                 if let Some(worktree) = self
                     .worktree_for_id(worktree_id, cx)
                     .and_then(|worktree| worktree.read(cx).as_local())
                 {
-                    let lsp_adapter = lsp_adapter.clone();
-                    let worktree_abs_path = worktree.abs_path().clone();
-                    requests.push(
-                        language_server
-                            .request::<lsp::request::WorkspaceSymbol>(lsp::WorkspaceSymbolParams {
-                                query: query.to_string(),
-                                ..Default::default()
-                            })
-                            .log_err()
-                            .map(move |response| {
-                                (
-                                    lsp_adapter,
-                                    worktree_id,
-                                    worktree_abs_path,
-                                    response.unwrap_or_default(),
+                    if let Some(LanguageServerState::Running { adapter, server }) =
+                        self.language_servers.get(server_id)
+                    {
+                        let adapter = adapter.clone();
+                        let worktree_abs_path = worktree.abs_path().clone();
+                        requests.push(
+                            server
+                                .request::<lsp::request::WorkspaceSymbol>(
+                                    lsp::WorkspaceSymbolParams {
+                                        query: query.to_string(),
+                                        ..Default::default()
+                                    },
                                 )
-                            }),
-                    );
+                                .log_err()
+                                .map(move |response| {
+                                    (
+                                        adapter,
+                                        worktree_id,
+                                        worktree_abs_path,
+                                        response.unwrap_or_default(),
+                                    )
+                                }),
+                        );
+                    }
                 }
             }
 
@@ -3137,11 +3243,11 @@ impl Project {
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<ModelHandle<Buffer>>> {
         if self.is_local() {
-            let (lsp_adapter, language_server) = if let Some(server) = self.language_servers.get(&(
+            let language_server_id = if let Some(id) = self.language_server_ids.get(&(
                 symbol.source_worktree_id,
                 symbol.language_server_name.clone(),
             )) {
-                server.clone()
+                *id
             } else {
                 return Task::ready(Err(anyhow!(
                     "language server for worktree and language not found"
@@ -3164,7 +3270,12 @@ impl Project {
                 return Task::ready(Err(anyhow!("invalid symbol path")));
             };
 
-            self.open_local_buffer_via_lsp(symbol_uri, lsp_adapter, language_server, cx)
+            self.open_local_buffer_via_lsp(
+                symbol_uri,
+                language_server_id,
+                symbol.language_server_name.clone(),
+                cx,
+            )
         } else if let Some(project_id) = self.remote_id() {
             let request = self.client.request(proto::OpenBufferForSymbol {
                 project_id,
@@ -3215,8 +3326,8 @@ impl Project {
 
         if worktree.read(cx).as_local().is_some() {
             let buffer_abs_path = buffer_abs_path.unwrap();
-            let (_, lang_server) =
-                if let Some(server) = self.language_server_for_buffer(source_buffer, cx) {
+            let lang_server =
+                if let Some((_, server)) = self.language_server_for_buffer(source_buffer, cx) {
                     server.clone()
                 } else {
                     return Task::ready(Ok(Default::default()));
@@ -3373,7 +3484,7 @@ impl Project {
         let buffer_id = buffer.remote_id();
 
         if self.is_local() {
-            let (_, lang_server) = if let Some(server) = self.language_server_for_buffer(buffer, cx)
+            let lang_server = if let Some((_, server)) = self.language_server_for_buffer(buffer, cx)
             {
                 server.clone()
             } else {
@@ -3470,7 +3581,7 @@ impl Project {
 
         if worktree.read(cx).as_local().is_some() {
             let buffer_abs_path = buffer_abs_path.unwrap();
-            let (_, lang_server) = if let Some(server) = self.language_server_for_buffer(buffer, cx)
+            let lang_server = if let Some((_, server)) = self.language_server_for_buffer(buffer, cx)
             {
                 server.clone()
             } else {
@@ -3557,8 +3668,8 @@ impl Project {
         if self.is_local() {
             let buffer = buffer_handle.read(cx);
             let (lsp_adapter, lang_server) =
-                if let Some(server) = self.language_server_for_buffer(buffer, cx) {
-                    server.clone()
+                if let Some((adapter, server)) = self.language_server_for_buffer(buffer, cx) {
+                    (adapter.clone(), server.clone())
                 } else {
                     return Task::ready(Ok(Default::default()));
                 };
@@ -3594,8 +3705,8 @@ impl Project {
                         this,
                         edit,
                         push_to_history,
-                        lsp_adapter,
-                        lang_server,
+                        lsp_adapter.clone(),
+                        lang_server.clone(),
                         &mut cx,
                     )
                     .await
@@ -3724,8 +3835,8 @@ impl Project {
                         .update(cx, |this, cx| {
                             this.open_local_buffer_via_lsp(
                                 op.text_document.uri,
-                                lsp_adapter.clone(),
-                                language_server.clone(),
+                                language_server.server_id(),
+                                lsp_adapter.name(),
                                 cx,
                             )
                         })
@@ -4019,9 +4130,10 @@ impl Project {
         let buffer = buffer_handle.read(cx);
         if self.is_local() {
             let file = File::from_dyn(buffer.file()).and_then(File::as_local);
-            if let Some((file, (_, language_server))) =
-                file.zip(self.language_server_for_buffer(buffer, cx).cloned())
-            {
+            if let Some((file, language_server)) = file.zip(
+                self.language_server_for_buffer(buffer, cx)
+                    .map(|(_, server)| server.clone()),
+            ) {
                 let lsp_params = request.to_lsp(&file.abs_path(cx), cx);
                 return cx.spawn(|this, cx| async move {
                     if !request.check_capabilities(&language_server.capabilities()) {
@@ -4503,18 +4615,9 @@ impl Project {
                 {
                     this.worktrees.push(WorktreeHandle::Strong(old_worktree));
                 } else {
-                    let worktree = proto::Worktree {
-                        id: worktree.id,
-                        root_name: worktree.root_name,
-                        entries: Default::default(),
-                        diagnostic_summaries: Default::default(),
-                        visible: worktree.visible,
-                        scan_id: 0,
-                    };
-                    let (worktree, load_task) =
+                    let worktree =
                         Worktree::remote(remote_id, replica_id, worktree, client.clone(), cx);
                     this.add_worktree(&worktree, cx);
-                    load_task.detach();
                 }
             }
 
@@ -4538,8 +4641,8 @@ impl Project {
             if let Some(worktree) = this.worktree_for_id(worktree_id, cx) {
                 worktree.update(cx, |worktree, _| {
                     let worktree = worktree.as_remote_mut().unwrap();
-                    worktree.update_from_remote(envelope)
-                })?;
+                    worktree.update_from_remote(envelope.payload);
+                });
             }
             Ok(())
         })

crates/project/src/worktree.rs πŸ”—

@@ -7,9 +7,9 @@ use super::{
 };
 use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
 use anyhow::{anyhow, Context, Result};
-use client::{proto, Client, TypedEnvelope};
+use client::{proto, Client};
 use clock::ReplicaId;
-use collections::HashMap;
+use collections::{HashMap, VecDeque};
 use futures::{
     channel::{
         mpsc::{self, UnboundedSender},
@@ -40,11 +40,11 @@ use std::{
     ffi::{OsStr, OsString},
     fmt,
     future::Future,
-    mem,
     ops::{Deref, DerefMut},
     os::unix::prelude::{OsStrExt, OsStringExt},
     path::{Path, PathBuf},
     sync::{atomic::AtomicUsize, Arc},
+    task::Poll,
     time::{Duration, SystemTime},
 };
 use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap};
@@ -82,7 +82,7 @@ pub struct RemoteWorktree {
     project_id: u64,
     client: Arc<Client>,
     updates_tx: Option<UnboundedSender<proto::UpdateWorktree>>,
-    last_scan_id_rx: watch::Receiver<usize>,
+    snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>,
     replica_id: ReplicaId,
     diagnostic_summaries: TreeMap<PathKey, DiagnosticSummary>,
     visible: bool,
@@ -96,6 +96,7 @@ pub struct Snapshot {
     entries_by_path: SumTree<Entry>,
     entries_by_id: SumTree<PathEntry>,
     scan_id: usize,
+    is_complete: bool,
 }
 
 #[derive(Clone)]
@@ -125,13 +126,16 @@ impl DerefMut for LocalSnapshot {
 #[derive(Clone, Debug)]
 enum ScanState {
     Idle,
-    Scanning,
+    /// The worktree is performing its initial scan of the filesystem.
+    Initializing,
+    /// The worktree is updating in response to filesystem events.
+    Updating,
     Err(Arc<anyhow::Error>),
 }
 
 struct ShareState {
     project_id: u64,
-    snapshots_tx: Sender<LocalSnapshot>,
+    snapshots_tx: watch::Sender<LocalSnapshot>,
     _maintain_remote_snapshot: Option<Task<Option<()>>>,
 }
 
@@ -172,10 +176,10 @@ impl Worktree {
     pub fn remote(
         project_remote_id: u64,
         replica_id: ReplicaId,
-        worktree: proto::Worktree,
+        worktree: proto::WorktreeMetadata,
         client: Arc<Client>,
         cx: &mut MutableAppContext,
-    ) -> (ModelHandle<Self>, Task<()>) {
+    ) -> ModelHandle<Self> {
         let remote_id = worktree.id;
         let root_char_bag: CharBag = worktree
             .root_name
@@ -190,13 +194,13 @@ impl Worktree {
             root_char_bag,
             entries_by_path: Default::default(),
             entries_by_id: Default::default(),
-            scan_id: worktree.scan_id as usize,
+            scan_id: 0,
+            is_complete: false,
         };
 
         let (updates_tx, mut updates_rx) = mpsc::unbounded();
         let background_snapshot = Arc::new(Mutex::new(snapshot.clone()));
         let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel();
-        let (mut last_scan_id_tx, last_scan_id_rx) = watch::channel_with(worktree.scan_id as usize);
         let worktree_handle = cx.add_model(|_: &mut ModelContext<Worktree>| {
             Worktree::Remote(RemoteWorktree {
                 project_id: project_remote_id,
@@ -204,96 +208,50 @@ impl Worktree {
                 snapshot: snapshot.clone(),
                 background_snapshot: background_snapshot.clone(),
                 updates_tx: Some(updates_tx),
-                last_scan_id_rx,
+                snapshot_subscriptions: Default::default(),
                 client: client.clone(),
-                diagnostic_summaries: TreeMap::from_ordered_entries(
-                    worktree.diagnostic_summaries.into_iter().map(|summary| {
-                        (
-                            PathKey(PathBuf::from(summary.path).into()),
-                            DiagnosticSummary {
-                                language_server_id: summary.language_server_id as usize,
-                                error_count: summary.error_count as usize,
-                                warning_count: summary.warning_count as usize,
-                            },
-                        )
-                    }),
-                ),
+                diagnostic_summaries: Default::default(),
                 visible,
             })
         });
 
-        let deserialize_task = cx.spawn({
-            let worktree_handle = worktree_handle.clone();
-            |cx| async move {
-                let (entries_by_path, entries_by_id) = cx
-                    .background()
-                    .spawn(async move {
-                        let mut entries_by_path_edits = Vec::new();
-                        let mut entries_by_id_edits = Vec::new();
-                        for entry in worktree.entries {
-                            match Entry::try_from((&root_char_bag, entry)) {
-                                Ok(entry) => {
-                                    entries_by_id_edits.push(Edit::Insert(PathEntry {
-                                        id: entry.id,
-                                        path: entry.path.clone(),
-                                        is_ignored: entry.is_ignored,
-                                        scan_id: 0,
-                                    }));
-                                    entries_by_path_edits.push(Edit::Insert(entry));
-                                }
-                                Err(err) => log::warn!("error for remote worktree entry {:?}", err),
-                            }
-                        }
-
-                        let mut entries_by_path = SumTree::new();
-                        let mut entries_by_id = SumTree::new();
-                        entries_by_path.edit(entries_by_path_edits, &());
-                        entries_by_id.edit(entries_by_id_edits, &());
-
-                        (entries_by_path, entries_by_id)
-                    })
-                    .await;
-
-                {
-                    let mut snapshot = background_snapshot.lock();
-                    snapshot.entries_by_path = entries_by_path;
-                    snapshot.entries_by_id = entries_by_id;
+        cx.background()
+            .spawn(async move {
+                while let Some(update) = updates_rx.next().await {
+                    if let Err(error) = background_snapshot.lock().apply_remote_update(update) {
+                        log::error!("error applying worktree update: {}", error);
+                    }
                     snapshot_updated_tx.send(()).await.ok();
                 }
+            })
+            .detach();
 
-                cx.background()
-                    .spawn(async move {
-                        while let Some(update) = updates_rx.next().await {
-                            if let Err(error) =
-                                background_snapshot.lock().apply_remote_update(update)
-                            {
-                                log::error!("error applying worktree update: {}", error);
-                            }
-                            snapshot_updated_tx.send(()).await.ok();
-                        }
-                    })
-                    .detach();
-
-                cx.spawn(|mut cx| {
-                    let this = worktree_handle.downgrade();
-                    async move {
-                        while let Some(_) = snapshot_updated_rx.recv().await {
-                            if let Some(this) = this.upgrade(&cx) {
-                                this.update(&mut cx, |this, cx| {
-                                    this.poll_snapshot(cx);
-                                    let this = this.as_remote_mut().unwrap();
-                                    *last_scan_id_tx.borrow_mut() = this.snapshot.scan_id;
-                                });
-                            } else {
-                                break;
+        cx.spawn(|mut cx| {
+            let this = worktree_handle.downgrade();
+            async move {
+                while let Some(_) = snapshot_updated_rx.recv().await {
+                    if let Some(this) = this.upgrade(&cx) {
+                        this.update(&mut cx, |this, cx| {
+                            this.poll_snapshot(cx);
+                            let this = this.as_remote_mut().unwrap();
+                            while let Some((scan_id, _)) = this.snapshot_subscriptions.front() {
+                                if this.observed_snapshot(*scan_id) {
+                                    let (_, tx) = this.snapshot_subscriptions.pop_front().unwrap();
+                                    let _ = tx.send(());
+                                } else {
+                                    break;
+                                }
                             }
-                        }
+                        });
+                    } else {
+                        break;
                     }
-                })
-                .detach();
+                }
             }
-        });
-        (worktree_handle, deserialize_task)
+        })
+        .detach();
+
+        worktree_handle
     }
 
     pub fn as_local(&self) -> Option<&LocalWorktree> {
@@ -377,38 +335,9 @@ impl Worktree {
 
     fn poll_snapshot(&mut self, cx: &mut ModelContext<Self>) {
         match self {
-            Self::Local(worktree) => {
-                let is_fake_fs = worktree.fs.is_fake();
-                worktree.snapshot = worktree.background_snapshot.lock().clone();
-                if worktree.is_scanning() {
-                    if worktree.poll_task.is_none() {
-                        worktree.poll_task = Some(cx.spawn_weak(|this, mut cx| async move {
-                            if is_fake_fs {
-                                #[cfg(any(test, feature = "test-support"))]
-                                cx.background().simulate_random_delay().await;
-                            } else {
-                                smol::Timer::after(Duration::from_millis(100)).await;
-                            }
-                            if let Some(this) = this.upgrade(&cx) {
-                                this.update(&mut cx, |this, cx| {
-                                    this.as_local_mut().unwrap().poll_task = None;
-                                    this.poll_snapshot(cx);
-                                });
-                            }
-                        }));
-                    }
-                } else {
-                    worktree.poll_task.take();
-                    cx.emit(Event::UpdatedEntries);
-                }
-            }
-            Self::Remote(worktree) => {
-                worktree.snapshot = worktree.background_snapshot.lock().clone();
-                cx.emit(Event::UpdatedEntries);
-            }
+            Self::Local(worktree) => worktree.poll_snapshot(false, cx),
+            Self::Remote(worktree) => worktree.poll_snapshot(cx),
         };
-
-        cx.notify();
     }
 }
 
@@ -436,7 +365,8 @@ impl LocalWorktree {
             .context("failed to stat worktree path")?;
 
         let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
-        let (mut last_scan_state_tx, last_scan_state_rx) = watch::channel_with(ScanState::Scanning);
+        let (mut last_scan_state_tx, last_scan_state_rx) =
+            watch::channel_with(ScanState::Initializing);
         let tree = cx.add_model(move |cx: &mut ModelContext<Worktree>| {
             let mut snapshot = LocalSnapshot {
                 abs_path,
@@ -450,6 +380,7 @@ impl LocalWorktree {
                     entries_by_path: Default::default(),
                     entries_by_id: Default::default(),
                     scan_id: 0,
+                    is_complete: true,
                 },
                 extension_counts: Default::default(),
             };
@@ -481,11 +412,7 @@ impl LocalWorktree {
                 while let Some(scan_state) = scan_states_rx.next().await {
                     if let Some(this) = this.upgrade(&cx) {
                         last_scan_state_tx.blocking_send(scan_state).ok();
-                        this.update(&mut cx, |this, cx| {
-                            this.poll_snapshot(cx);
-                            this.as_local().unwrap().broadcast_snapshot()
-                        })
-                        .await;
+                        this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
                     } else {
                         break;
                     }
@@ -569,22 +496,53 @@ impl LocalWorktree {
         Ok(updated)
     }
 
+    fn poll_snapshot(&mut self, force: bool, cx: &mut ModelContext<Worktree>) {
+        self.poll_task.take();
+        match self.scan_state() {
+            ScanState::Idle => {
+                self.snapshot = self.background_snapshot.lock().clone();
+                if let Some(share) = self.share.as_mut() {
+                    *share.snapshots_tx.borrow_mut() = self.snapshot.clone();
+                }
+                cx.emit(Event::UpdatedEntries);
+            }
+            ScanState::Initializing => {
+                let is_fake_fs = self.fs.is_fake();
+                self.snapshot = self.background_snapshot.lock().clone();
+                self.poll_task = Some(cx.spawn_weak(|this, mut cx| async move {
+                    if is_fake_fs {
+                        #[cfg(any(test, feature = "test-support"))]
+                        cx.background().simulate_random_delay().await;
+                    } else {
+                        smol::Timer::after(Duration::from_millis(100)).await;
+                    }
+                    if let Some(this) = this.upgrade(&cx) {
+                        this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
+                    }
+                }));
+                cx.emit(Event::UpdatedEntries);
+            }
+            _ => {
+                if force {
+                    self.snapshot = self.background_snapshot.lock().clone();
+                }
+            }
+        }
+        cx.notify();
+    }
+
     pub fn scan_complete(&self) -> impl Future<Output = ()> {
         let mut scan_state_rx = self.last_scan_state_rx.clone();
         async move {
             let mut scan_state = Some(scan_state_rx.borrow().clone());
-            while let Some(ScanState::Scanning) = scan_state {
+            while let Some(ScanState::Initializing | ScanState::Updating) = scan_state {
                 scan_state = scan_state_rx.recv().await;
             }
         }
     }
 
-    fn is_scanning(&self) -> bool {
-        if let ScanState::Scanning = *self.last_scan_state_rx.borrow() {
-            true
-        } else {
-            false
-        }
+    fn scan_state(&self) -> ScanState {
+        self.last_scan_state_rx.borrow().clone()
     }
 
     pub fn snapshot(&self) -> LocalSnapshot {
@@ -614,7 +572,6 @@ impl LocalWorktree {
                         .refresh_entry(path, abs_path, None, cx)
                 })
                 .await?;
-            this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
             Ok((
                 File {
                     entry_id: Some(entry.id),
@@ -712,16 +669,14 @@ impl LocalWorktree {
 
         Some(cx.spawn(|this, mut cx| async move {
             delete.await?;
-            this.update(&mut cx, |this, _| {
+            this.update(&mut cx, |this, cx| {
                 let this = this.as_local_mut().unwrap();
-                let mut snapshot = this.background_snapshot.lock();
-                snapshot.delete_entry(entry_id);
+                {
+                    let mut snapshot = this.background_snapshot.lock();
+                    snapshot.delete_entry(entry_id);
+                }
+                this.poll_snapshot(true, cx);
             });
-            this.update(&mut cx, |this, cx| {
-                this.poll_snapshot(cx);
-                this.as_local().unwrap().broadcast_snapshot()
-            })
-            .await;
             Ok(())
         }))
     }
@@ -757,11 +712,6 @@ impl LocalWorktree {
                     )
                 })
                 .await?;
-            this.update(&mut cx, |this, cx| {
-                this.poll_snapshot(cx);
-                this.as_local().unwrap().broadcast_snapshot()
-            })
-            .await;
             Ok(entry)
         }))
     }
@@ -797,11 +747,6 @@ impl LocalWorktree {
                     )
                 })
                 .await?;
-            this.update(&mut cx, |this, cx| {
-                this.poll_snapshot(cx);
-                this.as_local().unwrap().broadcast_snapshot()
-            })
-            .await;
             Ok(entry)
         }))
     }
@@ -835,11 +780,6 @@ impl LocalWorktree {
                         .refresh_entry(path, abs_path, None, cx)
                 })
                 .await?;
-            this.update(&mut cx, |this, cx| {
-                this.poll_snapshot(cx);
-                this.as_local().unwrap().broadcast_snapshot()
-            })
-            .await;
             Ok(entry)
         })
     }
@@ -872,61 +812,55 @@ impl LocalWorktree {
             let this = this
                 .upgrade(&cx)
                 .ok_or_else(|| anyhow!("worktree was dropped"))?;
-            let (entry, snapshot, snapshots_tx) = this.read_with(&cx, |this, _| {
-                let this = this.as_local().unwrap();
-                let mut snapshot = this.background_snapshot.lock();
-                entry.is_ignored = snapshot
-                    .ignore_stack_for_path(&path, entry.is_dir())
-                    .is_path_ignored(&path, entry.is_dir());
-                if let Some(old_path) = old_path {
-                    snapshot.remove_path(&old_path);
+            this.update(&mut cx, |this, cx| {
+                let this = this.as_local_mut().unwrap();
+                let inserted_entry;
+                {
+                    let mut snapshot = this.background_snapshot.lock();
+                    entry.is_ignored = snapshot
+                        .ignore_stack_for_path(&path, entry.is_dir())
+                        .is_path_ignored(&path, entry.is_dir());
+                    if let Some(old_path) = old_path {
+                        snapshot.remove_path(&old_path);
+                    }
+                    inserted_entry = snapshot.insert_entry(entry, fs.as_ref());
+                    snapshot.scan_id += 1;
                 }
-                let entry = snapshot.insert_entry(entry, fs.as_ref());
-                snapshot.scan_id += 1;
-                let snapshots_tx = this.share.as_ref().map(|s| s.snapshots_tx.clone());
-                (entry, snapshot.clone(), snapshots_tx)
-            });
-            this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
-
-            if let Some(snapshots_tx) = snapshots_tx {
-                snapshots_tx.send(snapshot).await.ok();
-            }
-
-            Ok(entry)
+                this.poll_snapshot(true, cx);
+                Ok(inserted_entry)
+            })
         })
     }
 
     pub fn share(&mut self, project_id: u64, cx: &mut ModelContext<Worktree>) -> Task<Result<()>> {
         let (share_tx, share_rx) = oneshot::channel();
-        let (snapshots_to_send_tx, snapshots_to_send_rx) =
-            smol::channel::unbounded::<LocalSnapshot>();
+
         if self.share.is_some() {
             let _ = share_tx.send(Ok(()));
         } else {
+            let (snapshots_tx, mut snapshots_rx) = watch::channel_with(self.snapshot());
             let rpc = self.client.clone();
             let worktree_id = cx.model_id() as u64;
             let maintain_remote_snapshot = cx.background().spawn({
                 let rpc = rpc.clone();
                 let diagnostic_summaries = self.diagnostic_summaries.clone();
                 async move {
-                    let mut prev_snapshot = match snapshots_to_send_rx.recv().await {
-                        Ok(snapshot) => {
-                            if let Err(error) = rpc
-                                .request(proto::UpdateWorktree {
-                                    project_id,
-                                    worktree_id,
-                                    root_name: snapshot.root_name().to_string(),
-                                    updated_entries: snapshot
-                                        .entries_by_path
-                                        .iter()
-                                        .filter(|e| !e.is_ignored)
-                                        .map(Into::into)
-                                        .collect(),
-                                    removed_entries: Default::default(),
-                                    scan_id: snapshot.scan_id as u64,
-                                })
-                                .await
-                            {
+                    let mut prev_snapshot = match snapshots_rx.recv().await {
+                        Some(snapshot) => {
+                            let update = proto::UpdateWorktree {
+                                project_id,
+                                worktree_id,
+                                root_name: snapshot.root_name().to_string(),
+                                updated_entries: snapshot
+                                    .entries_by_path
+                                    .iter()
+                                    .map(Into::into)
+                                    .collect(),
+                                removed_entries: Default::default(),
+                                scan_id: snapshot.scan_id as u64,
+                                is_last_update: true,
+                            };
+                            if let Err(error) = send_worktree_update(&rpc, update).await {
                                 let _ = share_tx.send(Err(error));
                                 return Err(anyhow!("failed to send initial update worktree"));
                             } else {
@@ -934,8 +868,10 @@ impl LocalWorktree {
                                 snapshot
                             }
                         }
-                        Err(error) => {
-                            let _ = share_tx.send(Err(error.into()));
+                        None => {
+                            share_tx
+                                .send(Err(anyhow!("worktree dropped before share completed")))
+                                .ok();
                             return Err(anyhow!("failed to send initial update worktree"));
                         }
                     };
@@ -948,44 +884,12 @@ impl LocalWorktree {
                         })?;
                     }
 
-                    // Stream ignored entries in chunks.
-                    {
-                        let mut ignored_entries = prev_snapshot
-                            .entries_by_path
-                            .iter()
-                            .filter(|e| e.is_ignored);
-                        let mut ignored_entries_to_send = Vec::new();
-                        loop {
-                            #[cfg(any(test, feature = "test-support"))]
-                            const CHUNK_SIZE: usize = 2;
-                            #[cfg(not(any(test, feature = "test-support")))]
-                            const CHUNK_SIZE: usize = 256;
-
-                            let entry = ignored_entries.next();
-                            if ignored_entries_to_send.len() >= CHUNK_SIZE || entry.is_none() {
-                                rpc.request(proto::UpdateWorktree {
-                                    project_id,
-                                    worktree_id,
-                                    root_name: prev_snapshot.root_name().to_string(),
-                                    updated_entries: mem::take(&mut ignored_entries_to_send),
-                                    removed_entries: Default::default(),
-                                    scan_id: prev_snapshot.scan_id as u64,
-                                })
-                                .await?;
-                            }
-
-                            if let Some(entry) = entry {
-                                ignored_entries_to_send.push(entry.into());
-                            } else {
-                                break;
-                            }
-                        }
-                    }
-
-                    while let Ok(snapshot) = snapshots_to_send_rx.recv().await {
-                        let message =
-                            snapshot.build_update(&prev_snapshot, project_id, worktree_id, true);
-                        rpc.request(message).await?;
+                    while let Some(snapshot) = snapshots_rx.recv().await {
+                        send_worktree_update(
+                            &rpc,
+                            snapshot.build_update(&prev_snapshot, project_id, worktree_id, true),
+                        )
+                        .await?;
                         prev_snapshot = snapshot;
                     }
 
@@ -995,18 +899,12 @@ impl LocalWorktree {
             });
             self.share = Some(ShareState {
                 project_id,
-                snapshots_tx: snapshots_to_send_tx.clone(),
+                snapshots_tx,
                 _maintain_remote_snapshot: Some(maintain_remote_snapshot),
             });
         }
 
-        cx.spawn_weak(|this, cx| async move {
-            if let Some(this) = this.upgrade(&cx) {
-                this.read_with(&cx, |this, _| {
-                    let this = this.as_local().unwrap();
-                    let _ = snapshots_to_send_tx.try_send(this.snapshot());
-                });
-            }
+        cx.foreground().spawn(async move {
             share_rx
                 .await
                 .unwrap_or_else(|_| Err(anyhow!("share ended")))
@@ -1021,23 +919,6 @@ impl LocalWorktree {
         self.share.is_some()
     }
 
-    fn broadcast_snapshot(&self) -> impl Future<Output = ()> {
-        let mut to_send = None;
-        if !self.is_scanning() {
-            if let Some(share) = self.share.as_ref() {
-                to_send = Some((self.snapshot(), share.snapshots_tx.clone()));
-            }
-        }
-
-        async move {
-            if let Some((snapshot, snapshots_to_send_tx)) = to_send {
-                if let Err(err) = snapshots_to_send_tx.send(snapshot).await {
-                    log::error!("error submitting snapshot to send {}", err);
-                }
-            }
-        }
-    }
-
     pub fn send_extension_counts(&self, project_id: u64) {
         let mut extensions = Vec::new();
         let mut counts = Vec::new();
@@ -1063,31 +944,45 @@ impl RemoteWorktree {
         self.snapshot.clone()
     }
 
+    fn poll_snapshot(&mut self, cx: &mut ModelContext<Worktree>) {
+        self.snapshot = self.background_snapshot.lock().clone();
+        cx.emit(Event::UpdatedEntries);
+        cx.notify();
+    }
+
     pub fn disconnected_from_host(&mut self) {
         self.updates_tx.take();
+        self.snapshot_subscriptions.clear();
     }
 
-    pub fn update_from_remote(
-        &mut self,
-        envelope: TypedEnvelope<proto::UpdateWorktree>,
-    ) -> Result<()> {
+    pub fn update_from_remote(&mut self, update: proto::UpdateWorktree) {
         if let Some(updates_tx) = &self.updates_tx {
             updates_tx
-                .unbounded_send(envelope.payload)
+                .unbounded_send(update)
                 .expect("consumer runs to completion");
         }
-        Ok(())
     }
 
-    fn wait_for_snapshot(&self, scan_id: usize) -> impl Future<Output = ()> {
-        let mut rx = self.last_scan_id_rx.clone();
-        async move {
-            while let Some(applied_scan_id) = rx.next().await {
-                if applied_scan_id >= scan_id {
-                    return;
-                }
+    fn observed_snapshot(&self, scan_id: usize) -> bool {
+        self.scan_id > scan_id || (self.scan_id == scan_id && self.is_complete)
+    }
+
+    fn wait_for_snapshot(&mut self, scan_id: usize) -> impl Future<Output = ()> {
+        let (tx, rx) = oneshot::channel();
+        if self.observed_snapshot(scan_id) {
+            let _ = tx.send(());
+        } else {
+            match self
+                .snapshot_subscriptions
+                .binary_search_by_key(&scan_id, |probe| probe.0)
+            {
+                Ok(ix) | Err(ix) => self.snapshot_subscriptions.insert(ix, (scan_id, tx)),
             }
         }
+
+        async move {
+            let _ = rx.await;
+        }
     }
 
     pub fn update_diagnostic_summary(
@@ -1109,7 +1004,7 @@ impl RemoteWorktree {
     }
 
     pub fn insert_entry(
-        &self,
+        &mut self,
         entry: proto::Entry,
         scan_id: usize,
         cx: &mut ModelContext<Worktree>,
@@ -1128,7 +1023,7 @@ impl RemoteWorktree {
     }
 
     pub(crate) fn delete_entry(
-        &self,
+        &mut self,
         id: ProjectEntryId,
         scan_id: usize,
         cx: &mut ModelContext<Worktree>,
@@ -1204,7 +1099,7 @@ impl Snapshot {
         for entry_id in update.removed_entries {
             let entry = self
                 .entry_for_id(ProjectEntryId::from_proto(entry_id))
-                .ok_or_else(|| anyhow!("unknown entry"))?;
+                .ok_or_else(|| anyhow!("unknown entry {}", entry_id))?;
             entries_by_path_edits.push(Edit::Remove(PathKey(entry.path.clone())));
             entries_by_id_edits.push(Edit::Remove(entry.id));
         }
@@ -1226,6 +1121,7 @@ impl Snapshot {
         self.entries_by_path.edit(entries_by_path_edits, &());
         self.entries_by_id.edit(entries_by_id_edits, &());
         self.scan_id = update.scan_id as usize;
+        self.is_complete = update.is_last_update;
 
         Ok(())
     }
@@ -1351,27 +1247,16 @@ impl LocalSnapshot {
     }
 
     #[cfg(test)]
-    pub(crate) fn to_proto(
-        &self,
-        diagnostic_summaries: &TreeMap<PathKey, DiagnosticSummary>,
-        visible: bool,
-    ) -> proto::Worktree {
+    pub(crate) fn build_initial_update(&self, project_id: u64) -> proto::UpdateWorktree {
         let root_name = self.root_name.clone();
-        proto::Worktree {
-            id: self.id.0 as u64,
+        proto::UpdateWorktree {
+            project_id,
+            worktree_id: self.id().to_proto(),
             root_name,
-            entries: self
-                .entries_by_path
-                .iter()
-                .filter(|e| !e.is_ignored)
-                .map(Into::into)
-                .collect(),
-            diagnostic_summaries: diagnostic_summaries
-                .iter()
-                .map(|(path, summary)| summary.to_proto(&path.0))
-                .collect(),
-            visible,
+            updated_entries: self.entries_by_path.iter().map(Into::into).collect(),
+            removed_entries: Default::default(),
             scan_id: self.scan_id as u64,
+            is_last_update: true,
         }
     }
 
@@ -1438,6 +1323,7 @@ impl LocalSnapshot {
             updated_entries,
             removed_entries,
             scan_id: self.scan_id as u64,
+            is_last_update: true,
         }
     }
 
@@ -2109,7 +1995,7 @@ impl BackgroundScanner {
     }
 
     async fn run(mut self, events_rx: impl Stream<Item = Vec<fsevent::Event>>) {
-        if self.notify.unbounded_send(ScanState::Scanning).is_err() {
+        if self.notify.unbounded_send(ScanState::Initializing).is_err() {
             return;
         }
 
@@ -2128,8 +2014,13 @@ impl BackgroundScanner {
         }
 
         futures::pin_mut!(events_rx);
-        while let Some(events) = events_rx.next().await {
-            if self.notify.unbounded_send(ScanState::Scanning).is_err() {
+
+        while let Some(mut events) = events_rx.next().await {
+            while let Poll::Ready(Some(additional_events)) = futures::poll!(events_rx.next()) {
+                events.extend(additional_events);
+            }
+
+            if self.notify.unbounded_send(ScanState::Updating).is_err() {
                 break;
             }
 
@@ -2781,6 +2672,19 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
     }
 }
 
+async fn send_worktree_update(client: &Arc<Client>, update: proto::UpdateWorktree) -> Result<()> {
+    #[cfg(any(test, feature = "test-support"))]
+    const MAX_CHUNK_SIZE: usize = 2;
+    #[cfg(not(any(test, feature = "test-support")))]
+    const MAX_CHUNK_SIZE: usize = 256;
+
+    for update in proto::split_worktree_update(update, MAX_CHUNK_SIZE) {
+        client.request(update).await?;
+    }
+
+    Ok(())
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -2990,6 +2894,7 @@ mod tests {
                 root_name: Default::default(),
                 root_char_bag: Default::default(),
                 scan_id: 0,
+                is_complete: true,
             },
             extension_counts: Default::default(),
         };

crates/rpc/proto/zed.proto πŸ”—

@@ -172,7 +172,7 @@ message JoinProjectResponse {
 
     message Accept {
         uint32 replica_id = 1;
-        repeated Worktree worktrees = 2;
+        repeated WorktreeMetadata worktrees = 2;
         repeated Collaborator collaborators = 3;
         repeated LanguageServer language_servers = 4;        
     }
@@ -199,6 +199,7 @@ message UpdateWorktree {
     repeated Entry updated_entries = 4;
     repeated uint64 removed_entries = 5;
     uint64 scan_id = 6;
+    bool is_last_update = 7;
 }
 
 message UpdateWorktreeExtensions {
@@ -776,15 +777,6 @@ message User {
     string avatar_url = 3;
 }
 
-message Worktree {
-    uint64 id = 1;
-    string root_name = 2;
-    repeated Entry entries = 3;
-    repeated DiagnosticSummary diagnostic_summaries = 4;
-    bool visible = 5;
-    uint64 scan_id = 6;
-}
-
 message File {
     uint64 worktree_id = 1;
     optional uint64 entry_id = 2;

crates/rpc/src/proto.rs πŸ”—

@@ -5,6 +5,7 @@ use futures::{SinkExt as _, StreamExt as _};
 use prost::Message as _;
 use serde::Serialize;
 use std::any::{Any, TypeId};
+use std::{cmp, iter, mem};
 use std::{
     fmt::Debug,
     io,
@@ -392,6 +393,31 @@ impl From<Nonce> for u128 {
     }
 }
 
+pub fn split_worktree_update(
+    mut message: UpdateWorktree,
+    max_chunk_size: usize,
+) -> impl Iterator<Item = UpdateWorktree> {
+    let mut done = false;
+    iter::from_fn(move || {
+        if done {
+            return None;
+        }
+
+        let chunk_size = cmp::min(message.updated_entries.len(), max_chunk_size);
+        let updated_entries = message.updated_entries.drain(..chunk_size).collect();
+        done = message.updated_entries.is_empty();
+        Some(UpdateWorktree {
+            project_id: message.project_id,
+            worktree_id: message.worktree_id,
+            root_name: message.root_name.clone(),
+            updated_entries,
+            removed_entries: mem::take(&mut message.removed_entries),
+            scan_id: message.scan_id,
+            is_last_update: done && message.is_last_update,
+        })
+    })
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

crates/search/src/buffer_search.rs πŸ”—

@@ -58,7 +58,7 @@ fn add_toggle_option_action<A: Action>(option: SearchOption, cx: &mut MutableApp
 }
 
 pub struct BufferSearchBar {
-    query_editor: ViewHandle<Editor>,
+    pub query_editor: ViewHandle<Editor>,
     active_editor: Option<ViewHandle<Editor>>,
     active_match_index: Option<usize>,
     active_editor_subscription: Option<Subscription>,

crates/terminal/Cargo.toml πŸ”—

@@ -0,0 +1,27 @@
+[package]
+name = "terminal"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/terminal.rs"
+doctest = false
+
+[dependencies]
+alacritty_terminal = "0.16.1"
+editor = { path = "../editor" }
+util = { path = "../util" }
+gpui = { path = "../gpui" }
+theme = { path = "../theme" }
+settings = { path = "../settings" }
+workspace = { path = "../workspace" }
+project = { path = "../project" }
+smallvec = { version = "1.6", features = ["union"] }
+mio-extras = "2.0.6"
+futures = "0.3"
+ordered-float = "2.1.1"
+itertools = "0.10"
+
+
+[dev-dependencies]
+gpui = { path = "../gpui", features = ["test-support"] }

crates/terminal/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 percieved 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/src/terminal.rs πŸ”—

@@ -0,0 +1,494 @@
+use alacritty_terminal::{
+    config::{Config, Program, PtyConfig},
+    event::{Event as AlacTermEvent, EventListener, Notify},
+    event_loop::{EventLoop, Msg, Notifier},
+    grid::Scroll,
+    sync::FairMutex,
+    term::{color::Rgb as AlacRgb, SizeInfo},
+    tty, Term,
+};
+
+use futures::{
+    channel::mpsc::{unbounded, UnboundedSender},
+    StreamExt,
+};
+use gpui::{
+    actions, color::Color, elements::*, impl_internal_actions, platform::CursorStyle,
+    ClipboardItem, Entity, MutableAppContext, View, ViewContext,
+};
+use project::{Project, ProjectPath};
+use settings::Settings;
+use smallvec::SmallVec;
+use std::{path::PathBuf, sync::Arc};
+use workspace::{Item, Workspace};
+
+use crate::terminal_element::{get_color_at_index, TerminalEl};
+
+//ASCII Control characters on a keyboard
+const ETX_CHAR: char = 3_u8 as char; //'End of text', the control code for 'ctrl-c'
+const TAB_CHAR: char = 9_u8 as char;
+const CARRIAGE_RETURN_CHAR: char = 13_u8 as char;
+const ESC_CHAR: char = 27_u8 as char;
+const DEL_CHAR: char = 127_u8 as char;
+const LEFT_SEQ: &str = "\x1b[D";
+const RIGHT_SEQ: &str = "\x1b[C";
+const UP_SEQ: &str = "\x1b[A";
+const DOWN_SEQ: &str = "\x1b[B";
+const DEFAULT_TITLE: &str = "Terminal";
+
+pub mod terminal_element;
+
+///Action for carrying the input to the PTY
+#[derive(Clone, Default, Debug, PartialEq, Eq)]
+pub struct Input(pub String);
+
+///Event to transmit the scroll from the element to the view
+#[derive(Clone, Debug, PartialEq)]
+pub struct ScrollTerminal(pub i32);
+
+actions!(
+    terminal,
+    [Sigint, Escape, Del, Return, Left, Right, Up, Down, Tab, Clear, Paste, Deploy, Quit]
+);
+impl_internal_actions!(terminal, [Input, ScrollTerminal]);
+
+///Initialize and register all of our action handlers
+pub fn init(cx: &mut MutableAppContext) {
+    cx.add_action(Terminal::deploy);
+    cx.add_action(Terminal::write_to_pty);
+    cx.add_action(Terminal::send_sigint);
+    cx.add_action(Terminal::escape);
+    cx.add_action(Terminal::quit);
+    cx.add_action(Terminal::del);
+    cx.add_action(Terminal::carriage_return); //TODO figure out how to do this properly. Should we be checking the terminal mode?
+    cx.add_action(Terminal::left);
+    cx.add_action(Terminal::right);
+    cx.add_action(Terminal::up);
+    cx.add_action(Terminal::down);
+    cx.add_action(Terminal::tab);
+    cx.add_action(Terminal::paste);
+    cx.add_action(Terminal::scroll_terminal);
+}
+
+///A translation struct for Alacritty to communicate with us from their event loop
+#[derive(Clone)]
+pub struct ZedListener(UnboundedSender<AlacTermEvent>);
+
+impl EventListener for ZedListener {
+    fn send_event(&self, event: AlacTermEvent) {
+        self.0.unbounded_send(event).ok();
+    }
+}
+
+///A terminal view, maintains the PTY's file handles and communicates with the terminal
+pub struct Terminal {
+    pty_tx: Notifier,
+    term: Arc<FairMutex<Term<ZedListener>>>,
+    title: String,
+    has_new_content: bool,
+    has_bell: bool, //Currently using iTerm bell, show bell emoji in tab until input is received
+    cur_size: SizeInfo,
+}
+
+///Upward flowing events, for changing the title and such
+pub enum Event {
+    TitleChanged,
+    CloseTerminal,
+    Activate,
+}
+
+impl Entity for Terminal {
+    type Event = Event;
+}
+
+impl Terminal {
+    ///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices
+    fn new(cx: &mut ViewContext<Self>, working_directory: Option<PathBuf>) -> Self {
+        //Spawn a task so the Alacritty EventLoop can communicate with us in a view context
+        let (events_tx, mut events_rx) = unbounded();
+        cx.spawn_weak(|this, mut cx| async move {
+            while let Some(event) = events_rx.next().await {
+                match this.upgrade(&cx) {
+                    Some(handle) => {
+                        handle.update(&mut cx, |this, cx| {
+                            this.process_terminal_event(event, cx);
+                            cx.notify();
+                        });
+                    }
+                    None => break,
+                }
+            }
+        })
+        .detach();
+
+        let pty_config = PtyConfig {
+            shell: Some(Program::Just("zsh".to_string())),
+            working_directory,
+            hold: false,
+        };
+
+        let config = Config {
+            pty_config: pty_config.clone(),
+            ..Default::default()
+        };
+
+        //The details here don't matter, the terminal will be resized on the first layout
+        //Set to something small for easier debugging
+        let size_info = SizeInfo::new(200., 100.0, 5., 5., 0., 0., false);
+
+        //Set up the terminal...
+        let term = Term::new(&config, size_info, ZedListener(events_tx.clone()));
+        let term = Arc::new(FairMutex::new(term));
+
+        //Setup the pty...
+        let pty = tty::new(&pty_config, &size_info, None).expect("Could not create tty");
+
+        //And connect them together
+        let event_loop = EventLoop::new(
+            term.clone(),
+            ZedListener(events_tx.clone()),
+            pty,
+            pty_config.hold,
+            false,
+        );
+
+        //Kick things off
+        let pty_tx = Notifier(event_loop.channel());
+        let _io_thread = event_loop.spawn();
+        Terminal {
+            title: DEFAULT_TITLE.to_string(),
+            term,
+            pty_tx,
+            has_new_content: false,
+            has_bell: false,
+            cur_size: size_info,
+        }
+    }
+
+    ///Takes events from Alacritty and translates them to behavior on this view
+    fn process_terminal_event(
+        &mut self,
+        event: alacritty_terminal::event::Event,
+        cx: &mut ViewContext<Self>,
+    ) {
+        match event {
+            AlacTermEvent::Wakeup => {
+                if !cx.is_self_focused() {
+                    self.has_new_content = true; //Change tab content
+                    cx.emit(Event::TitleChanged);
+                } else {
+                    cx.notify()
+                }
+            }
+            AlacTermEvent::PtyWrite(out) => self.write_to_pty(&Input(out), cx),
+            AlacTermEvent::MouseCursorDirty => {
+                //Calculate new cursor style.
+                //TODO
+                //Check on correctly handling mouse events for terminals
+                cx.platform().set_cursor_style(CursorStyle::Arrow); //???
+            }
+            AlacTermEvent::Title(title) => {
+                self.title = title;
+                cx.emit(Event::TitleChanged);
+            }
+            AlacTermEvent::ResetTitle => {
+                self.title = DEFAULT_TITLE.to_string();
+                cx.emit(Event::TitleChanged);
+            }
+            AlacTermEvent::ClipboardStore(_, data) => {
+                cx.write_to_clipboard(ClipboardItem::new(data))
+            }
+            AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(
+                &Input(format(
+                    &cx.read_from_clipboard()
+                        .map(|ci| ci.text().to_string())
+                        .unwrap_or("".to_string()),
+                )),
+                cx,
+            ),
+            AlacTermEvent::ColorRequest(index, format) => {
+                let color = self.term.lock().colors()[index].unwrap_or_else(|| {
+                    let term_style = &cx.global::<Settings>().theme.terminal;
+                    match index {
+                        0..=255 => to_alac_rgb(get_color_at_index(&(index as u8), term_style)),
+                        //These additional values are required to match the Alacritty Colors object's behavior
+                        256 => to_alac_rgb(term_style.foreground),
+                        257 => to_alac_rgb(term_style.background),
+                        258 => to_alac_rgb(term_style.cursor),
+                        259 => to_alac_rgb(term_style.dim_black),
+                        260 => to_alac_rgb(term_style.dim_red),
+                        261 => to_alac_rgb(term_style.dim_green),
+                        262 => to_alac_rgb(term_style.dim_yellow),
+                        263 => to_alac_rgb(term_style.dim_blue),
+                        264 => to_alac_rgb(term_style.dim_magenta),
+                        265 => to_alac_rgb(term_style.dim_cyan),
+                        266 => to_alac_rgb(term_style.dim_white),
+                        267 => to_alac_rgb(term_style.bright_foreground),
+                        268 => to_alac_rgb(term_style.black), //Dim Background, non-standard
+                        _ => AlacRgb { r: 0, g: 0, b: 0 },
+                    }
+                });
+                self.write_to_pty(&Input(format(color)), cx)
+            }
+            AlacTermEvent::CursorBlinkingChange => {
+                //TODO: Set a timer to blink the cursor on and off
+            }
+            AlacTermEvent::Bell => {
+                self.has_bell = true;
+                cx.emit(Event::TitleChanged);
+            }
+            AlacTermEvent::Exit => self.quit(&Quit, cx),
+        }
+    }
+
+    ///Resize the terminal and the PTY. This locks the terminal.
+    fn set_size(&mut self, new_size: SizeInfo) {
+        if new_size != self.cur_size {
+            self.pty_tx.0.send(Msg::Resize(new_size)).ok();
+            self.term.lock().resize(new_size);
+            self.cur_size = new_size;
+        }
+    }
+
+    ///Scroll the terminal. This locks the terminal
+    fn scroll_terminal(&mut self, scroll: &ScrollTerminal, _: &mut ViewContext<Self>) {
+        self.term.lock().scroll_display(Scroll::Delta(scroll.0));
+    }
+
+    ///Create a new Terminal in the current working directory or the user's home directory
+    fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
+        let project = workspace.project().read(cx);
+        let abs_path = project
+            .active_entry()
+            .and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
+            .and_then(|worktree_handle| worktree_handle.read(cx).as_local())
+            .map(|wt| wt.abs_path().to_path_buf());
+
+        workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(cx, abs_path))), cx);
+    }
+
+    ///Send the shutdown message to Alacritty
+    fn shutdown_pty(&mut self) {
+        self.pty_tx.0.send(Msg::Shutdown).ok();
+    }
+
+    ///Tell Zed to close us
+    fn quit(&mut self, _: &Quit, cx: &mut ViewContext<Self>) {
+        cx.emit(Event::CloseTerminal);
+    }
+
+    ///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.write_to_pty(&Input(item.text().to_owned()), cx);
+        }
+    }
+
+    ///Write the Input payload to the tty. This locks the terminal so we can scroll it.
+    fn write_to_pty(&mut self, input: &Input, cx: &mut ViewContext<Self>) {
+        //iTerm bell behavior, bell stays until terminal is interacted with
+        self.has_bell = false;
+        self.term.lock().scroll_display(Scroll::Bottom);
+        cx.emit(Event::TitleChanged);
+        self.pty_tx.notify(input.0.clone().into_bytes());
+    }
+
+    ///Send the `up` key
+    fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
+        self.write_to_pty(&Input(UP_SEQ.to_string()), cx);
+    }
+
+    ///Send the `down` key
+    fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
+        self.write_to_pty(&Input(DOWN_SEQ.to_string()), cx);
+    }
+
+    ///Send the `tab` key
+    fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
+        self.write_to_pty(&Input(TAB_CHAR.to_string()), cx);
+    }
+
+    ///Send `SIGINT` (`ctrl-c`)
+    fn send_sigint(&mut self, _: &Sigint, cx: &mut ViewContext<Self>) {
+        self.write_to_pty(&Input(ETX_CHAR.to_string()), cx);
+    }
+
+    ///Send the `escape` key
+    fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
+        self.write_to_pty(&Input(ESC_CHAR.to_string()), cx);
+    }
+
+    ///Send the `delete` key. TODO: Difference between this and backspace?
+    fn del(&mut self, _: &Del, cx: &mut ViewContext<Self>) {
+        self.write_to_pty(&Input(DEL_CHAR.to_string()), cx);
+    }
+
+    ///Send a carriage return. TODO: May need to check the terminal mode.
+    fn carriage_return(&mut self, _: &Return, cx: &mut ViewContext<Self>) {
+        self.write_to_pty(&Input(CARRIAGE_RETURN_CHAR.to_string()), cx);
+    }
+
+    //Send the `left` key
+    fn left(&mut self, _: &Left, cx: &mut ViewContext<Self>) {
+        self.write_to_pty(&Input(LEFT_SEQ.to_string()), cx);
+    }
+
+    //Send the `right` key
+    fn right(&mut self, _: &Right, cx: &mut ViewContext<Self>) {
+        self.write_to_pty(&Input(RIGHT_SEQ.to_string()), cx);
+    }
+}
+
+impl Drop for Terminal {
+    fn drop(&mut self) {
+        self.shutdown_pty();
+    }
+}
+
+impl View for Terminal {
+    fn ui_name() -> &'static str {
+        "Terminal"
+    }
+
+    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
+        TerminalEl::new(cx.handle()).contained().boxed()
+    }
+
+    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
+        cx.emit(Event::Activate);
+        self.has_new_content = false;
+    }
+}
+
+impl Item for Terminal {
+    fn tab_content(&self, tab_theme: &theme::Tab, cx: &gpui::AppContext) -> ElementBox {
+        let settings = cx.global::<Settings>();
+        let search_theme = &settings.theme.search; //TODO properly integrate themes
+
+        let mut flex = Flex::row();
+
+        if self.has_bell {
+            flex.add_child(
+                Svg::new("icons/zap.svg") //TODO: Swap out for a better icon, or at least resize this
+                    .with_color(tab_theme.label.text.color)
+                    .constrained()
+                    .with_width(search_theme.tab_icon_width)
+                    .aligned()
+                    .boxed(),
+            );
+        };
+
+        flex.with_child(
+            Label::new(self.title.clone(), tab_theme.label.clone())
+                .aligned()
+                .contained()
+                .with_margin_left(if self.has_bell {
+                    search_theme.tab_icon_spacing
+                } else {
+                    0.
+                })
+                .boxed(),
+        )
+        .boxed()
+    }
+
+    fn project_path(&self, _cx: &gpui::AppContext) -> Option<ProjectPath> {
+        None
+    }
+
+    fn project_entry_ids(&self, _cx: &gpui::AppContext) -> SmallVec<[project::ProjectEntryId; 3]> {
+        SmallVec::new()
+    }
+
+    fn is_singleton(&self, _cx: &gpui::AppContext) -> bool {
+        false
+    }
+
+    fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
+
+    fn can_save(&self, _cx: &gpui::AppContext) -> bool {
+        false
+    }
+
+    fn save(
+        &mut self,
+        _project: gpui::ModelHandle<Project>,
+        _cx: &mut ViewContext<Self>,
+    ) -> gpui::Task<gpui::anyhow::Result<()>> {
+        unreachable!("save should not have been called");
+    }
+
+    fn save_as(
+        &mut self,
+        _project: gpui::ModelHandle<Project>,
+        _abs_path: std::path::PathBuf,
+        _cx: &mut ViewContext<Self>,
+    ) -> gpui::Task<gpui::anyhow::Result<()>> {
+        unreachable!("save_as should not have been called");
+    }
+
+    fn reload(
+        &mut self,
+        _project: gpui::ModelHandle<Project>,
+        _cx: &mut ViewContext<Self>,
+    ) -> gpui::Task<gpui::anyhow::Result<()>> {
+        gpui::Task::ready(Ok(()))
+    }
+
+    fn is_dirty(&self, _: &gpui::AppContext) -> bool {
+        self.has_new_content
+    }
+
+    fn should_update_tab_on_event(event: &Self::Event) -> bool {
+        matches!(event, &Event::TitleChanged)
+    }
+
+    fn should_close_item_on_event(event: &Self::Event) -> bool {
+        matches!(event, &Event::CloseTerminal)
+    }
+
+    fn should_activate_item_on_event(event: &Self::Event) -> bool {
+        matches!(event, &Event::Activate)
+    }
+}
+
+//Convenience method for less lines
+fn to_alac_rgb(color: Color) -> AlacRgb {
+    AlacRgb {
+        r: color.r,
+        g: color.g,
+        b: color.g,
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::terminal_element::{build_chunks, BuiltChunks};
+    use gpui::TestAppContext;
+
+    ///Basic integration test, can we get the terminal to show up, execute a command,
+    //and produce noticable output?
+    #[gpui::test]
+    async fn test_terminal(cx: &mut TestAppContext) {
+        let terminal = cx.add_view(Default::default(), |cx| Terminal::new(cx, None));
+
+        terminal.update(cx, |terminal, cx| {
+            terminal.write_to_pty(&Input(("expr 3 + 4".to_string()).to_string()), cx);
+            terminal.carriage_return(&Return, cx);
+        });
+
+        terminal
+            .condition(cx, |terminal, _cx| {
+                let term = terminal.term.clone();
+                let BuiltChunks { chunks, .. } = build_chunks(
+                    term.lock().renderable_content().display_iter,
+                    &Default::default(),
+                    Default::default(),
+                );
+                let content = chunks.iter().map(|e| e.0.trim()).collect::<String>();
+                content.contains("7")
+            })
+            .await;
+    }
+}

crates/terminal/src/terminal_element.rs πŸ”—

@@ -0,0 +1,621 @@
+use alacritty_terminal::{
+    ansi::Color as AnsiColor,
+    grid::{GridIterator, Indexed},
+    index::Point,
+    term::{
+        cell::{Cell, Flags},
+        SizeInfo,
+    },
+};
+use editor::{Cursor, CursorShape};
+use gpui::{
+    color::Color,
+    elements::*,
+    fonts::{HighlightStyle, TextStyle, Underline},
+    geometry::{
+        rect::RectF,
+        vector::{vec2f, Vector2F},
+    },
+    json::json,
+    text_layout::{Line, RunStyle},
+    Event, FontCache, MouseRegion, PaintContext, Quad, SizeConstraint, WeakViewHandle,
+};
+use itertools::Itertools;
+use ordered_float::OrderedFloat;
+use settings::Settings;
+use std::{iter, rc::Rc};
+use theme::TerminalStyle;
+
+use crate::{Input, ScrollTerminal, Terminal};
+
+///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
+///Scroll multiplier that is set to 3 by default. This will be removed when I
+///Implement scroll bars.
+const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
+
+///Used to display the grid as passed to Alacritty and the TTY.
+///Useful for debugging inconsistencies between behavior and display
+#[cfg(debug_assertions)]
+const DEBUG_GRID: bool = false;
+
+///The GPUI element that paints the terminal.
+pub struct TerminalEl {
+    view: WeakViewHandle<Terminal>,
+}
+
+///Represents a span of cells in a single line in the terminal's grid.
+///This is used for drawing background rectangles
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
+pub struct RectSpan {
+    start: i32,
+    end: i32,
+    line: usize,
+    color: Color,
+}
+
+///A background color span
+impl RectSpan {
+    ///Creates a new LineSpan. `start` must be <= `end`.
+    ///If `start` == `end`, then this span is considered to be over a
+    /// single cell
+    fn new(start: i32, end: i32, line: usize, color: Color) -> RectSpan {
+        debug_assert!(start <= end);
+        RectSpan {
+            start,
+            end,
+            line,
+            color,
+        }
+    }
+}
+
+///Helper types so I don't mix these two up
+struct CellWidth(f32);
+struct LineHeight(f32);
+
+///The information generated during layout that is nescessary for painting
+pub struct LayoutState {
+    lines: Vec<Line>,
+    line_height: LineHeight,
+    em_width: CellWidth,
+    cursor: Option<Cursor>,
+    cur_size: SizeInfo,
+    background_color: Color,
+    background_rects: Vec<(RectF, Color)>, //Vec index == Line index for the LineSpan
+}
+
+impl TerminalEl {
+    pub fn new(view: WeakViewHandle<Terminal>) -> TerminalEl {
+        TerminalEl { view }
+    }
+}
+
+impl Element for TerminalEl {
+    type LayoutState = LayoutState;
+    type PaintState = ();
+
+    fn layout(
+        &mut self,
+        constraint: gpui::SizeConstraint,
+        cx: &mut gpui::LayoutContext,
+    ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
+        //Settings immutably borrows cx here for the settings and font cache
+        //and we need to modify the cx to resize the terminal. So instead of
+        //storing Settings or the font_cache(), we toss them ASAP and then reborrow later
+        let text_style = make_text_style(cx.font_cache(), cx.global::<Settings>());
+        let line_height = LineHeight(cx.font_cache().line_height(text_style.font_size));
+        let cell_width = CellWidth(
+            cx.font_cache()
+                .em_advance(text_style.font_id, text_style.font_size),
+        );
+        let view_handle = self.view.upgrade(cx).unwrap();
+
+        //Tell the view our new size. Requires a mutable borrow of cx and the view
+        let cur_size = make_new_size(constraint, &cell_width, &line_height);
+        //Note that set_size locks and mutates the terminal.
+        //TODO: Would be nice to lock once for the whole of layout
+        view_handle.update(cx.app, |view, _cx| view.set_size(cur_size));
+
+        //Now that we're done with the mutable portion, grab the immutable settings and view again
+        let terminal_theme = &(cx.global::<Settings>()).theme.terminal;
+        let term = view_handle.read(cx).term.lock();
+
+        let grid = term.grid();
+        let cursor_point = grid.cursor.point;
+        let cursor_text = grid[cursor_point.line][cursor_point.column].c.to_string();
+
+        let content = term.renderable_content();
+
+        //And we're off! Begin layouting
+        let BuiltChunks {
+            chunks,
+            line_count,
+            cursor_index,
+        } = build_chunks(content.display_iter, &terminal_theme, cursor_point);
+
+        let shaped_lines = layout_highlighted_chunks(
+            chunks
+                .iter()
+                .map(|(text, style, _)| (text.as_str(), *style)),
+            &text_style,
+            cx.text_layout_cache,
+            cx.font_cache(),
+            usize::MAX,
+            line_count,
+        );
+
+        let backgrounds = chunks
+            .iter()
+            .filter(|(_, _, line_span)| line_span != &RectSpan::default())
+            .map(|(_, _, line_span)| *line_span)
+            .collect();
+        let background_rects = make_background_rects(backgrounds, &shaped_lines, &line_height);
+
+        let block_text = cx.text_layout_cache.layout_str(
+            &cursor_text,
+            text_style.font_size,
+            &[(
+                cursor_text.len(),
+                RunStyle {
+                    font_id: text_style.font_id,
+                    color: terminal_theme.background,
+                    underline: Default::default(),
+                },
+            )],
+        );
+
+        let cursor = get_cursor_position(
+            content.cursor.point.line.0 as usize,
+            cursor_index,
+            &shaped_lines,
+            content.display_offset,
+            &line_height,
+        )
+        .map(move |(cursor_position, block_width)| {
+            let block_width = if block_width != 0.0 {
+                block_width
+            } else {
+                cell_width.0
+            };
+
+            Cursor::new(
+                cursor_position,
+                block_width,
+                line_height.0,
+                terminal_theme.cursor,
+                CursorShape::Block,
+                Some(block_text.clone()),
+            )
+        });
+
+        (
+            constraint.max,
+            LayoutState {
+                lines: shaped_lines,
+                line_height,
+                em_width: cell_width,
+                cursor,
+                cur_size,
+                background_rects,
+                background_color: terminal_theme.background,
+            },
+        )
+    }
+
+    fn paint(
+        &mut self,
+        bounds: gpui::geometry::rect::RectF,
+        visible_bounds: gpui::geometry::rect::RectF,
+        layout: &mut Self::LayoutState,
+        cx: &mut gpui::PaintContext,
+    ) -> Self::PaintState {
+        //Setup element stuff
+        cx.scene.push_layer(Some(visible_bounds));
+
+        //Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
+        cx.scene.push_mouse_region(MouseRegion {
+            view_id: self.view.id(),
+            mouse_down: Some(Rc::new(|_, cx| cx.focus_parent_view())),
+            bounds: visible_bounds,
+            ..Default::default()
+        });
+
+        let origin = bounds.origin() + vec2f(layout.em_width.0, 0.);
+
+        //Start us off with a nice simple background color
+        cx.scene.push_layer(Some(visible_bounds));
+        cx.scene.push_quad(Quad {
+            bounds: RectF::new(bounds.origin(), bounds.size()),
+            background: Some(layout.background_color),
+            border: Default::default(),
+            corner_radius: 0.,
+        });
+
+        //Draw cell backgrounds
+        for background_rect in &layout.background_rects {
+            let new_origin = origin + background_rect.0.origin();
+            cx.scene.push_quad(Quad {
+                bounds: RectF::new(new_origin, background_rect.0.size()),
+                background: Some(background_rect.1),
+                border: Default::default(),
+                corner_radius: 0.,
+            })
+        }
+        cx.scene.pop_layer();
+
+        //Draw text
+        cx.scene.push_layer(Some(visible_bounds));
+        let mut line_origin = origin.clone();
+        for line in &layout.lines {
+            let boundaries = RectF::new(line_origin, vec2f(bounds.width(), layout.line_height.0));
+            if boundaries.intersects(visible_bounds) {
+                line.paint(line_origin, visible_bounds, layout.line_height.0, cx);
+            }
+            line_origin.set_y(boundaries.max_y());
+        }
+        cx.scene.pop_layer();
+
+        //Draw cursor
+        if let Some(cursor) = &layout.cursor {
+            cx.scene.push_layer(Some(visible_bounds));
+            cursor.paint(origin, cx);
+            cx.scene.pop_layer();
+        }
+
+        #[cfg(debug_assertions)]
+        if DEBUG_GRID {
+            draw_debug_grid(bounds, layout, cx);
+        }
+
+        cx.scene.pop_layer();
+    }
+
+    fn dispatch_event(
+        &mut self,
+        event: &gpui::Event,
+        _bounds: gpui::geometry::rect::RectF,
+        visible_bounds: gpui::geometry::rect::RectF,
+        layout: &mut Self::LayoutState,
+        _paint: &mut Self::PaintState,
+        cx: &mut gpui::EventContext,
+    ) -> bool {
+        match event {
+            Event::ScrollWheel {
+                delta, position, ..
+            } => visible_bounds
+                .contains_point(*position)
+                .then(|| {
+                    let vertical_scroll =
+                        (delta.y() / layout.line_height.0) * ALACRITTY_SCROLL_MULTIPLIER;
+                    cx.dispatch_action(ScrollTerminal(vertical_scroll.round() as i32));
+                })
+                .is_some(),
+            Event::KeyDown {
+                input: Some(input), ..
+            } => cx
+                .is_parent_view_focused()
+                .then(|| {
+                    cx.dispatch_action(Input(input.to_string()));
+                })
+                .is_some(),
+            _ => false,
+        }
+    }
+
+    fn debug(
+        &self,
+        _bounds: gpui::geometry::rect::RectF,
+        _layout: &Self::LayoutState,
+        _paint: &Self::PaintState,
+        _cx: &gpui::DebugContext,
+    ) -> gpui::serde_json::Value {
+        json!({
+            "type": "TerminalElement",
+        })
+    }
+}
+
+///Configures a text style from the current settings.
+fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle {
+    TextStyle {
+        color: settings.theme.editor.text_color,
+        font_family_id: settings.buffer_font_family,
+        font_family_name: font_cache.family_name(settings.buffer_font_family).unwrap(),
+        font_id: font_cache
+            .select_font(settings.buffer_font_family, &Default::default())
+            .unwrap(),
+        font_size: settings.buffer_font_size,
+        font_properties: Default::default(),
+        underline: Default::default(),
+    }
+}
+
+///Configures a size info object from the given information.
+fn make_new_size(
+    constraint: SizeConstraint,
+    cell_width: &CellWidth,
+    line_height: &LineHeight,
+) -> SizeInfo {
+    SizeInfo::new(
+        constraint.max.x() - cell_width.0,
+        constraint.max.y(),
+        cell_width.0,
+        line_height.0,
+        0.,
+        0.,
+        false,
+    )
+}
+
+pub struct BuiltChunks {
+    pub chunks: Vec<(String, Option<HighlightStyle>, RectSpan)>,
+    pub line_count: usize,
+    pub cursor_index: usize,
+}
+
+///In a single pass, this function generates the background and foreground color info for every item in the grid.
+pub(crate) fn build_chunks(
+    grid_iterator: GridIterator<Cell>,
+    theme: &TerminalStyle,
+    cursor_point: Point,
+) -> BuiltChunks {
+    let mut line_count: usize = 0;
+    let mut cursor_index: usize = 0;
+    //Every `group_by()` -> `into_iter()` pair needs to be seperated by a local variable so
+    //rust knows where to put everything.
+    //Start by grouping by lines
+    let lines = grid_iterator.group_by(|i| i.point.line.0);
+    let result = lines
+        .into_iter()
+        .map(|(_line_grid_index, line)| {
+            line_count += 1;
+            let mut col_index = 0;
+            //Setup a variable
+
+            //Then group by style
+            let chunks = line.group_by(|i| cell_style(&i, theme));
+            chunks
+                .into_iter()
+                .map(|(style, fragment)| {
+                    //And assemble the styled fragment into it's background and foreground information
+                    let mut str_fragment = String::new();
+                    for indexed_cell in fragment {
+                        if cursor_point.line.0 == indexed_cell.point.line.0
+                            && indexed_cell.point.column < cursor_point.column.0
+                        {
+                            cursor_index += indexed_cell.c.to_string().len();
+                        }
+                        str_fragment.push(indexed_cell.c);
+                    }
+
+                    let start = col_index;
+                    let end = start + str_fragment.len() as i32;
+
+                    //munge it here
+                    col_index = end;
+                    (
+                        str_fragment,
+                        Some(style.0),
+                        RectSpan::new(start, end, line_count - 1, style.1), //Line count -> Line index
+                    )
+                })
+                //Add a \n to the end, as we're using text layouting rather than grid layouts
+                .chain(iter::once(("\n".to_string(), None, Default::default())))
+                .collect::<Vec<(String, Option<HighlightStyle>, RectSpan)>>()
+        })
+        .flatten()
+        //We have a Vec<Vec<>> (Vec of lines of styled chunks), flatten to just Vec<> (the styled chunks)
+        .collect::<Vec<(String, Option<HighlightStyle>, RectSpan)>>();
+
+    BuiltChunks {
+        chunks: result,
+        line_count,
+        cursor_index,
+    }
+}
+
+///Convert a RectSpan in terms of character offsets, into RectFs of exact offsets
+fn make_background_rects(
+    backgrounds: Vec<RectSpan>,
+    shaped_lines: &Vec<Line>,
+    line_height: &LineHeight,
+) -> Vec<(RectF, Color)> {
+    backgrounds
+        .into_iter()
+        .map(|line_span| {
+            //This should always be safe, as the shaped lines and backgrounds where derived
+            //At the same time earlier
+            let line = shaped_lines
+                .get(line_span.line)
+                .expect("Background line_num did not correspond to a line number");
+            let x = line.x_for_index(line_span.start as usize);
+            let width = line.x_for_index(line_span.end as usize) - x;
+            (
+                RectF::new(
+                    vec2f(x, line_span.line as f32 * line_height.0),
+                    vec2f(width, line_height.0),
+                ),
+                line_span.color,
+            )
+        })
+        .collect::<Vec<(RectF, Color)>>()
+}
+
+// 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 get_cursor_position(
+    line: usize,
+    line_index: usize,
+    shaped_lines: &Vec<Line>,
+    display_offset: usize,
+    line_height: &LineHeight,
+) -> Option<(Vector2F, f32)> {
+    let cursor_line = line + display_offset;
+    shaped_lines.get(cursor_line).map(|layout_line| {
+        let cursor_x = layout_line.x_for_index(line_index);
+        let next_char_x = layout_line.x_for_index(line_index + 1);
+        (
+            vec2f(cursor_x, cursor_line as f32 * line_height.0),
+            next_char_x - cursor_x,
+        )
+    })
+}
+
+///Convert the Alacritty cell styles to GPUI text styles and background color
+fn cell_style(indexed: &Indexed<&Cell>, style: &TerminalStyle) -> (HighlightStyle, Color) {
+    let flags = indexed.cell.flags;
+    let fg = Some(convert_color(&indexed.cell.fg, style));
+    let bg = convert_color(&indexed.cell.bg, style);
+
+    let underline = flags.contains(Flags::UNDERLINE).then(|| Underline {
+        color: fg,
+        squiggly: false,
+        thickness: OrderedFloat(1.),
+    });
+
+    (
+        HighlightStyle {
+            color: fg,
+            underline,
+            ..Default::default()
+        },
+        bg,
+    )
+}
+
+///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent
+fn convert_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color {
+    match alac_color {
+        //Named and theme defined colors
+        alacritty_terminal::ansi::Color::Named(n) => match n {
+            alacritty_terminal::ansi::NamedColor::Black => style.black,
+            alacritty_terminal::ansi::NamedColor::Red => style.red,
+            alacritty_terminal::ansi::NamedColor::Green => style.green,
+            alacritty_terminal::ansi::NamedColor::Yellow => style.yellow,
+            alacritty_terminal::ansi::NamedColor::Blue => style.blue,
+            alacritty_terminal::ansi::NamedColor::Magenta => style.magenta,
+            alacritty_terminal::ansi::NamedColor::Cyan => style.cyan,
+            alacritty_terminal::ansi::NamedColor::White => style.white,
+            alacritty_terminal::ansi::NamedColor::BrightBlack => style.bright_black,
+            alacritty_terminal::ansi::NamedColor::BrightRed => style.bright_red,
+            alacritty_terminal::ansi::NamedColor::BrightGreen => style.bright_green,
+            alacritty_terminal::ansi::NamedColor::BrightYellow => style.bright_yellow,
+            alacritty_terminal::ansi::NamedColor::BrightBlue => style.bright_blue,
+            alacritty_terminal::ansi::NamedColor::BrightMagenta => style.bright_magenta,
+            alacritty_terminal::ansi::NamedColor::BrightCyan => style.bright_cyan,
+            alacritty_terminal::ansi::NamedColor::BrightWhite => style.bright_white,
+            alacritty_terminal::ansi::NamedColor::Foreground => style.foreground,
+            alacritty_terminal::ansi::NamedColor::Background => style.background,
+            alacritty_terminal::ansi::NamedColor::Cursor => style.cursor,
+            alacritty_terminal::ansi::NamedColor::DimBlack => style.dim_black,
+            alacritty_terminal::ansi::NamedColor::DimRed => style.dim_red,
+            alacritty_terminal::ansi::NamedColor::DimGreen => style.dim_green,
+            alacritty_terminal::ansi::NamedColor::DimYellow => style.dim_yellow,
+            alacritty_terminal::ansi::NamedColor::DimBlue => style.dim_blue,
+            alacritty_terminal::ansi::NamedColor::DimMagenta => style.dim_magenta,
+            alacritty_terminal::ansi::NamedColor::DimCyan => style.dim_cyan,
+            alacritty_terminal::ansi::NamedColor::DimWhite => style.dim_white,
+            alacritty_terminal::ansi::NamedColor::BrightForeground => style.bright_foreground,
+            alacritty_terminal::ansi::NamedColor::DimForeground => style.dim_foreground,
+        },
+        //'True' colors
+        alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, u8::MAX),
+        //8 bit, indexed colors
+        alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(i, style),
+    }
+}
+
+///Converts an 8 bit ANSI color to it's GPUI equivalent.
+pub fn get_color_at_index(index: &u8, style: &TerminalStyle) -> Color {
+    match index {
+        //0-15 are the same as the named colors above
+        0 => style.black,
+        1 => style.red,
+        2 => style.green,
+        3 => style.yellow,
+        4 => style.blue,
+        5 => style.magenta,
+        6 => style.cyan,
+        7 => style.white,
+        8 => style.bright_black,
+        9 => style.bright_red,
+        10 => style.bright_green,
+        11 => style.bright_yellow,
+        12 => style.bright_blue,
+        13 => style.bright_magenta,
+        14 => style.bright_cyan,
+        15 => style.bright_white,
+        //16-231 are mapped to their RGB colors on a 0-5 range per channel
+        16..=231 => {
+            let (r, g, b) = rgb_for_index(index); //Split the index into it's ANSI-RGB components
+            let step = (u8::MAX as f32 / 5.).floor() as u8; //Split the RGB range into 5 chunks, with floor so no overflow
+            Color::new(r * step, g * step, b * step, u8::MAX) //Map the ANSI-RGB components to an RGB color
+        }
+        //232-255 are a 24 step grayscale from black to white
+        232..=255 => {
+            let i = index - 232; //Align index to 0..24
+            let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks
+            Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale
+        }
+    }
+}
+
+///Generates the rgb channels in [0, 5] for a given index into the 6x6x6 ANSI color cube
+///See: [8 bit ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit).
+///
+///Wikipedia gives a formula for calculating the index for a given color:
+///
+///index = 16 + 36 Γ— r + 6 Γ— g + b (0 ≀ r, g, b ≀ 5)
+///
+///This function does the reverse, calculating the r, g, and b components from a given index.
+fn rgb_for_index(i: &u8) -> (u8, u8, u8) {
+    debug_assert!(i >= &16 && i <= &231);
+    let i = i - 16;
+    let r = (i - (i % 36)) / 36;
+    let g = ((i % 36) - (i % 6)) / 6;
+    let b = (i % 36) % 6;
+    (r, g, b)
+}
+
+///Draws the grid as Alacritty sees it. Useful for checking if there is an inconsistency between
+///Display and conceptual grid.
+#[cfg(debug_assertions)]
+fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) {
+    let width = layout.cur_size.width();
+    let height = layout.cur_size.height();
+    //Alacritty uses 'as usize', so shall we.
+    for col in 0..(width / layout.em_width.0).round() as usize {
+        cx.scene.push_quad(Quad {
+            bounds: RectF::new(
+                bounds.origin() + vec2f((col + 1) as f32 * layout.em_width.0, 0.),
+                vec2f(1., height),
+            ),
+            background: Some(Color::green()),
+            border: Default::default(),
+            corner_radius: 0.,
+        });
+    }
+    for row in 0..((height / layout.line_height.0) + 1.0).round() as usize {
+        cx.scene.push_quad(Quad {
+            bounds: RectF::new(
+                bounds.origin() + vec2f(layout.em_width.0, row as f32 * layout.line_height.0),
+                vec2f(width, 1.),
+            ),
+            background: Some(Color::green()),
+            border: Default::default(),
+            corner_radius: 0.,
+        });
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn test_rgb_for_index() {
+        //Test every possible value in the color cube
+        for i in 16..=231 {
+            let (r, g, b) = crate::terminal_element::rgb_for_index(&(i as u8));
+            assert_eq!(i, 16 + 36 * r + 6 * g + b);
+        }
+    }
+}

crates/terminal/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/theme/src/theme.rs πŸ”—

@@ -33,6 +33,7 @@ pub struct Theme {
     pub contact_notification: ContactNotification,
     pub update_notification: UpdateNotification,
     pub tooltip: TooltipStyle,
+    pub terminal: TerminalStyle,
 }
 
 #[derive(Deserialize, Default)]
@@ -633,3 +634,36 @@ pub struct HoverPopover {
     pub prose: TextStyle,
     pub highlight: Color,
 }
+
+#[derive(Clone, Deserialize, Default)]
+pub struct TerminalStyle {
+    pub black: Color,
+    pub red: Color,
+    pub green: Color,
+    pub yellow: Color,
+    pub blue: Color,
+    pub magenta: Color,
+    pub cyan: Color,
+    pub white: Color,
+    pub bright_black: Color,
+    pub bright_red: Color,
+    pub bright_green: Color,
+    pub bright_yellow: Color,
+    pub bright_blue: Color,
+    pub bright_magenta: Color,
+    pub bright_cyan: Color,
+    pub bright_white: Color,
+    pub foreground: Color,
+    pub background: Color,
+    pub cursor: Color,
+    pub dim_black: Color,
+    pub dim_red: Color,
+    pub dim_green: Color,
+    pub dim_yellow: Color,
+    pub dim_blue: Color,
+    pub dim_magenta: Color,
+    pub dim_cyan: Color,
+    pub dim_white: Color,
+    pub bright_foreground: Color,
+    pub dim_foreground: Color,
+}

crates/vim/Cargo.toml πŸ”—

@@ -14,6 +14,7 @@ command_palette = { path = "../command_palette" }
 editor = { path = "../editor" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
+search = { path = "../search" }
 serde = { version = "1.0", features = ["derive", "rc"] }
 settings = { path = "../settings" }
 workspace = { path = "../workspace" }

crates/vim/src/editor_events.rs πŸ”—

@@ -13,7 +13,7 @@ pub fn init(cx: &mut MutableAppContext) {
 fn editor_created(EditorCreated(editor): &EditorCreated, cx: &mut MutableAppContext) {
     cx.update_default_global(|vim: &mut Vim, cx| {
         vim.editors.insert(editor.id(), editor.downgrade());
-        vim.sync_editor_options(cx);
+        vim.sync_vim_settings(cx);
     })
 }
 
@@ -29,8 +29,17 @@ fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppCont
             }
         }));
 
-        if editor.read(cx).mode() != EditorMode::Full {
-            vim.switch_mode(Mode::Insert, cx);
+        if !vim.enabled {
+            return;
+        }
+
+        let editor = editor.read(cx);
+        if editor.selections.newest::<usize>(cx).is_empty() {
+            if editor.mode() != EditorMode::Full {
+                vim.switch_mode(Mode::Insert, cx);
+            }
+        } else {
+            vim.switch_mode(Mode::Visual { line: false }, cx);
         }
     });
 }
@@ -42,7 +51,7 @@ fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppCont
                 vim.active_editor = None;
             }
         }
-        vim.sync_editor_options(cx);
+        vim.sync_vim_settings(cx);
     })
 }
 

crates/vim/src/normal.rs πŸ”—

@@ -1165,7 +1165,7 @@ mod test {
                 The quick brown
                 fox [jump}s over
                 the lazy dog"},
-            Mode::Normal,
+            Mode::Visual { line: false },
         );
         cx.simulate_keystroke("y");
         cx.set_state(

crates/vim/src/normal/delete.rs πŸ”—

@@ -40,7 +40,7 @@ pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
 mod test {
     use indoc::indoc;
 
-    use crate::vim_test_context::VimTestContext;
+    use crate::{state::Mode, vim_test_context::VimTestContext};
 
     #[gpui::test]
     async fn test_delete_h(cx: &mut gpui::TestAppContext) {
@@ -390,4 +390,42 @@ mod test {
                 the lazy"},
         );
     }
+
+    #[gpui::test]
+    async fn test_cancel_delete_operator(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+        cx.set_state(
+            indoc! {"
+                The quick brown
+                fox ju|mps over
+                the lazy dog"},
+            Mode::Normal,
+        );
+
+        // Canceling operator twice reverts to normal mode with no active operator
+        cx.simulate_keystrokes(["d", "escape", "k"]);
+        assert_eq!(cx.active_operator(), None);
+        assert_eq!(cx.mode(), Mode::Normal);
+        cx.assert_editor_state(indoc! {"
+            The qu|ick brown
+            fox jumps over
+            the lazy dog"});
+    }
+
+    #[gpui::test]
+    async fn test_unbound_command_cancels_pending_operator(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+        cx.set_state(
+            indoc! {"
+                The quick brown
+                fox ju|mps over
+                the lazy dog"},
+            Mode::Normal,
+        );
+
+        // Canceling operator twice reverts to normal mode with no active operator
+        cx.simulate_keystrokes(["d", "y"]);
+        assert_eq!(cx.active_operator(), None);
+        assert_eq!(cx.mode(), Mode::Normal);
+    }
 }

crates/vim/src/state.rs πŸ”—

@@ -37,7 +37,14 @@ pub struct VimState {
 impl VimState {
     pub fn cursor_shape(&self) -> CursorShape {
         match self.mode {
-            Mode::Normal | Mode::Visual { .. } => CursorShape::Block,
+            Mode::Normal => {
+                if self.operator_stack.is_empty() {
+                    CursorShape::Block
+                } else {
+                    CursorShape::Underscore
+                }
+            }
+            Mode::Visual { .. } => CursorShape::Block,
             Mode::Insert => CursorShape::Bar,
         }
     }
@@ -73,20 +80,20 @@ impl VimState {
             context.set.insert("VimControl".to_string());
         }
 
-        if let Some(operator) = &self.operator_stack.last() {
-            operator.set_context(&mut context);
-        }
+        Operator::set_context(self.operator_stack.last(), &mut context);
+
         context
     }
 }
 
 impl Operator {
-    pub fn set_context(&self, context: &mut Context) {
-        let operator_context = match self {
-            Operator::Namespace(Namespace::G) => "g",
-            Operator::Change => "c",
-            Operator::Delete => "d",
-            Operator::Yank => "y",
+    pub fn set_context(operator: Option<&Operator>, context: &mut Context) {
+        let operator_context = match operator {
+            Some(Operator::Namespace(Namespace::G)) => "g",
+            Some(Operator::Change) => "c",
+            Some(Operator::Delete) => "d",
+            Some(Operator::Yank) => "y",
+            None => "none",
         }
         .to_owned();
 

crates/vim/src/vim.rs πŸ”—

@@ -11,7 +11,7 @@ mod visual;
 
 use collections::HashMap;
 use command_palette::CommandPaletteFilter;
-use editor::{Bias, CursorShape, Editor, Input};
+use editor::{Bias, Cancel, CursorShape, Editor, Input};
 use gpui::{impl_actions, MutableAppContext, Subscription, ViewContext, WeakViewHandle};
 use serde::Deserialize;
 
@@ -34,6 +34,7 @@ pub fn init(cx: &mut MutableAppContext) {
     insert::init(cx);
     motion::init(cx);
 
+    // Vim Actions
     cx.add_action(|_: &mut Workspace, &SwitchMode(mode): &SwitchMode, cx| {
         Vim::update(cx, |vim, cx| vim.switch_mode(mode, cx))
     });
@@ -42,7 +43,11 @@ pub fn init(cx: &mut MutableAppContext) {
             Vim::update(cx, |vim, cx| vim.push_operator(operator, cx))
         },
     );
+
+    // Editor Actions
     cx.add_action(|_: &mut Editor, _: &Input, cx| {
+        // If we have an unbound input with an active operator, cancel that operator. Otherwise forward
+        // the input to the editor
         if Vim::read(cx).active_operator().is_some() {
             // Defer without updating editor
             MutableAppContext::defer(cx, |cx| Vim::update(cx, |vim, cx| vim.clear_operator(cx)))
@@ -50,7 +55,25 @@ pub fn init(cx: &mut MutableAppContext) {
             cx.propagate_action()
         }
     });
+    cx.add_action(|_: &mut Editor, _: &Cancel, cx| {
+        // If we are in a non normal mode or have an active operator, swap to normal mode
+        // Otherwise forward cancel on to the editor
+        let vim = Vim::read(cx);
+        if vim.state.mode != Mode::Normal || vim.active_operator().is_some() {
+            MutableAppContext::defer(cx, |cx| {
+                Vim::update(cx, |state, cx| {
+                    state.switch_mode(Mode::Normal, cx);
+                });
+            });
+        } else {
+            cx.propagate_action();
+        }
+    });
+
+    // Sync initial settings with the rest of the app
+    Vim::update(cx, |state, cx| state.sync_vim_settings(cx));
 
+    // Any time settings change, update vim mode to match
     cx.observe_global::<Settings, _>(|cx| {
         Vim::update(cx, |state, cx| {
             state.set_enabled(cx.global::<Settings>().vim_mode, cx)
@@ -93,25 +116,62 @@ impl Vim {
     }
 
     fn switch_mode(&mut self, mode: Mode, cx: &mut MutableAppContext) {
+        let previous_mode = self.state.mode;
         self.state.mode = mode;
         self.state.operator_stack.clear();
-        self.sync_editor_options(cx);
+
+        // Sync editor settings like clip mode
+        self.sync_vim_settings(cx);
+
+        // Adjust selections
+        for editor in self.editors.values() {
+            if let Some(editor) = editor.upgrade(cx) {
+                editor.update(cx, |editor, cx| {
+                    editor.change_selections(None, cx, |s| {
+                        s.move_with(|map, selection| {
+                            // If empty selections
+                            if self.state.empty_selections_only() {
+                                let new_head = map.clip_point(selection.head(), Bias::Left);
+                                selection.collapse_to(new_head, selection.goal)
+                            } else {
+                                if matches!(mode, Mode::Visual { line: false })
+                                    && !matches!(previous_mode, Mode::Visual { .. })
+                                    && !selection.reversed
+                                    && !selection.is_empty()
+                                {
+                                    // Mode wasn't visual mode before, but is now. We need to move the end
+                                    // back by one character so that the region to be modifed stays the same
+                                    *selection.end.column_mut() =
+                                        selection.end.column().saturating_sub(1);
+                                    selection.end = map.clip_point(selection.end, Bias::Left);
+                                }
+
+                                selection.set_head(
+                                    map.clip_point(selection.head(), Bias::Left),
+                                    selection.goal,
+                                );
+                            }
+                        });
+                    })
+                })
+            }
+        }
     }
 
     fn push_operator(&mut self, operator: Operator, cx: &mut MutableAppContext) {
         self.state.operator_stack.push(operator);
-        self.sync_editor_options(cx);
+        self.sync_vim_settings(cx);
     }
 
     fn pop_operator(&mut self, cx: &mut MutableAppContext) -> Operator {
         let popped_operator = self.state.operator_stack.pop().expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config");
-        self.sync_editor_options(cx);
+        self.sync_vim_settings(cx);
         popped_operator
     }
 
     fn clear_operator(&mut self, cx: &mut MutableAppContext) {
         self.state.operator_stack.clear();
-        self.sync_editor_options(cx);
+        self.sync_vim_settings(cx);
     }
 
     fn active_operator(&self) -> Option<Operator> {
@@ -123,23 +183,24 @@ impl Vim {
             self.enabled = enabled;
             self.state = Default::default();
             if enabled {
-                self.state.mode = Mode::Normal;
+                self.switch_mode(Mode::Normal, cx);
             }
-            cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
-                if enabled {
-                    filter.filtered_namespaces.remove("vim");
-                } else {
-                    filter.filtered_namespaces.insert("vim");
-                }
-            });
-            self.sync_editor_options(cx);
+            self.sync_vim_settings(cx);
         }
     }
 
-    fn sync_editor_options(&self, cx: &mut MutableAppContext) {
+    fn sync_vim_settings(&self, cx: &mut MutableAppContext) {
         let state = &self.state;
         let cursor_shape = state.cursor_shape();
 
+        cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
+            if self.enabled {
+                filter.filtered_namespaces.remove("vim");
+            } else {
+                filter.filtered_namespaces.insert("vim");
+            }
+        });
+
         for editor in self.editors.values() {
             if let Some(editor) = editor.upgrade(cx) {
                 editor.update(cx, |editor, cx| {
@@ -151,17 +212,6 @@ impl Vim {
                             matches!(state.mode, Mode::Visual { line: true });
                         let context_layer = state.keymap_context_layer();
                         editor.set_keymap_context_layer::<Self>(context_layer);
-                        editor.change_selections(None, cx, |s| {
-                            s.move_with(|map, selection| {
-                                selection.set_head(
-                                    map.clip_point(selection.head(), Bias::Left),
-                                    selection.goal,
-                                );
-                                if state.empty_selections_only() {
-                                    selection.collapse_to(selection.head(), selection.goal)
-                                }
-                            });
-                        })
                     } else {
                         editor.set_cursor_shape(CursorShape::Bar, cx);
                         editor.set_clip_at_line_ends(false, cx);
@@ -177,6 +227,9 @@ impl Vim {
 
 #[cfg(test)]
 mod test {
+    use indoc::indoc;
+    use search::BufferSearchBar;
+
     use crate::{state::Mode, vim_test_context::VimTestContext};
 
     #[gpui::test]
@@ -221,4 +274,34 @@ mod test {
         cx.enable_vim();
         assert_eq!(cx.mode(), Mode::Normal);
     }
+
+    #[gpui::test]
+    async fn test_buffer_search_switches_mode(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        cx.set_state(
+            indoc! {"
+            The quick brown
+            fox ju|mps over
+            the lazy dog"},
+            Mode::Normal,
+        );
+        cx.simulate_keystroke("/");
+
+        assert_eq!(cx.mode(), Mode::Visual { line: false });
+
+        let search_bar = cx.workspace(|workspace, cx| {
+            workspace
+                .active_pane()
+                .read(cx)
+                .toolbar()
+                .read(cx)
+                .item_of_type::<BufferSearchBar>()
+                .expect("Buffer search bar should be deployed")
+        });
+
+        search_bar.read_with(cx.cx, |bar, cx| {
+            assert_eq!(bar.query_editor.read(cx).text(cx), "jumps");
+        })
+    }
 }

crates/vim/src/vim_test_context.rs πŸ”—

@@ -1,14 +1,16 @@
 use std::ops::{Deref, DerefMut};
 
 use editor::test::EditorTestContext;
-use gpui::json::json;
+use gpui::{json::json, AppContext, ViewHandle};
 use project::Project;
+use search::{BufferSearchBar, ProjectSearchBar};
 use workspace::{pane, AppState, WorkspaceHandle};
 
 use crate::{state::Operator, *};
 
 pub struct VimTestContext<'a> {
     cx: EditorTestContext<'a>,
+    workspace: ViewHandle<Workspace>,
 }
 
 impl<'a> VimTestContext<'a> {
@@ -16,6 +18,7 @@ impl<'a> VimTestContext<'a> {
         cx.update(|cx| {
             editor::init(cx);
             pane::init(cx);
+            search::init(cx);
             crate::init(cx);
 
             settings::KeymapFileContent::load("keymaps/vim.json", cx).unwrap();
@@ -37,6 +40,19 @@ impl<'a> VimTestContext<'a> {
             .await;
 
         let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+
+        // Setup search toolbars
+        workspace.update(cx, |workspace, cx| {
+            workspace.active_pane().update(cx, |pane, cx| {
+                pane.toolbar().update(cx, |toolbar, cx| {
+                    let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx));
+                    toolbar.add_item(buffer_search_bar, cx);
+                    let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
+                    toolbar.add_item(project_search_bar, cx);
+                })
+            });
+        });
+
         project
             .update(cx, |project, cx| {
                 project.find_or_create_local_worktree("/root", true, cx)
@@ -64,9 +80,17 @@ impl<'a> VimTestContext<'a> {
                 window_id,
                 editor,
             },
+            workspace,
         }
     }
 
+    pub fn workspace<F, T>(&mut self, read: F) -> T
+    where
+        F: FnOnce(&Workspace, &AppContext) -> T,
+    {
+        self.workspace.read_with(self.cx.cx, read)
+    }
+
     pub fn enable_vim(&mut self) {
         self.cx.update(|cx| {
             cx.update_global(|settings: &mut Settings, _| {

crates/workspace/src/pane.rs πŸ”—

@@ -18,11 +18,15 @@ use settings::Settings;
 use std::{any::Any, cell::RefCell, mem, path::Path, rc::Rc};
 use util::ResultExt;
 
+#[derive(Clone, Deserialize, PartialEq)]
+pub struct ActivateItem(pub usize);
+
 actions!(
     pane,
     [
         ActivatePrevItem,
         ActivateNextItem,
+        ActivateLastItem,
         CloseActiveItem,
         CloseInactiveItems,
         ReopenClosedItem,
@@ -39,9 +43,6 @@ pub struct CloseItem {
     pub pane: WeakViewHandle<Pane>,
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
-pub struct ActivateItem(pub usize);
-
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct GoBack {
     #[serde(skip_deserializing)]
@@ -54,8 +55,8 @@ pub struct GoForward {
     pub pane: Option<WeakViewHandle<Pane>>,
 }
 
-impl_actions!(pane, [GoBack, GoForward]);
-impl_internal_actions!(pane, [CloseItem, ActivateItem]);
+impl_actions!(pane, [GoBack, GoForward, ActivateItem]);
+impl_internal_actions!(pane, [CloseItem]);
 
 const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 
@@ -63,6 +64,9 @@ pub fn init(cx: &mut MutableAppContext) {
     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(cx);
     });

crates/workspace/src/sidebar.rs πŸ”—

@@ -55,7 +55,8 @@ impl Into<AnyViewHandle> for &dyn SidebarItemHandle {
 pub struct Sidebar {
     side: Side,
     items: Vec<Item>,
-    active_item_ix: Option<usize>,
+    is_open: bool,
+    active_item_ix: usize,
     actual_width: Rc<RefCell<f32>>,
     custom_width: Rc<RefCell<f32>>,
 }
@@ -83,25 +84,41 @@ pub struct ToggleSidebarItem {
     pub item_index: usize,
 }
 
-#[derive(Clone, Debug, Deserialize, PartialEq)]
-pub struct ToggleSidebarItemFocus {
-    pub side: Side,
-    pub item_index: usize,
-}
-
-impl_actions!(workspace, [ToggleSidebarItem, ToggleSidebarItemFocus]);
+impl_actions!(workspace, [ToggleSidebarItem]);
 
 impl Sidebar {
     pub fn new(side: Side) -> Self {
         Self {
             side,
             items: Default::default(),
-            active_item_ix: None,
+            active_item_ix: 0,
+            is_open: false,
             actual_width: Rc::new(RefCell::new(260.)),
             custom_width: Rc::new(RefCell::new(260.)),
         }
     }
 
+    pub fn is_open(&self) -> bool {
+        self.is_open
+    }
+
+    pub fn active_item_ix(&self) -> usize {
+        self.active_item_ix
+    }
+
+    pub fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) {
+        if open != self.is_open {
+            self.is_open = open;
+            cx.notify();
+        }
+    }
+
+    pub fn toggle_open(&mut self, cx: &mut ViewContext<Self>) {
+        if self.is_open {}
+        self.is_open = !self.is_open;
+        cx.notify();
+    }
+
     pub fn add_item<T: SidebarItem>(
         &mut self,
         icon_path: &'static str,
@@ -133,23 +150,25 @@ impl Sidebar {
     }
 
     pub fn activate_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
-        self.active_item_ix = Some(item_ix);
+        self.active_item_ix = item_ix;
         cx.notify();
     }
 
     pub fn toggle_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
-        if self.active_item_ix == Some(item_ix) {
-            self.active_item_ix = None;
+        if self.active_item_ix == item_ix {
+            self.is_open = false;
         } else {
-            self.active_item_ix = Some(item_ix);
+            self.active_item_ix = item_ix;
         }
         cx.notify();
     }
 
     pub fn active_item(&self) -> Option<&Rc<dyn SidebarItemHandle>> {
-        self.active_item_ix
-            .and_then(|ix| self.items.get(ix))
-            .map(|item| &item.view)
+        if self.is_open {
+            self.items.get(self.active_item_ix).map(|item| &item.view)
+        } else {
+            None
+        }
     }
 
     fn render_resize_handle(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
@@ -249,6 +268,7 @@ impl View for SidebarButtons {
         let item_style = theme.item;
         let badge_style = theme.badge;
         let active_ix = sidebar.active_item_ix;
+        let is_open = sidebar.is_open;
         let side = sidebar.side;
         let group_style = match side {
             Side::Left => theme.group_left,
@@ -267,7 +287,7 @@ impl View for SidebarButtons {
                         item_index: ix,
                     };
                     MouseEventHandler::new::<Self, _, _>(ix, cx, move |state, cx| {
-                        let is_active = Some(ix) == active_ix;
+                        let is_active = is_open && ix == active_ix;
                         let style = item_style.style_for(state, is_active);
                         Stack::new()
                             .with_child(Svg::new(icon_path).with_color(style.icon_color).boxed())

crates/workspace/src/waiting_room.rs πŸ”—

@@ -1,7 +1,4 @@
-use crate::{
-    sidebar::{Side, ToggleSidebarItem},
-    AppState, ToggleFollow, Workspace,
-};
+use crate::{sidebar::Side, AppState, ToggleFollow, Workspace};
 use anyhow::Result;
 use client::{proto, Client, Contact};
 use gpui::{
@@ -104,13 +101,7 @@ impl WaitingRoom {
                                             &app_state,
                                             cx,
                                         );
-                                        workspace.toggle_sidebar_item(
-                                            &ToggleSidebarItem {
-                                                side: Side::Left,
-                                                item_index: 0,
-                                            },
-                                            cx,
-                                        );
+                                        workspace.toggle_sidebar(Side::Left, cx);
                                         if let Some((host_peer_id, _)) =
                                             workspace.project.read(cx).collaborators().iter().find(
                                                 |(_, collaborator)| collaborator.replica_id == 0,

crates/workspace/src/workspace.rs πŸ”—

@@ -31,7 +31,7 @@ use postage::prelude::Stream;
 use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
 use serde::Deserialize;
 use settings::Settings;
-use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem, ToggleSidebarItemFocus};
+use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem};
 use smallvec::SmallVec;
 use status_bar::StatusBar;
 pub use status_bar::StatusItemView;
@@ -90,6 +90,8 @@ actions!(
         ActivatePreviousPane,
         ActivateNextPane,
         FollowNextCollaborator,
+        ToggleLeftSidebar,
+        ToggleRightSidebar,
     ]
 );
 
@@ -104,6 +106,9 @@ pub struct ToggleProjectOnline {
     pub project: Option<ModelHandle<Project>>,
 }
 
+#[derive(Clone, Deserialize, PartialEq)]
+pub struct ActivatePane(pub usize);
+
 #[derive(Clone, PartialEq)]
 pub struct ToggleFollow(pub PeerId);
 
@@ -122,7 +127,7 @@ impl_internal_actions!(
         RemoveWorktreeFromProject
     ]
 );
-impl_actions!(workspace, [ToggleProjectOnline]);
+impl_actions!(workspace, [ToggleProjectOnline, ActivatePane]);
 
 pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
     pane::init(cx);
@@ -185,7 +190,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
         },
     );
     cx.add_action(Workspace::toggle_sidebar_item);
-    cx.add_action(Workspace::toggle_sidebar_item_focus);
     cx.add_action(Workspace::focus_center);
     cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
         workspace.activate_previous_pane(cx)
@@ -193,6 +197,13 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
     cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
         workspace.activate_next_pane(cx)
     });
+    cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftSidebar, cx| {
+        workspace.toggle_sidebar(Side::Left, cx);
+    });
+    cx.add_action(|workspace: &mut Workspace, _: &ToggleRightSidebar, cx| {
+        workspace.toggle_sidebar(Side::Right, cx);
+    });
+    cx.add_action(Workspace::activate_pane_at_index);
 
     let client = &app_state.client;
     client.add_view_request_handler(Workspace::handle_follow);
@@ -1248,17 +1259,39 @@ impl Workspace {
         }
     }
 
+    pub fn toggle_sidebar(&mut self, side: Side, cx: &mut ViewContext<Self>) {
+        let sidebar = match side {
+            Side::Left => &mut self.left_sidebar,
+            Side::Right => &mut self.right_sidebar,
+        };
+        sidebar.update(cx, |sidebar, cx| {
+            sidebar.set_open(!sidebar.is_open(), cx);
+        });
+        cx.focus_self();
+        cx.notify();
+    }
+
     pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
         let sidebar = match action.side {
             Side::Left => &mut self.left_sidebar,
             Side::Right => &mut self.right_sidebar,
         };
         let active_item = sidebar.update(cx, |sidebar, cx| {
-            sidebar.toggle_item(action.item_index, cx);
-            sidebar.active_item().map(|item| item.to_any())
+            if sidebar.is_open() && sidebar.active_item_ix() == action.item_index {
+                sidebar.set_open(false, cx);
+                None
+            } else {
+                sidebar.set_open(true, cx);
+                sidebar.activate_item(action.item_index, cx);
+                sidebar.active_item().cloned()
+            }
         });
         if let Some(active_item) = active_item {
-            cx.focus(active_item);
+            if active_item.is_focused(cx) {
+                cx.focus_self();
+            } else {
+                cx.focus(active_item.to_any());
+            }
         } else {
             cx.focus_self();
         }
@@ -1267,15 +1300,17 @@ impl Workspace {
 
     pub fn toggle_sidebar_item_focus(
         &mut self,
-        action: &ToggleSidebarItemFocus,
+        side: Side,
+        item_index: usize,
         cx: &mut ViewContext<Self>,
     ) {
-        let sidebar = match action.side {
+        let sidebar = match side {
             Side::Left => &mut self.left_sidebar,
             Side::Right => &mut self.right_sidebar,
         };
         let active_item = sidebar.update(cx, |sidebar, cx| {
-            sidebar.activate_item(action.item_index, cx);
+            sidebar.set_open(true, cx);
+            sidebar.activate_item(item_index, cx);
             sidebar.active_item().cloned()
         });
         if let Some(active_item) = active_item {
@@ -1405,6 +1440,15 @@ impl Workspace {
         }
     }
 
+    fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
+        let panes = self.center.panes();
+        if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
+            self.activate_pane(pane, cx);
+        } else {
+            self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
+        }
+    }
+
     pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
         let next_pane = {
             let panes = self.center.panes();
@@ -2481,13 +2525,7 @@ pub fn open_paths(
                 let mut workspace = Workspace::new(project, cx);
                 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
                 if contains_directory {
-                    workspace.toggle_sidebar_item(
-                        &ToggleSidebarItem {
-                            side: Side::Left,
-                            item_index: 0,
-                        },
-                        cx,
-                    );
+                    workspace.toggle_sidebar(Side::Left, cx);
                 }
                 workspace
             })

crates/zed/Cargo.toml πŸ”—

@@ -15,6 +15,7 @@ name = "Zed"
 path = "src/main.rs"
 
 [dependencies]
+activity_indicator = { path = "../activity_indicator" }
 assets = { path = "../assets" }
 auto_update = { path = "../auto_update" }
 breadcrumbs = { path = "../breadcrumbs" }
@@ -37,7 +38,6 @@ gpui = { path = "../gpui" }
 journal = { path = "../journal" }
 language = { path = "../language" }
 lsp = { path = "../lsp" }
-lsp_status = { path = "../lsp_status" }
 outline = { path = "../outline" }
 project = { path = "../project" }
 project_panel = { path = "../project_panel" }
@@ -46,6 +46,7 @@ rpc = { path = "../rpc" }
 settings = { path = "../settings" }
 sum_tree = { path = "../sum_tree" }
 text = { path = "../text" }
+terminal = { path = "../terminal" }
 theme = { path = "../theme" }
 theme_selector = { path = "../theme_selector" }
 util = { path = "../util" }

crates/zed/src/main.rs πŸ”—

@@ -36,6 +36,7 @@ use std::{
     thread,
     time::Duration,
 };
+use terminal;
 use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
 use util::{ResultExt, TryFutureExt};
 use workspace::{self, AppState, NewFile, OpenPaths};
@@ -181,6 +182,7 @@ fn main() {
         diagnostics::init(cx);
         search::init(cx);
         vim::init(cx);
+        terminal::init(cx);
 
         let db = cx.background().block(db);
         let (settings_file, keymap_file) = cx.background().block(config_files).unwrap();

crates/zed/src/menus.rs πŸ”—

@@ -187,11 +187,42 @@ pub fn menus() -> Vec<Menu<'static>> {
                 },
                 MenuItem::Separator,
                 MenuItem::Action {
-                    name: "Project Browser",
-                    action: Box::new(workspace::sidebar::ToggleSidebarItemFocus {
-                        side: workspace::sidebar::Side::Left,
-                        item_index: 0,
-                    }),
+                    name: "Toggle Left Sidebar",
+                    action: Box::new(workspace::ToggleLeftSidebar),
+                },
+                MenuItem::Action {
+                    name: "Toggle Right Sidebar",
+                    action: Box::new(workspace::ToggleRightSidebar),
+                },
+                MenuItem::Submenu(Menu {
+                    name: "Editor Layout",
+                    items: vec![
+                        MenuItem::Action {
+                            name: "Split Up",
+                            action: Box::new(workspace::SplitUp),
+                        },
+                        MenuItem::Action {
+                            name: "Split Down",
+                            action: Box::new(workspace::SplitDown),
+                        },
+                        MenuItem::Action {
+                            name: "Split Left",
+                            action: Box::new(workspace::SplitLeft),
+                        },
+                        MenuItem::Action {
+                            name: "Split Right",
+                            action: Box::new(workspace::SplitRight),
+                        },
+                    ],
+                }),
+                MenuItem::Separator,
+                MenuItem::Action {
+                    name: "Project Panel",
+                    action: Box::new(project_panel::Toggle),
+                },
+                MenuItem::Action {
+                    name: "Contacts Panel",
+                    action: Box::new(contacts_panel::Toggle),
                 },
                 MenuItem::Action {
                     name: "Command Palette",

crates/zed/src/zed.rs πŸ”—

@@ -34,7 +34,7 @@ use std::{
 };
 use util::ResultExt;
 pub use workspace;
-use workspace::{AppState, Workspace};
+use workspace::{sidebar::Side, AppState, Workspace};
 
 #[derive(Deserialize, Clone, PartialEq)]
 struct OpenBrowser {
@@ -97,6 +97,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
     cx.add_action({
         let app_state = app_state.clone();
         move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
+            println!("open settings");
             open_config_file(&SETTINGS_PATH, app_state.clone(), cx);
         }
     });
@@ -128,8 +129,18 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
             }
         },
     );
+    cx.add_action(
+        |workspace: &mut Workspace, _: &project_panel::Toggle, cx: &mut ViewContext<Workspace>| {
+            workspace.toggle_sidebar_item_focus(Side::Left, 0, cx);
+        },
+    );
+    cx.add_action(
+        |workspace: &mut Workspace, _: &contacts_panel::Toggle, cx: &mut ViewContext<Workspace>| {
+            workspace.toggle_sidebar_item_focus(Side::Right, 0, cx);
+        },
+    );
 
-    lsp_status::init(cx);
+    activity_indicator::init(cx);
     settings::KeymapFileContent::load_defaults(cx);
 }
 
@@ -212,15 +223,14 @@ pub fn initialize_workspace(
 
     let diagnostic_summary =
         cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx));
-    let lsp_status = lsp_status::LspStatusItem::new(workspace, app_state.languages.clone(), cx);
+    let activity_indicator =
+        activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
     let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
-    let auto_update = cx.add_view(|cx| auto_update::AutoUpdateIndicator::new(cx));
     let feedback_link = cx.add_view(|_| feedback::FeedbackLink);
     workspace.status_bar().update(cx, |status_bar, cx| {
         status_bar.add_left_item(diagnostic_summary, cx);
-        status_bar.add_left_item(lsp_status, cx);
+        status_bar.add_left_item(activity_indicator, cx);
         status_bar.add_right_item(cursor_position, cx);
-        status_bar.add_right_item(auto_update, cx);
         status_bar.add_right_item(feedback_link, cx);
     });
 
@@ -429,7 +439,7 @@ mod tests {
         let workspace_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
         workspace_1.update(cx, |workspace, cx| {
             assert_eq!(workspace.worktrees(cx).count(), 2);
-            assert!(workspace.left_sidebar().read(cx).active_item().is_some());
+            assert!(workspace.left_sidebar().read(cx).is_open());
             assert!(workspace.active_pane().is_focused(cx));
         });
 

styles/src/styleTree/app.ts πŸ”—

@@ -14,6 +14,7 @@ import projectDiagnostics from "./projectDiagnostics";
 import contactNotification from "./contactNotification";
 import updateNotification from "./updateNotification";
 import tooltip from "./tooltip";
+import terminal from "./terminal";
 
 export const panel = {
   padding: { top: 12, bottom: 12 },
@@ -41,5 +42,6 @@ export default function app(theme: Theme): Object {
     contactNotification: contactNotification(theme),
     updateNotification: updateNotification(theme),
     tooltip: tooltip(theme),
+    terminal: terminal(theme),
   };
 }

styles/src/styleTree/terminal.ts πŸ”—

@@ -0,0 +1,35 @@
+import Theme from "../themes/common/theme";
+
+export default function terminal(theme: Theme) {
+  return {
+    black: theme.ramps.neutral(0).hex(),
+    red: theme.ramps.red(0.5).hex(),
+    green: theme.ramps.green(0.5).hex(),
+    yellow: theme.ramps.yellow(0.5).hex(),
+    blue: theme.ramps.blue(0.5).hex(),
+    magenta: theme.ramps.magenta(0.5).hex(),
+    cyan: theme.ramps.cyan(0.5).hex(),
+    white: theme.ramps.neutral(7).hex(),
+    brightBlack: theme.ramps.neutral(2).hex(),
+    brightRed: theme.ramps.red(0.25).hex(),
+    brightGreen: theme.ramps.green(0.25).hex(),
+    brightYellow: theme.ramps.yellow(0.25).hex(),
+    brightBlue: theme.ramps.blue(0.25).hex(),
+    brightMagenta: theme.ramps.magenta(0.25).hex(),
+    brightCyan: theme.ramps.cyan(0.25).hex(),
+    brightWhite: theme.ramps.neutral(7).hex(),
+    foreground: theme.ramps.neutral(7).hex(),
+    background: theme.ramps.neutral(0).hex(),
+    cursor: theme.ramps.neutral(7).hex(),
+    dimBlack: theme.ramps.neutral(7).hex(),
+    dimRed: theme.ramps.red(0.75).hex(),
+    dimGreen: theme.ramps.green(0.75).hex(),
+    dimYellow: theme.ramps.yellow(0.75).hex(),
+    dimBlue: theme.ramps.blue(0.75).hex(),
+    dimMagenta: theme.ramps.magenta(0.75).hex(),
+    dimCyan: theme.ramps.cyan(0.75).hex(),
+    dimWhite: theme.ramps.neutral(5).hex(),
+    brightForeground: theme.ramps.neutral(7).hex(),
+    dimForeground: theme.ramps.neutral(0).hex(),
+  };
+}

styles/src/themes/cave.ts πŸ”—

@@ -25,4 +25,4 @@ const ramps = {
 };
 
 export const dark = createTheme(`${name}-dark`, false, ramps);
-export const light = createTheme(`${name}-light`, true, ramps);
+export const light = createTheme(`${name}-light`, true, ramps);

styles/src/themes/common/base16.ts πŸ”—

@@ -13,15 +13,25 @@ export function colorRamp(color: Color): Scale {
 export function createTheme(
   name: string,
   isLight: boolean,
-  ramps: { [rampName: string]: Scale },
+  color_ramps: { [rampName: string]: Scale },
 ): Theme {
+  let ramps: typeof color_ramps = {};
+  // Chromajs mutates the underlying ramp when you call domain. This causes problems because
+  // we now store the ramps object in the theme so that we can pull colors out of them. 
+  // So instead of calling domain and storing the result, we have to construct new ramps for each
+  // theme so that we don't modify the passed in ramps.
+  // This combined with an error in the type definitions for chroma js means we have to cast the colors
+  // function to any in order to get the colors back out from the original ramps.
   if (isLight) {
-    for (var rampName in ramps) {
-      ramps[rampName] = ramps[rampName].domain([1, 0]);
+    for (var rampName in color_ramps) {
+      ramps[rampName] = chroma.scale((color_ramps[rampName].colors as any)()).domain([1, 0]);
     }
-    ramps.neutral = ramps.neutral.domain([7, 0]);
+    ramps.neutral = chroma.scale((color_ramps.neutral.colors as any)()).domain([7, 0]);
   } else {
-    ramps.neutral = ramps.neutral.domain([0, 7]);
+    for (var rampName in color_ramps) {
+      ramps[rampName] = chroma.scale((color_ramps[rampName].colors as any)()).domain([0, 1]);
+    }
+    ramps.neutral = chroma.scale((color_ramps.neutral.colors as any)()).domain([0, 7]);
   }
 
   let blend = isLight ? 0.12 : 0.24;
@@ -237,6 +247,7 @@ export function createTheme(
 
   return {
     name,
+    isLight,
     backgroundColor,
     borderColor,
     textColor,
@@ -245,5 +256,6 @@ export function createTheme(
     syntax,
     player,
     shadow,
+    ramps,
   };
 }

styles/src/themes/common/theme.ts πŸ”—

@@ -1,3 +1,4 @@
+import { Scale } from "chroma-js";
 import { FontWeight } from "../../common";
 import { withOpacity } from "../../utils/color";
 
@@ -60,6 +61,7 @@ export interface Syntax {
 
 export default interface Theme {
   name: string;
+  isLight: boolean,
   backgroundColor: {
     // Basically just Title Bar
     // Lowest background level
@@ -155,4 +157,5 @@ export default interface Theme {
     8: Player;
   },
   shadow: string;
+  ramps: { [rampName: string]: Scale };
 }