Merge branch 'main' into piotr/z-2556-add-create-branch-button

Piotr Osiewicz created

Change summary

.github/workflows/release_actions.yml                   |   4 
Cargo.lock                                              |  89 +--
Cargo.toml                                              |   4 
assets/icons/radix/maximize.svg                         |   4 
assets/icons/radix/minimize.svg                         |   4 
assets/keymaps/default.json                             |   1 
assets/keymaps/textmate.json                            |   1 
assets/keymaps/vim.json                                 |  18 
crates/ai/src/assistant.rs                              |   2 
crates/call/src/call.rs                                 |  43 +
crates/cli/src/main.rs                                  |   1 
crates/client/src/telemetry.rs                          |   4 
crates/collab/src/db.rs                                 |   5 
crates/collab/src/tests/integration_tests.rs            |   6 
crates/collab/src/tests/randomized_integration_tests.rs |   6 
crates/collab_ui/Cargo.toml                             |   1 
crates/collab_ui/src/collab_titlebar_item.rs            |  27 
crates/collab_ui/src/collab_ui.rs                       |  18 
crates/collab_ui/src/incoming_call_notification.rs      |   4 
crates/command_palette/src/command_palette.rs           |   1 
crates/db/src/db.rs                                     |   5 
crates/editor/src/editor.rs                             |  59 ++
crates/editor/src/editor_tests.rs                       | 160 +++++
crates/editor/src/element.rs                            |  19 
crates/editor/src/inlay_hint_cache.rs                   |  10 
crates/editor/src/items.rs                              |   2 
crates/editor/src/movement.rs                           |  24 
crates/editor/src/test/editor_test_context.rs           |  26 
crates/language/src/language.rs                         |   6 
crates/language/src/syntax_map.rs                       |   5 
crates/live_kit_client/Cargo.toml                       |   3 
crates/live_kit_client/src/test.rs                      |   7 
crates/project/src/project.rs                           |  30 
crates/recent_projects/src/recent_projects.rs           |   2 
crates/search/src/project_search.rs                     |   3 
crates/terminal_view/src/terminal_panel.rs              |   8 
crates/terminal_view/src/terminal_view.rs               |   3 
crates/theme/src/theme.rs                               |   1 
crates/vcs_menu/Cargo.toml                              |  16 
crates/vcs_menu/src/lib.rs                              |  37 +
crates/vim/Cargo.toml                                   |   1 
crates/vim/src/motion.rs                                | 160 ++++++
crates/vim/src/normal/case.rs                           |  96 ++-
crates/vim/src/test.rs                                  |  14 
crates/vim/src/test/neovim_backed_test_context.rs       |  51 +
crates/vim/src/test/neovim_connection.rs                |  22 
crates/vim/src/test/vim_test_context.rs                 |   2 
crates/vim/src/vim.rs                                   |  23 
crates/vim/test_data/test_change_case.json              |  18 
crates/vim/test_data/test_matching.json                 |  17 
crates/vim/test_data/test_start_end_of_paragraph.json   |  13 
crates/workspace/src/item.rs                            |   2 
crates/workspace/src/pane.rs                            |   1 
crates/zed/src/main.rs                                  |   9 
crates/zed/src/only_instance.rs                         | 103 +++
crates/zed/src/zed.rs                                   |   1 
styles/src/component/tab_bar_button.ts                  |  55 ++
styles/src/style_tree/assistant.ts                      | 287 +++-------
styles/src/style_tree/titlebar.ts                       |   3 
styles/src/theme/create_theme.ts                        |   9 
60 files changed, 1,114 insertions(+), 442 deletions(-)

Detailed changes

.github/workflows/release_actions.yml 🔗

@@ -16,8 +16,4 @@ jobs:
 
           Restart your Zed or head to https://zed.dev/releases/stable/latest to grab it.
 
-          ```md
-          # Changelog
-
           ${{ github.event.release.body }}
-          ```

Cargo.lock 🔗

@@ -482,7 +482,7 @@ dependencies = [
  "async-global-executor",
  "async-io",
  "async-lock",
- "crossbeam-utils 0.8.15",
+ "crossbeam-utils",
  "futures-channel",
  "futures-core",
  "futures-io",
@@ -1491,6 +1491,7 @@ dependencies = [
  "theme",
  "theme_selector",
  "util",
+ "vcs_menu",
  "workspace",
  "zed-actions",
 ]
@@ -1550,7 +1551,7 @@ version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c"
 dependencies = [
- "crossbeam-utils 0.8.15",
+ "crossbeam-utils",
 ]
 
 [[package]]
@@ -1863,16 +1864,6 @@ dependencies = [
  "cfg-if 1.0.0",
 ]
 
-[[package]]
-name = "crossbeam-channel"
-version = "0.4.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87"
-dependencies = [
- "crossbeam-utils 0.7.2",
- "maybe-uninit",
-]
-
 [[package]]
 name = "crossbeam-channel"
 version = "0.5.8"
@@ -1880,7 +1871,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
 dependencies = [
  "cfg-if 1.0.0",
- "crossbeam-utils 0.8.15",
+ "crossbeam-utils",
 ]
 
 [[package]]
@@ -1891,7 +1882,7 @@ checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
 dependencies = [
  "cfg-if 1.0.0",
  "crossbeam-epoch",
- "crossbeam-utils 0.8.15",
+ "crossbeam-utils",
 ]
 
 [[package]]
@@ -1902,7 +1893,7 @@ checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695"
 dependencies = [
  "autocfg 1.1.0",
  "cfg-if 1.0.0",
- "crossbeam-utils 0.8.15",
+ "crossbeam-utils",
  "memoffset 0.8.0",
  "scopeguard",
 ]
@@ -1914,18 +1905,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
 dependencies = [
  "cfg-if 1.0.0",
- "crossbeam-utils 0.8.15",
-]
-
-[[package]]
-name = "crossbeam-utils"
-version = "0.7.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
-dependencies = [
- "autocfg 1.1.0",
- "cfg-if 0.1.10",
- "lazy_static",
+ "crossbeam-utils",
 ]
 
 [[package]]
@@ -1990,7 +1970,6 @@ checksum = "14d05c10f541ae6f3bc5b3d923c20001f47db7d5f0b2bc6ad16490133842db79"
 dependencies = [
  "cc",
  "libc",
- "libnghttp2-sys",
  "libz-sys",
  "openssl-sys",
  "pkg-config",
@@ -3521,12 +3500,12 @@ dependencies = [
 
 [[package]]
 name = "ipc-channel"
-version = "0.16.0"
+version = "0.16.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cb1d9211085f0ea6f1379d944b93c4d07e8207aa3bcf49f37eda12b85081887"
+checksum = "342d636452fbc2895574e0b319b23c014fd01c9ed71dcd87f6a4a8e2f948db4b"
 dependencies = [
  "bincode",
- "crossbeam-channel 0.4.4",
+ "crossbeam-channel",
  "fnv",
  "lazy_static",
  "libc",
@@ -3534,7 +3513,7 @@ dependencies = [
  "rand 0.7.3",
  "serde",
  "tempfile",
- "uuid 0.8.2",
+ "uuid 1.3.2",
  "winapi 0.3.9",
 ]
 
@@ -3576,7 +3555,7 @@ checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9"
 dependencies = [
  "async-channel",
  "castaway",
- "crossbeam-utils 0.8.15",
+ "crossbeam-utils",
  "curl",
  "curl-sys",
  "encoding_rs",
@@ -3906,16 +3885,6 @@ version = "0.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb"
 
-[[package]]
-name = "libnghttp2-sys"
-version = "0.1.7+1.45.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57ed28aba195b38d5ff02b9170cbff627e336a20925e43b4945390401c5dc93f"
-dependencies = [
- "cc",
- "libc",
-]
-
 [[package]]
 name = "libsqlite3-sys"
 version = "0.24.2"
@@ -4004,7 +3973,6 @@ dependencies = [
  "gpui",
  "hmac 0.12.1",
  "jwt",
- "lazy_static",
  "live_kit_server",
  "log",
  "media",
@@ -4149,12 +4117,6 @@ version = "0.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4"
 
-[[package]]
-name = "maybe-uninit"
-version = "2.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
-
 [[package]]
 name = "md-5"
 version = "0.10.5"
@@ -5678,9 +5640,9 @@ version = "1.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
 dependencies = [
- "crossbeam-channel 0.5.8",
+ "crossbeam-channel",
  "crossbeam-deque",
- "crossbeam-utils 0.8.15",
+ "crossbeam-utils",
  "num_cpus",
 ]
 
@@ -8333,15 +8295,6 @@ version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bcc7e3b898aa6f6c08e5295b6c89258d1331e9ac578cc992fb818759951bdc22"
 
-[[package]]
-name = "uuid"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
-dependencies = [
- "getrandom 0.2.9",
-]
-
 [[package]]
 name = "uuid"
 version = "1.3.2"
@@ -8378,6 +8331,19 @@ version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
 
+[[package]]
+name = "vcs_menu"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "fuzzy",
+ "gpui",
+ "picker",
+ "theme",
+ "util",
+ "workspace",
+]
+
 [[package]]
 name = "version_check"
 version = "0.9.4"
@@ -8398,7 +8364,6 @@ dependencies = [
  "indoc",
  "itertools",
  "language",
- "lazy_static",
  "log",
  "nvim-rs",
  "parking_lot 0.11.2",

Cargo.toml 🔗

@@ -64,6 +64,7 @@ members = [
     "crates/theme_selector",
     "crates/util",
     "crates/vim",
+    "crates/vcs_menu",
     "crates/workspace",
     "crates/welcome",
     "crates/xtask",
@@ -81,7 +82,8 @@ env_logger = { version = "0.9" }
 futures = { version = "0.3" }
 globset = { version = "0.4" }
 indoc = "1"
-isahc = "1.7.2"
+# We explicitly disable a http2 support in isahc.
+isahc = { version = "1.7.2", default-features = false, features = ["static-curl", "text-decoding"] }
 lazy_static = { version = "1.4.0" }
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 ordered-float = { version = "2.1.1" }

assets/icons/radix/maximize.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.5 1.5H13.5M13.5 1.5V5.5M13.5 1.5C12.1332 2.86683 10.3668 4.63317 9 6" stroke="white" stroke-linecap="round"/>
+<path d="M1.5 9.5V13.5M1.5 13.5L6 9M1.5 13.5H5.5" stroke="white" stroke-linecap="round"/>
+</svg>

assets/icons/radix/minimize.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13 6L9 6M9 6L9 2M9 6C10.3668 4.63316 12.1332 2.86683 13.5 1.5" stroke="white" stroke-linecap="round"/>
+<path d="M6 13L6 9M6 9L1.5 13.5M6 9L2 9" stroke="white" stroke-linecap="round"/>
+</svg>

assets/keymaps/default.json 🔗

@@ -39,6 +39,7 @@
       "cmd-shift-n": "workspace::NewWindow",
       "cmd-o": "workspace::Open",
       "alt-cmd-o": "projects::OpenRecent",
+      "alt-cmd-b": "branches::OpenRecent",
       "ctrl-~": "workspace::NewTerminal",
       "ctrl-`": "terminal_panel::ToggleFocus",
       "shift-escape": "workspace::ToggleZoom"

assets/keymaps/textmate.json 🔗

@@ -2,6 +2,7 @@
   {
     "bindings": {
       "cmd-shift-o": "projects::OpenRecent",
+      "cmd-shift-b": "branches::OpenRecent",
       "cmd-alt-tab": "project_panel::ToggleFocus"
     }
   },

assets/keymaps/vim.json 🔗

@@ -35,8 +35,11 @@
       "l": "vim::Right",
       "right": "vim::Right",
       "$": "vim::EndOfLine",
+      "^": "vim::FirstNonWhitespace",
       "shift-g": "vim::EndOfDocument",
       "w": "vim::NextWordStart",
+      "{": "vim::StartOfParagraph",
+      "}": "vim::EndOfParagraph",
       "shift-w": [
         "vim::NextWordStart",
         {
@@ -92,7 +95,10 @@
       ],
       "ctrl-o": "pane::GoBack",
       "ctrl-]": "editor::GoToDefinition",
-      "escape": "editor::Cancel",
+      "escape": [
+        "vim::SwitchMode",
+        "Normal"
+      ],
       "0": "vim::StartOfLine", // When no number operator present, use start of line motion
       "1": [
         "vim::Number",
@@ -165,7 +171,6 @@
       "shift-a": "vim::InsertEndOfLine",
       "x": "vim::DeleteRight",
       "shift-x": "vim::DeleteLeft",
-      "^": "vim::FirstNonWhitespace",
       "o": "vim::InsertLineBelow",
       "shift-o": "vim::InsertLineAbove",
       "~": "vim::ChangeCase",
@@ -305,6 +310,10 @@
         "vim::PushOperator",
         "Replace"
       ],
+      "ctrl-c": [
+        "vim::SwitchMode",
+        "Normal"
+      ],
       "> >": "editor::Indent",
       "< <": "editor::Outdent"
     }
@@ -321,7 +330,10 @@
     "bindings": {
       "tab": "vim::Tab",
       "enter": "vim::Enter",
-      "escape": "editor::Cancel"
+      "escape": [
+        "vim::SwitchMode",
+        "Normal"
+      ]
     }
   }
 ]

crates/ai/src/assistant.rs 🔗

@@ -2061,6 +2061,8 @@ impl ConversationEditor {
         let remaining_tokens = self.conversation.read(cx).remaining_tokens()?;
         let remaining_tokens_style = if remaining_tokens <= 0 {
             &style.no_remaining_tokens
+        } else if remaining_tokens <= 500 {
+            &style.low_remaining_tokens
         } else {
             &style.remaining_tokens
         };

crates/call/src/call.rs 🔗

@@ -4,7 +4,7 @@ pub mod room;
 use std::sync::Arc;
 
 use anyhow::{anyhow, Result};
-use client::{proto, Client, TypedEnvelope, User, UserStore};
+use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore};
 use collections::HashSet;
 use futures::{future::Shared, FutureExt};
 use postage::watch;
@@ -198,6 +198,7 @@ impl ActiveCall {
             let result = invite.await;
             this.update(&mut cx, |this, cx| {
                 this.pending_invites.remove(&called_user_id);
+                this.report_call_event("invite", cx);
                 cx.notify();
             });
             result
@@ -243,21 +244,26 @@ impl ActiveCall {
         };
 
         let join = Room::join(&call, self.client.clone(), self.user_store.clone(), cx);
+
         cx.spawn(|this, mut cx| async move {
             let room = join.await?;
             this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))
                 .await?;
+            this.update(&mut cx, |this, cx| {
+                this.report_call_event("accept incoming", cx)
+            });
             Ok(())
         })
     }
 
-    pub fn decline_incoming(&mut self) -> Result<()> {
+    pub fn decline_incoming(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
         let call = self
             .incoming_call
             .0
             .borrow_mut()
             .take()
             .ok_or_else(|| anyhow!("no incoming call"))?;
+        self.report_call_event_for_room("decline incoming", call.room_id, cx);
         self.client.send(proto::DeclineCall {
             room_id: call.room_id,
         })?;
@@ -266,6 +272,7 @@ impl ActiveCall {
 
     pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
         cx.notify();
+        self.report_call_event("hang up", cx);
         if let Some((room, _)) = self.room.take() {
             room.update(cx, |room, cx| room.leave(cx))
         } else {
@@ -273,12 +280,28 @@ impl ActiveCall {
         }
     }
 
+    pub fn toggle_screen_sharing(&self, cx: &mut AppContext) {
+        if let Some(room) = self.room().cloned() {
+            let toggle_screen_sharing = room.update(cx, |room, cx| {
+                if room.is_screen_sharing() {
+                    self.report_call_event("disable screen share", cx);
+                    Task::ready(room.unshare_screen(cx))
+                } else {
+                    self.report_call_event("enable screen share", cx);
+                    room.share_screen(cx)
+                }
+            });
+            toggle_screen_sharing.detach_and_log_err(cx);
+        }
+    }
+
     pub fn share_project(
         &mut self,
         project: ModelHandle<Project>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<u64>> {
         if let Some((room, _)) = self.room.as_ref() {
+            self.report_call_event("share project", cx);
             room.update(cx, |room, cx| room.share_project(project, cx))
         } else {
             Task::ready(Err(anyhow!("no active call")))
@@ -291,6 +314,7 @@ impl ActiveCall {
         cx: &mut ModelContext<Self>,
     ) -> Result<()> {
         if let Some((room, _)) = self.room.as_ref() {
+            self.report_call_event("unshare project", cx);
             room.update(cx, |room, cx| room.unshare_project(project, cx))
         } else {
             Err(anyhow!("no active call"))
@@ -352,4 +376,19 @@ impl ActiveCall {
     pub fn pending_invites(&self) -> &HashSet<u64> {
         &self.pending_invites
     }
+
+    fn report_call_event(&self, operation: &'static str, cx: &AppContext) {
+        if let Some(room) = self.room() {
+            self.report_call_event_for_room(operation, room.read(cx).id(), cx)
+        }
+    }
+
+    fn report_call_event_for_room(&self, operation: &'static str, room_id: u64, cx: &AppContext) {
+        let telemetry = self.client.telemetry();
+        let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
+
+        let event = ClickhouseEvent::Call { operation, room_id };
+
+        telemetry.report_clickhouse_event(event, telemetry_settings);
+    }
 }

crates/cli/src/main.rs 🔗

@@ -201,6 +201,7 @@ impl Bundle {
                     self.zed_version_string()
                 );
             }
+
             Self::LocalPath { executable, .. } => {
                 let executable_parent = executable
                     .parent()

crates/client/src/telemetry.rs 🔗

@@ -70,6 +70,10 @@ pub enum ClickhouseEvent {
         suggestion_accepted: bool,
         file_extension: Option<String>,
     },
+    Call {
+        operation: &'static str,
+        room_id: u64,
+    },
 }
 
 #[cfg(debug_assertions)]

crates/collab/src/db.rs 🔗

@@ -3517,7 +3517,6 @@ pub use test::*;
 mod test {
     use super::*;
     use gpui::executor::Background;
-    use lazy_static::lazy_static;
     use parking_lot::Mutex;
     use sea_orm::ConnectionTrait;
     use sqlx::migrate::MigrateDatabase;
@@ -3566,9 +3565,7 @@ mod test {
         }
 
         pub fn postgres(background: Arc<Background>) -> Self {
-            lazy_static! {
-                static ref LOCK: Mutex<()> = Mutex::new(());
-            }
+            static LOCK: Mutex<()> = Mutex::new(());
 
             let _guard = LOCK.lock();
             let mut rng = StdRng::from_entropy();

crates/collab/src/tests/integration_tests.rs 🔗

@@ -157,7 +157,7 @@ async fn test_basic_calls(
     // User C receives the call, but declines it.
     let call_c = incoming_call_c.next().await.unwrap().unwrap();
     assert_eq!(call_c.calling_user.github_login, "user_b");
-    active_call_c.update(cx_c, |call, _| call.decline_incoming().unwrap());
+    active_call_c.update(cx_c, |call, cx| call.decline_incoming(cx).unwrap());
     assert!(incoming_call_c.next().await.unwrap().is_none());
 
     deterministic.run_until_parked();
@@ -1080,7 +1080,7 @@ async fn test_calls_on_multiple_connections(
 
     // User B declines the call on one of the two connections, causing both connections
     // to stop ringing.
-    active_call_b2.update(cx_b2, |call, _| call.decline_incoming().unwrap());
+    active_call_b2.update(cx_b2, |call, cx| call.decline_incoming(cx).unwrap());
     deterministic.run_until_parked();
     assert!(incoming_call_b1.next().await.unwrap().is_none());
     assert!(incoming_call_b2.next().await.unwrap().is_none());
@@ -5945,7 +5945,7 @@ async fn test_contacts(
         [("user_b".to_string(), "online", "busy")]
     );
 
-    active_call_b.update(cx_b, |call, _| call.decline_incoming().unwrap());
+    active_call_b.update(cx_b, |call, cx| call.decline_incoming(cx).unwrap());
     deterministic.run_until_parked();
     assert_eq!(
         contacts(&client_a, cx_a),

crates/collab/src/tests/randomized_integration_tests.rs 🔗

@@ -37,9 +37,9 @@ use util::ResultExt;
 lazy_static::lazy_static! {
     static ref PLAN_LOAD_PATH: Option<PathBuf> = path_env_var("LOAD_PLAN");
     static ref PLAN_SAVE_PATH: Option<PathBuf> = path_env_var("SAVE_PLAN");
-    static ref LOADED_PLAN_JSON: Mutex<Option<Vec<u8>>> = Default::default();
-    static ref PLAN: Mutex<Option<Arc<Mutex<TestPlan>>>> = Default::default();
 }
+static LOADED_PLAN_JSON: Mutex<Option<Vec<u8>>> = Mutex::new(None);
+static PLAN: Mutex<Option<Arc<Mutex<TestPlan>>>> = Mutex::new(None);
 
 #[gpui::test(iterations = 100, on_failure = "on_failure")]
 async fn test_random_collaboration(
@@ -365,7 +365,7 @@ async fn apply_client_operation(
             }
 
             log::info!("{}: declining incoming call", client.username);
-            active_call.update(cx, |call, _| call.decline_incoming())?;
+            active_call.update(cx, |call, cx| call.decline_incoming(cx))?;
         }
 
         ClientOperation::LeaveCall => {

crates/collab_ui/Cargo.toml 🔗

@@ -39,6 +39,7 @@ recent_projects = {path = "../recent_projects"}
 settings = { path = "../settings" }
 theme = { path = "../theme" }
 theme_selector = { path = "../theme_selector" }
+vcs_menu = { path = "../vcs_menu" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }
 zed-actions = {path = "../zed-actions"}

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -1,8 +1,5 @@
 use crate::{
-    branch_list::{build_branch_list, BranchList},
-    contact_notification::ContactNotification,
-    contacts_popover,
-    face_pile::FacePile,
+    contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
     toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute,
     ToggleScreenSharing,
 };
@@ -27,6 +24,7 @@ use recent_projects::{build_recent_projects, RecentProjects};
 use std::{ops::Range, sync::Arc};
 use theme::{AvatarStyle, Theme};
 use util::ResultExt;
+use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
 use workspace::{FollowNextCollaborator, Workspace, WORKSPACE_DB};
 
 const MAX_PROJECT_NAME_LENGTH: usize = 40;
@@ -37,7 +35,6 @@ actions!(
     [
         ToggleContactsMenu,
         ToggleUserMenu,
-        ToggleVcsMenu,
         ToggleProjectMenu,
         SwitchBranch,
         ShareProject,
@@ -229,15 +226,23 @@ impl CollabTitlebarItem {
         let mut ret = Flex::row().with_child(
             Stack::new()
                 .with_child(
-                    MouseEventHandler::<ToggleProjectMenu, Self>::new(0, cx, |mouse_state, _| {
+                    MouseEventHandler::<ToggleProjectMenu, Self>::new(0, cx, |mouse_state, cx| {
                         let style = project_style
                             .in_state(self.project_popover.is_some())
                             .style_for(mouse_state);
+                        enum RecentProjectsTooltip {}
                         Label::new(name, style.text.clone())
                             .contained()
                             .with_style(style.container)
                             .aligned()
                             .left()
+                            .with_tooltip::<RecentProjectsTooltip>(
+                                0,
+                                "Recent projects".into(),
+                                Some(Box::new(recent_projects::OpenRecent)),
+                                theme.tooltip.clone(),
+                                cx,
+                            )
                             .into_any_named("title-project-name")
                     })
                     .with_cursor_style(CursorStyle::PointingHand)
@@ -264,7 +269,8 @@ impl CollabTitlebarItem {
                                 MouseEventHandler::<ToggleVcsMenu, Self>::new(
                                     0,
                                     cx,
-                                    |mouse_state, _| {
+                                    |mouse_state, cx| {
+                                        enum BranchPopoverTooltip {}
                                         let style = git_style
                                             .in_state(self.branch_popover.is_some())
                                             .style_for(mouse_state);
@@ -274,6 +280,13 @@ impl CollabTitlebarItem {
                                             .with_margin_right(item_spacing)
                                             .aligned()
                                             .left()
+                                            .with_tooltip::<BranchPopoverTooltip>(
+                                                0,
+                                                "Recent branches".into(),
+                                                Some(Box::new(ToggleVcsMenu)),
+                                                theme.tooltip.clone(),
+                                                cx,
+                                            )
                                             .into_any_named("title-project-branch")
                                     },
                                 )

crates/collab_ui/src/collab_ui.rs 🔗

@@ -1,4 +1,3 @@
-mod branch_list;
 mod collab_titlebar_item;
 mod contact_finder;
 mod contact_list;
@@ -12,7 +11,7 @@ mod sharing_status_indicator;
 
 use call::{ActiveCall, Room};
 pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu};
-use gpui::{actions, AppContext, Task};
+use gpui::{actions, AppContext};
 use std::sync::Arc;
 use util::ResultExt;
 use workspace::AppState;
@@ -29,7 +28,7 @@ actions!(
 );
 
 pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
-    branch_list::init(cx);
+    vcs_menu::init(cx);
     collab_titlebar_item::init(cx);
     contact_list::init(cx);
     contact_finder::init(cx);
@@ -45,16 +44,9 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
 }
 
 pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
-    if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
-        let toggle_screen_sharing = room.update(cx, |room, cx| {
-            if room.is_screen_sharing() {
-                Task::ready(room.unshare_screen(cx))
-            } else {
-                room.share_screen(cx)
-            }
-        });
-        toggle_screen_sharing.detach_and_log_err(cx);
-    }
+    ActiveCall::global(cx).update(cx, |call, cx| {
+        call.toggle_screen_sharing(cx);
+    });
 }
 
 pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {

crates/collab_ui/src/incoming_call_notification.rs 🔗

@@ -99,8 +99,8 @@ impl IncomingCallNotification {
                 })
                 .detach_and_log_err(cx);
         } else {
-            active_call.update(cx, |active_call, _| {
-                active_call.decline_incoming().log_err();
+            active_call.update(cx, |active_call, cx| {
+                active_call.decline_incoming(cx).log_err();
             });
         }
     }

crates/db/src/db.rs 🔗

@@ -41,12 +41,11 @@ const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB";
 const DB_FILE_NAME: &'static str = "db.sqlite";
 
 lazy_static::lazy_static! {
-    // !!!!!!! CHANGE BACK TO DEFAULT FALSE BEFORE SHIPPING
-    static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
-    static ref DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
+    pub static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
     pub static ref BACKUP_DB_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
     pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false);
 }
+static DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
 
 /// Open or create a database at the given directory path.
 /// This will retry a couple times if there are failures. If opening fails once, the db directory

crates/editor/src/editor.rs 🔗

@@ -5123,7 +5123,7 @@ impl Editor {
         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
             s.move_with(|map, selection| {
                 selection.collapse_to(
-                    movement::start_of_paragraph(map, selection.head()),
+                    movement::start_of_paragraph(map, selection.head(), 1),
                     SelectionGoal::None,
                 )
             });
@@ -5143,7 +5143,7 @@ impl Editor {
         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
             s.move_with(|map, selection| {
                 selection.collapse_to(
-                    movement::end_of_paragraph(map, selection.head()),
+                    movement::end_of_paragraph(map, selection.head(), 1),
                     SelectionGoal::None,
                 )
             });
@@ -5162,7 +5162,10 @@ impl Editor {
 
         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
             s.move_heads_with(|map, head, _| {
-                (movement::start_of_paragraph(map, head), SelectionGoal::None)
+                (
+                    movement::start_of_paragraph(map, head, 1),
+                    SelectionGoal::None,
+                )
             });
         })
     }
@@ -5179,7 +5182,10 @@ impl Editor {
 
         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
             s.move_heads_with(|map, head, _| {
-                (movement::end_of_paragraph(map, head), SelectionGoal::None)
+                (
+                    movement::end_of_paragraph(map, head, 1),
+                    SelectionGoal::None,
+                )
             });
         })
     }
@@ -7216,6 +7222,47 @@ impl Editor {
         }
         results
     }
+    pub fn background_highlights_in_range_for<T: 'static>(
+        &self,
+        search_range: Range<Anchor>,
+        display_snapshot: &DisplaySnapshot,
+        theme: &Theme,
+    ) -> Vec<(Range<DisplayPoint>, Color)> {
+        let mut results = Vec::new();
+        let buffer = &display_snapshot.buffer_snapshot;
+        let Some((color_fetcher, ranges)) = self.background_highlights
+            .get(&TypeId::of::<T>()) else {
+                return vec![];
+            };
+
+        let color = color_fetcher(theme);
+        let start_ix = match ranges.binary_search_by(|probe| {
+            let cmp = probe.end.cmp(&search_range.start, buffer);
+            if cmp.is_gt() {
+                Ordering::Greater
+            } else {
+                Ordering::Less
+            }
+        }) {
+            Ok(i) | Err(i) => i,
+        };
+        for range in &ranges[start_ix..] {
+            if range.start.cmp(&search_range.end, buffer).is_ge() {
+                break;
+            }
+            let start = range
+                .start
+                .to_point(buffer)
+                .to_display_point(display_snapshot);
+            let end = range
+                .end
+                .to_point(buffer)
+                .to_display_point(display_snapshot);
+            results.push((start..end, color))
+        }
+
+        results
+    }
 
     pub fn highlight_text<T: 'static>(
         &mut self,
@@ -7518,7 +7565,7 @@ impl Editor {
 
     fn report_editor_event(
         &self,
-        name: &'static str,
+        operation: &'static str,
         file_extension: Option<String>,
         cx: &AppContext,
     ) {
@@ -7555,7 +7602,7 @@ impl Editor {
         let event = ClickhouseEvent::Editor {
             file_extension,
             vim_mode,
-            operation: name,
+            operation,
             copilot_enabled,
             copilot_enabled_for_language,
         };

crates/editor/src/editor_tests.rs 🔗

@@ -22,7 +22,10 @@ use language::{
     BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegistry, Point,
 };
 use parking_lot::Mutex;
+use project::project_settings::{LspSettings, ProjectSettings};
 use project::FakeFs;
+use std::sync::atomic;
+use std::sync::atomic::AtomicUsize;
 use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
 use unindent::Unindent;
 use util::{
@@ -1796,7 +1799,7 @@ async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
     "});
     }
     // Ensure that comment continuations can be disabled.
-    update_test_settings(cx, |settings| {
+    update_test_language_settings(cx, |settings| {
         settings.defaults.extend_comment_on_newline = Some(false);
     });
     let mut cx = EditorTestContext::new(cx).await;
@@ -4546,7 +4549,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
     assert!(!cx.read(|cx| editor.is_dirty(cx)));
 
     // Set rust language override and assert overridden tabsize is sent to language server
-    update_test_settings(cx, |settings| {
+    update_test_language_settings(cx, |settings| {
         settings.languages.insert(
             "Rust".into(),
             LanguageSettingsContent {
@@ -4660,7 +4663,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
     assert!(!cx.read(|cx| editor.is_dirty(cx)));
 
     // Set rust language override and assert overridden tabsize is sent to language server
-    update_test_settings(cx, |settings| {
+    update_test_language_settings(cx, |settings| {
         settings.languages.insert(
             "Rust".into(),
             LanguageSettingsContent {
@@ -7084,6 +7087,142 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
     });
 }
 
+#[gpui::test]
+async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let language_name: Arc<str> = "Rust".into();
+    let mut language = Language::new(
+        LanguageConfig {
+            name: Arc::clone(&language_name),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+
+    let server_restarts = Arc::new(AtomicUsize::new(0));
+    let closure_restarts = Arc::clone(&server_restarts);
+    let language_server_name = "test language server";
+    let mut fake_servers = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            name: language_server_name,
+            initialization_options: Some(json!({
+                "testOptionValue": true
+            })),
+            initializer: Some(Box::new(move |fake_server| {
+                let task_restarts = Arc::clone(&closure_restarts);
+                fake_server.handle_request::<lsp::request::Shutdown, _, _>(move |_, _| {
+                    task_restarts.fetch_add(1, atomic::Ordering::Release);
+                    futures::future::ready(Ok(()))
+                });
+            })),
+            ..Default::default()
+        }))
+        .await;
+
+    let fs = FakeFs::new(cx.background());
+    fs.insert_tree(
+        "/a",
+        json!({
+            "main.rs": "fn main() { let a = 5; }",
+            "other.rs": "// Test file",
+        }),
+    )
+    .await;
+    let project = Project::test(fs, ["/a".as_ref()], cx).await;
+    project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+    let (_, _workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+    let _buffer = project
+        .update(cx, |project, cx| {
+            project.open_local_buffer("/a/main.rs", cx)
+        })
+        .await
+        .unwrap();
+    let _fake_server = fake_servers.next().await.unwrap();
+    update_test_language_settings(cx, |language_settings| {
+        language_settings.languages.insert(
+            Arc::clone(&language_name),
+            LanguageSettingsContent {
+                tab_size: NonZeroU32::new(8),
+                ..Default::default()
+            },
+        );
+    });
+    cx.foreground().run_until_parked();
+    assert_eq!(
+        server_restarts.load(atomic::Ordering::Acquire),
+        0,
+        "Should not restart LSP server on an unrelated change"
+    );
+
+    update_test_project_settings(cx, |project_settings| {
+        project_settings.lsp.insert(
+            "Some other server name".into(),
+            LspSettings {
+                initialization_options: Some(json!({
+                    "some other init value": false
+                })),
+            },
+        );
+    });
+    cx.foreground().run_until_parked();
+    assert_eq!(
+        server_restarts.load(atomic::Ordering::Acquire),
+        0,
+        "Should not restart LSP server on an unrelated LSP settings change"
+    );
+
+    update_test_project_settings(cx, |project_settings| {
+        project_settings.lsp.insert(
+            language_server_name.into(),
+            LspSettings {
+                initialization_options: Some(json!({
+                    "anotherInitValue": false
+                })),
+            },
+        );
+    });
+    cx.foreground().run_until_parked();
+    assert_eq!(
+        server_restarts.load(atomic::Ordering::Acquire),
+        1,
+        "Should restart LSP server on a related LSP settings change"
+    );
+
+    update_test_project_settings(cx, |project_settings| {
+        project_settings.lsp.insert(
+            language_server_name.into(),
+            LspSettings {
+                initialization_options: Some(json!({
+                    "anotherInitValue": false
+                })),
+            },
+        );
+    });
+    cx.foreground().run_until_parked();
+    assert_eq!(
+        server_restarts.load(atomic::Ordering::Acquire),
+        1,
+        "Should not restart LSP server on a related LSP settings change that is the same"
+    );
+
+    update_test_project_settings(cx, |project_settings| {
+        project_settings.lsp.insert(
+            language_server_name.into(),
+            LspSettings {
+                initialization_options: None,
+            },
+        );
+    });
+    cx.foreground().run_until_parked();
+    assert_eq!(
+        server_restarts.load(atomic::Ordering::Acquire),
+        2,
+        "Should restart LSP server on another related LSP settings change"
+    );
+}
+
 fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
     let point = DisplayPoint::new(row as u32, column as u32);
     point..point
@@ -7203,7 +7342,7 @@ fn handle_copilot_completion_request(
     });
 }
 
-pub(crate) fn update_test_settings(
+pub(crate) fn update_test_language_settings(
     cx: &mut TestAppContext,
     f: impl Fn(&mut AllLanguageSettingsContent),
 ) {
@@ -7214,6 +7353,17 @@ pub(crate) fn update_test_settings(
     });
 }
 
+pub(crate) fn update_test_project_settings(
+    cx: &mut TestAppContext,
+    f: impl Fn(&mut ProjectSettings),
+) {
+    cx.update(|cx| {
+        cx.update_global::<SettingsStore, _, _>(|store, cx| {
+            store.update_user_settings::<ProjectSettings>(cx, f);
+        });
+    });
+}
+
 pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
     cx.foreground().forbid_parking();
 
@@ -7227,5 +7377,5 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC
         crate::init(cx);
     });
 
-    update_test_settings(cx, f);
+    update_test_language_settings(cx, f);
 }

crates/editor/src/element.rs 🔗

@@ -1086,11 +1086,13 @@ impl EditorElement {
                         })
                     }
                 };
-                for (row, _) in &editor.background_highlights_in_range(
-                    start_anchor..end_anchor,
-                    &layout.position_map.snapshot,
-                    &theme,
-                ) {
+                for (row, _) in &editor
+                    .background_highlights_in_range_for::<crate::items::BufferSearchHighlights>(
+                        start_anchor..end_anchor,
+                        &layout.position_map.snapshot,
+                        &theme,
+                    )
+                {
                     let start_display = row.start;
                     let end_display = row.end;
 
@@ -2149,6 +2151,9 @@ impl Element<Editor> for EditorElement {
             ShowScrollbar::Auto => {
                 // Git
                 (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs())
+                ||
+                // Selections
+                (is_singleton && scrollbar_settings.selections && !highlighted_ranges.is_empty())
                 // Scrollmanager
                 || editor.scroll_manager.scrollbars_visible()
             }
@@ -2911,7 +2916,7 @@ mod tests {
     use super::*;
     use crate::{
         display_map::{BlockDisposition, BlockProperties},
-        editor_tests::{init_test, update_test_settings},
+        editor_tests::{init_test, update_test_language_settings},
         Editor, MultiBuffer,
     };
     use gpui::TestAppContext;
@@ -3108,7 +3113,7 @@ mod tests {
         let resize_step = 10.0;
         let mut editor_width = 200.0;
         while editor_width <= 1000.0 {
-            update_test_settings(cx, |s| {
+            update_test_language_settings(cx, |s| {
                 s.defaults.tab_size = NonZeroU32::new(tab_size);
                 s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
                 s.defaults.preferred_line_length = Some(editor_width as u32);

crates/editor/src/inlay_hint_cache.rs 🔗

@@ -847,7 +847,7 @@ mod tests {
     use text::Point;
     use workspace::Workspace;
 
-    use crate::editor_tests::update_test_settings;
+    use crate::editor_tests::update_test_language_settings;
 
     use super::*;
 
@@ -1476,7 +1476,7 @@ mod tests {
             ),
         ] {
             edits_made += 1;
-            update_test_settings(cx, |settings| {
+            update_test_language_settings(cx, |settings| {
                 settings.defaults.inlay_hints = Some(InlayHintSettings {
                     enabled: true,
                     show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
@@ -1520,7 +1520,7 @@ mod tests {
 
         edits_made += 1;
         let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
-        update_test_settings(cx, |settings| {
+        update_test_language_settings(cx, |settings| {
             settings.defaults.inlay_hints = Some(InlayHintSettings {
                 enabled: false,
                 show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
@@ -1577,7 +1577,7 @@ mod tests {
 
         let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
         edits_made += 1;
-        update_test_settings(cx, |settings| {
+        update_test_language_settings(cx, |settings| {
             settings.defaults.inlay_hints = Some(InlayHintSettings {
                 enabled: true,
                 show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
@@ -2269,7 +2269,7 @@ unedited (2nd) buffer should have the same hint");
             crate::init(cx);
         });
 
-        update_test_settings(cx, f);
+        update_test_language_settings(cx, f);
     }
 
     async fn prepare_test_objects(

crates/editor/src/items.rs 🔗

@@ -883,7 +883,7 @@ impl ProjectItem for Editor {
     }
 }
 
-enum BufferSearchHighlights {}
+pub(crate) enum BufferSearchHighlights {}
 impl SearchableItem for Editor {
     type Match = Range<Anchor>;
 

crates/editor/src/movement.rs 🔗

@@ -193,7 +193,11 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo
     })
 }
 
-pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
+pub fn start_of_paragraph(
+    map: &DisplaySnapshot,
+    display_point: DisplayPoint,
+    mut count: usize,
+) -> DisplayPoint {
     let point = display_point.to_point(map);
     if point.row == 0 {
         return map.max_point();
@@ -203,7 +207,11 @@ pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) ->
     for row in (0..point.row + 1).rev() {
         let blank = map.buffer_snapshot.is_line_blank(row);
         if found_non_blank_line && blank {
-            return Point::new(row, 0).to_display_point(map);
+            if count <= 1 {
+                return Point::new(row, 0).to_display_point(map);
+            }
+            count -= 1;
+            found_non_blank_line = false;
         }
 
         found_non_blank_line |= !blank;
@@ -212,7 +220,11 @@ pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) ->
     DisplayPoint::zero()
 }
 
-pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
+pub fn end_of_paragraph(
+    map: &DisplaySnapshot,
+    display_point: DisplayPoint,
+    mut count: usize,
+) -> DisplayPoint {
     let point = display_point.to_point(map);
     if point.row == map.max_buffer_row() {
         return DisplayPoint::zero();
@@ -222,7 +234,11 @@ pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> D
     for row in point.row..map.max_buffer_row() + 1 {
         let blank = map.buffer_snapshot.is_line_blank(row);
         if found_non_blank_line && blank {
-            return Point::new(row, 0).to_display_point(map);
+            if count <= 1 {
+                return Point::new(row, 0).to_display_point(map);
+            }
+            count -= 1;
+            found_non_blank_line = false;
         }
 
         found_non_blank_line |= !blank;

crates/editor/src/test/editor_test_context.rs 🔗

@@ -210,6 +210,10 @@ impl<'a> EditorTestContext<'a> {
         self.assert_selections(expected_selections, marked_text.to_string())
     }
 
+    pub fn editor_state(&mut self) -> String {
+        generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
+    }
+
     #[track_caller]
     pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
         let expected_ranges = self.ranges(marked_text);
@@ -248,14 +252,8 @@ impl<'a> EditorTestContext<'a> {
         self.assert_selections(expected_selections, expected_marked_text)
     }
 
-    #[track_caller]
-    fn assert_selections(
-        &mut self,
-        expected_selections: Vec<Range<usize>>,
-        expected_marked_text: String,
-    ) {
-        let actual_selections = self
-            .editor
+    fn editor_selections(&self) -> Vec<Range<usize>> {
+        self.editor
             .read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
             .into_iter()
             .map(|s| {
@@ -265,12 +263,22 @@ impl<'a> EditorTestContext<'a> {
                     s.start..s.end
                 }
             })
-            .collect::<Vec<_>>();
+            .collect::<Vec<_>>()
+    }
+
+    #[track_caller]
+    fn assert_selections(
+        &mut self,
+        expected_selections: Vec<Range<usize>>,
+        expected_marked_text: String,
+    ) {
+        let actual_selections = self.editor_selections();
         let actual_marked_text =
             generate_marked_text(&self.buffer_text(), &actual_selections, true);
         if expected_selections != actual_selections {
             panic!(
                 indoc! {"
+
                     {}Editor has unexpected selections.
 
                     Expected selections:

crates/language/src/language.rs 🔗

@@ -427,6 +427,7 @@ fn deserialize_regex<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Regex>, D
 #[cfg(any(test, feature = "test-support"))]
 pub struct FakeLspAdapter {
     pub name: &'static str,
+    pub initialization_options: Option<Value>,
     pub capabilities: lsp::ServerCapabilities,
     pub initializer: Option<Box<dyn 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer)>>,
     pub disk_based_diagnostics_progress_token: Option<String>,
@@ -1637,6 +1638,7 @@ impl Default for FakeLspAdapter {
             capabilities: lsp::LanguageServer::full_capabilities(),
             initializer: None,
             disk_based_diagnostics_progress_token: None,
+            initialization_options: None,
             disk_based_diagnostics_sources: Vec::new(),
         }
     }
@@ -1686,6 +1688,10 @@ impl LspAdapter for Arc<FakeLspAdapter> {
     async fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
         self.disk_based_diagnostics_progress_token.clone()
     }
+
+    async fn initialization_options(&self) -> Option<Value> {
+        self.initialization_options.clone()
+    }
 }
 
 fn get_capture_indices(query: &Query, captures: &mut [(&str, &mut Option<u32>)]) {

crates/language/src/syntax_map.rs 🔗

@@ -4,7 +4,6 @@ mod syntax_map_tests;
 use crate::{Grammar, InjectionConfig, Language, LanguageRegistry};
 use collections::HashMap;
 use futures::FutureExt;
-use lazy_static::lazy_static;
 use parking_lot::Mutex;
 use std::{
     borrow::Cow,
@@ -25,9 +24,7 @@ thread_local! {
     static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
 }
 
-lazy_static! {
-    static ref QUERY_CURSORS: Mutex<Vec<QueryCursor>> = Default::default();
-}
+static QUERY_CURSORS: Mutex<Vec<QueryCursor>> = Mutex::new(vec![]);
 
 #[derive(Default)]
 pub struct SyntaxMap {

crates/live_kit_client/Cargo.toml 🔗

@@ -17,7 +17,6 @@ test-support = [
     "async-trait",
     "collections/test-support",
     "gpui/test-support",
-    "lazy_static",
     "live_kit_server",
     "nanoid",
 ]
@@ -38,7 +37,6 @@ parking_lot.workspace = true
 postage.workspace = true
 
 async-trait = { workspace = true, optional = true }
-lazy_static = { workspace = true, optional = true }
 nanoid = { version ="0.4", optional = true}
 
 [dev-dependencies]
@@ -60,7 +58,6 @@ foreign-types = "0.3"
 futures.workspace = true
 hmac = "0.12"
 jwt = "0.16"
-lazy_static.workspace = true
 objc = "0.2"
 parking_lot.workspace = true
 serde.workspace = true

crates/live_kit_client/src/test.rs 🔗

@@ -1,18 +1,15 @@
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
-use collections::HashMap;
+use collections::{BTreeMap, HashMap};
 use futures::Stream;
 use gpui::executor::Background;
-use lazy_static::lazy_static;
 use live_kit_server::token;
 use media::core_video::CVImageBuffer;
 use parking_lot::Mutex;
 use postage::watch;
 use std::{future::Future, mem, sync::Arc};
 
-lazy_static! {
-    static ref SERVERS: Mutex<HashMap<String, Arc<TestServer>>> = Default::default();
-}
+static SERVERS: Mutex<BTreeMap<String, Arc<TestServer>>> = Mutex::new(BTreeMap::new());
 
 pub struct TestServer {
     pub url: String,

crates/project/src/project.rs 🔗

@@ -50,7 +50,7 @@ use lsp::{
 };
 use lsp_command::*;
 use postage::watch;
-use project_settings::ProjectSettings;
+use project_settings::{LspSettings, ProjectSettings};
 use rand::prelude::*;
 use search::SearchQuery;
 use serde::Serialize;
@@ -149,6 +149,7 @@ pub struct Project {
     _maintain_workspace_config: Task<()>,
     terminals: Terminals,
     copilot_enabled: bool,
+    current_lsp_settings: HashMap<Arc<str>, LspSettings>,
 }
 
 struct DelayedDebounced {
@@ -614,6 +615,7 @@ impl Project {
                     local_handles: Vec::new(),
                 },
                 copilot_enabled: Copilot::global(cx).is_some(),
+                current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
             }
         })
     }
@@ -706,6 +708,7 @@ impl Project {
                     local_handles: Vec::new(),
                 },
                 copilot_enabled: Copilot::global(cx).is_some(),
+                current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
             };
             for worktree in worktrees {
                 let _ = this.add_worktree(&worktree, cx);
@@ -779,7 +782,9 @@ impl Project {
         let mut language_servers_to_stop = Vec::new();
         let mut language_servers_to_restart = Vec::new();
         let languages = self.languages.to_vec();
-        let project_settings = settings::get::<ProjectSettings>(cx).clone();
+
+        let new_lsp_settings = settings::get::<ProjectSettings>(cx).lsp.clone();
+        let current_lsp_settings = &self.current_lsp_settings;
         for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
             let language = languages.iter().find_map(|l| {
                 let adapter = l
@@ -796,16 +801,25 @@ impl Project {
                 if !language_settings(Some(language), file.as_ref(), cx).enable_language_server {
                     language_servers_to_stop.push((*worktree_id, started_lsp_name.clone()));
                 } else if let Some(worktree) = worktree {
-                    let new_lsp_settings = project_settings
-                        .lsp
-                        .get(&adapter.name.0)
-                        .and_then(|s| s.initialization_options.as_ref());
-                    if adapter.initialization_options.as_ref() != new_lsp_settings {
-                        language_servers_to_restart.push((worktree, Arc::clone(language)));
+                    let server_name = &adapter.name.0;
+                    match (
+                        current_lsp_settings.get(server_name),
+                        new_lsp_settings.get(server_name),
+                    ) {
+                        (None, None) => {}
+                        (Some(_), None) | (None, Some(_)) => {
+                            language_servers_to_restart.push((worktree, Arc::clone(language)));
+                        }
+                        (Some(current_lsp_settings), Some(new_lsp_settings)) => {
+                            if current_lsp_settings != new_lsp_settings {
+                                language_servers_to_restart.push((worktree, Arc::clone(language)));
+                            }
+                        }
                     }
                 }
             }
         }
+        self.current_lsp_settings = new_lsp_settings;
 
         // Stop all newly-disabled language servers.
         for (worktree_id, adapter_name) in language_servers_to_stop {

crates/recent_projects/src/recent_projects.rs 🔗

@@ -134,7 +134,7 @@ impl PickerDelegate for RecentProjectsDelegate {
                 let combined_string = location
                     .paths()
                     .iter()
-                    .map(|path| path.to_string_lossy().to_owned())
+                    .map(|path| util::paths::compact(&path).to_string_lossy().into_owned())
                     .collect::<Vec<_>>()
                     .join("");
                 StringMatchCandidate::new(id, combined_string)

crates/search/src/project_search.rs 🔗

@@ -675,6 +675,9 @@ impl ProjectSearchView {
         if match_ranges.is_empty() {
             self.active_match_index = None;
         } else {
+            self.active_match_index = Some(0);
+            self.select_match(Direction::Next, cx);
+            self.update_match_index(cx);
             let prev_search_id = mem::replace(&mut self.search_id, self.model.read(cx).search_id);
             let is_new_search = self.search_id != prev_search_id;
             self.results_editor.update(cx, |editor, cx| {

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -221,6 +221,14 @@ impl TerminalPanel {
             pane::Event::ZoomIn => cx.emit(Event::ZoomIn),
             pane::Event::ZoomOut => cx.emit(Event::ZoomOut),
             pane::Event::Focus => cx.emit(Event::Focus),
+
+            pane::Event::AddItem { item } => {
+                if let Some(workspace) = self.workspace.upgrade(cx) {
+                    let pane = self.pane.clone();
+                    workspace.update(cx, |workspace, cx| item.added_to_pane(workspace, pane, cx))
+                }
+            }
+
             _ => {}
         }
     }

crates/terminal_view/src/terminal_view.rs 🔗

@@ -275,7 +275,7 @@ impl TerminalView {
         cx.spawn(|this, mut cx| async move {
             Timer::after(CURSOR_BLINK_INTERVAL).await;
             this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
-                .log_err();
+                .ok();
         })
         .detach();
     }
@@ -907,6 +907,7 @@ mod tests {
         let params = cx.update(AppState::test);
         cx.update(|cx| {
             theme::init((), cx);
+            Project::init_settings(cx);
             language::init(cx);
         });
 

crates/theme/src/theme.rs 🔗

@@ -1030,6 +1030,7 @@ pub struct AssistantStyle {
     pub system_sender: Interactive<ContainedText>,
     pub model: Interactive<ContainedText>,
     pub remaining_tokens: ContainedText,
+    pub low_remaining_tokens: ContainedText,
     pub no_remaining_tokens: ContainedText,
     pub error_icon: Icon,
     pub api_key_editor: FieldEditor,

crates/vcs_menu/Cargo.toml 🔗

@@ -0,0 +1,16 @@
+[package]
+name = "vcs_menu"
+version = "0.1.0"
+edition = "2021"
+publish = false
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+fuzzy = {path = "../fuzzy"}
+gpui = {path = "../gpui"}
+picker = {path = "../picker"}
+util = {path = "../util"}
+theme = {path = "../theme"}
+workspace = {path = "../workspace"}
+
+anyhow.workspace = true

crates/collab_ui/src/branch_list.rs → crates/vcs_menu/src/lib.rs 🔗

@@ -1,17 +1,20 @@
-use anyhow::{anyhow, bail};
+use anyhow::{anyhow, bail, Result};
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    elements::*, platform::MouseButton, AppContext, MouseState, Task, ViewContext, ViewHandle,
+    actions, elements::*, platform::MouseButton, AppContext, MouseState, Task, ViewContext,
+    ViewHandle,
 };
 use picker::{Picker, PickerDelegate, PickerEvent};
 use std::{ops::Not, sync::Arc};
 use util::ResultExt;
 use workspace::{Toast, Workspace};
 
+actions!(branches, [OpenRecent]);
+
 pub fn init(cx: &mut AppContext) {
     Picker::<BranchListDelegate>::init(cx);
+    cx.add_async_action(toggle);
 }
-
 pub type BranchList = Picker<BranchListDelegate>;
 
 pub fn build_branch_list(
@@ -30,6 +33,34 @@ pub fn build_branch_list(
     .with_theme(|theme| theme.picker.clone())
 }
 
+fn toggle(
+    _: &mut Workspace,
+    _: &OpenRecent,
+    cx: &mut ViewContext<Workspace>,
+) -> Option<Task<Result<()>>> {
+    Some(cx.spawn(|workspace, mut cx| async move {
+        workspace.update(&mut cx, |workspace, cx| {
+            workspace.toggle_modal(cx, |_, cx| {
+                let workspace = cx.handle();
+                cx.add_view(|cx| {
+                    Picker::new(
+                        BranchListDelegate {
+                            matches: vec![],
+                            workspace,
+                            selected_index: 0,
+                            last_query: String::default(),
+                        },
+                        cx,
+                    )
+                    .with_theme(|theme| theme.picker.clone())
+                    .with_max_size(800., 1200.)
+                })
+            });
+        })?;
+        Ok(())
+    }))
+}
+
 pub struct BranchListDelegate {
     matches: Vec<StringMatch>,
     workspace: ViewHandle<Workspace>,

crates/vim/Cargo.toml 🔗

@@ -36,7 +36,6 @@ workspace = { path = "../workspace" }
 [dev-dependencies]
 indoc.workspace = true
 parking_lot.workspace = true
-lazy_static.workspace = true
 
 editor = { path = "../editor", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }

crates/vim/src/motion.rs 🔗

@@ -31,6 +31,8 @@ pub enum Motion {
     CurrentLine,
     StartOfLine,
     EndOfLine,
+    StartOfParagraph,
+    EndOfParagraph,
     StartOfDocument,
     EndOfDocument,
     Matching,
@@ -72,6 +74,8 @@ actions!(
         StartOfLine,
         EndOfLine,
         CurrentLine,
+        StartOfParagraph,
+        EndOfParagraph,
         StartOfDocument,
         EndOfDocument,
         Matching,
@@ -92,6 +96,12 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx));
     cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx));
     cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx));
+    cx.add_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
+        motion(Motion::StartOfParagraph, cx)
+    });
+    cx.add_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| {
+        motion(Motion::EndOfParagraph, cx)
+    });
     cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
         motion(Motion::StartOfDocument, cx)
     });
@@ -142,7 +152,8 @@ impl Motion {
     pub fn linewise(&self) -> bool {
         use Motion::*;
         match self {
-            Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart => true,
+            Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart
+            | StartOfParagraph | EndOfParagraph => true,
             EndOfLine
             | NextWordEnd { .. }
             | Matching
@@ -172,6 +183,8 @@ impl Motion {
             | Backspace
             | Right
             | StartOfLine
+            | StartOfParagraph
+            | EndOfParagraph
             | NextWordStart { .. }
             | PreviousWordStart { .. }
             | FirstNonWhitespace
@@ -197,6 +210,8 @@ impl Motion {
             | Backspace
             | Right
             | StartOfLine
+            | StartOfParagraph
+            | EndOfParagraph
             | NextWordStart { .. }
             | PreviousWordStart { .. }
             | FirstNonWhitespace
@@ -235,6 +250,14 @@ impl Motion {
             FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None),
             StartOfLine => (start_of_line(map, point), SelectionGoal::None),
             EndOfLine => (end_of_line(map, point), SelectionGoal::None),
+            StartOfParagraph => (
+                movement::start_of_paragraph(map, point, times),
+                SelectionGoal::None,
+            ),
+            EndOfParagraph => (
+                map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
+                SelectionGoal::None,
+            ),
             CurrentLine => (end_of_line(map, point), SelectionGoal::None),
             StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
             EndOfDocument => (
@@ -502,10 +525,13 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint
     if line_end == point {
         line_end = map.max_point().to_point(map);
     }
-    line_end.column = line_end.column.saturating_sub(1);
 
     let line_range = map.prev_line_boundary(point).0..line_end;
-    let ranges = map.buffer_snapshot.bracket_ranges(line_range.clone());
+    let visible_line_range =
+        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
+    let ranges = map
+        .buffer_snapshot
+        .bracket_ranges(visible_line_range.clone());
     if let Some(ranges) = ranges {
         let line_range = line_range.start.to_offset(&map.buffer_snapshot)
             ..line_range.end.to_offset(&map.buffer_snapshot);
@@ -590,3 +616,131 @@ fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) ->
     let new_row = (point.row() + times as u32).min(map.max_buffer_row());
     map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left)
 }
+
+#[cfg(test)]
+
+mod test {
+
+    use crate::test::NeovimBackedTestContext;
+    use indoc::indoc;
+
+    #[gpui::test]
+    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        let initial_state = indoc! {r"ˇabc
+            def
+
+            paragraph
+            the second
+
+
+
+            third and
+            final"};
+
+        // goes down once
+        cx.set_shared_state(initial_state).await;
+        cx.simulate_shared_keystrokes(["}"]).await;
+        cx.assert_shared_state(indoc! {r"abc
+            def
+            ˇ
+            paragraph
+            the second
+
+
+
+            third and
+            final"})
+            .await;
+
+        // goes up once
+        cx.simulate_shared_keystrokes(["{"]).await;
+        cx.assert_shared_state(initial_state).await;
+
+        // goes down twice
+        cx.simulate_shared_keystrokes(["2", "}"]).await;
+        cx.assert_shared_state(indoc! {r"abc
+            def
+
+            paragraph
+            the second
+            ˇ
+
+
+            third and
+            final"})
+            .await;
+
+        // goes down over multiple blanks
+        cx.simulate_shared_keystrokes(["}"]).await;
+        cx.assert_shared_state(indoc! {r"abc
+                def
+
+                paragraph
+                the second
+
+
+
+                third and
+                finaˇl"})
+            .await;
+
+        // goes up twice
+        cx.simulate_shared_keystrokes(["2", "{"]).await;
+        cx.assert_shared_state(indoc! {r"abc
+                def
+                ˇ
+                paragraph
+                the second
+
+
+
+                third and
+                final"})
+            .await
+    }
+
+    #[gpui::test]
+    async fn test_matching(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {r"func ˇ(a string) {
+                do(something(with<Types>.and_arrays[0, 2]))
+            }"})
+            .await;
+        cx.simulate_shared_keystrokes(["%"]).await;
+        cx.assert_shared_state(indoc! {r"func (a stringˇ) {
+                do(something(with<Types>.and_arrays[0, 2]))
+            }"})
+            .await;
+
+        // test it works on the last character of the line
+        cx.set_shared_state(indoc! {r"func (a string) ˇ{
+            do(something(with<Types>.and_arrays[0, 2]))
+            }"})
+            .await;
+        cx.simulate_shared_keystrokes(["%"]).await;
+        cx.assert_shared_state(indoc! {r"func (a string) {
+            do(something(with<Types>.and_arrays[0, 2]))
+            ˇ}"})
+            .await;
+
+        // test it works on immediate nesting
+        cx.set_shared_state("ˇ{()}").await;
+        cx.simulate_shared_keystrokes(["%"]).await;
+        cx.assert_shared_state("{()ˇ}").await;
+        cx.simulate_shared_keystrokes(["%"]).await;
+        cx.assert_shared_state("ˇ{()}").await;
+
+        // test it works on immediate nesting inside braces
+        cx.set_shared_state("{\n    ˇ{()}\n}").await;
+        cx.simulate_shared_keystrokes(["%"]).await;
+        cx.assert_shared_state("{\n    {()ˇ}\n}").await;
+
+        // test it jumps to the next paren on a line
+        cx.set_shared_state("func ˇboop() {\n}").await;
+        cx.simulate_shared_keystrokes(["%"]).await;
+        cx.assert_shared_state("func boop(ˇ) {\n}").await;
+    }
+}

crates/vim/src/normal/case.rs 🔗

@@ -1,29 +1,51 @@
+use editor::scroll::autoscroll::Autoscroll;
 use gpui::ViewContext;
-use language::Point;
+use language::{Bias, Point};
 use workspace::Workspace;
 
-use crate::{motion::Motion, normal::ChangeCase, Vim};
+use crate::{normal::ChangeCase, state::Mode, Vim};
 
 pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) {
     Vim::update(cx, |vim, cx| {
-        let count = vim.pop_number_operator(cx);
+        let count = vim.pop_number_operator(cx).unwrap_or(1) as u32;
         vim.update_active_editor(cx, |editor, cx| {
-            editor.set_clip_at_line_ends(false, cx);
-            editor.transact(cx, |editor, cx| {
-                editor.change_selections(None, cx, |s| {
-                    s.move_with(|map, selection| {
-                        if selection.start == selection.end {
-                            Motion::Right.expand_selection(map, selection, count, true);
+            let mut ranges = Vec::new();
+            let mut cursor_positions = Vec::new();
+            let snapshot = editor.buffer().read(cx).snapshot(cx);
+            for selection in editor.selections.all::<Point>(cx) {
+                match vim.state.mode {
+                    Mode::Visual { line: true } => {
+                        let start = Point::new(selection.start.row, 0);
+                        let end =
+                            Point::new(selection.end.row, snapshot.line_len(selection.end.row));
+                        ranges.push(start..end);
+                        cursor_positions.push(start..start);
+                    }
+                    Mode::Visual { line: false } => {
+                        ranges.push(selection.start..selection.end);
+                        cursor_positions.push(selection.start..selection.start);
+                    }
+                    Mode::Insert | Mode::Normal => {
+                        let start = selection.start;
+                        let mut end = start;
+                        for _ in 0..count {
+                            end = snapshot.clip_point(end + Point::new(0, 1), Bias::Right);
                         }
-                    })
-                });
-                let selections = editor.selections.all::<Point>(cx);
-                for selection in selections.into_iter().rev() {
+                        ranges.push(start..end);
+
+                        if end.column == snapshot.line_len(end.row) {
+                            end = snapshot.clip_point(end - Point::new(0, 1), Bias::Left);
+                        }
+                        cursor_positions.push(end..end)
+                    }
+                }
+            }
+            editor.transact(cx, |editor, cx| {
+                for range in ranges.into_iter().rev() {
                     let snapshot = editor.buffer().read(cx).snapshot(cx);
                     editor.buffer().update(cx, |buffer, cx| {
-                        let range = selection.start..selection.end;
                         let text = snapshot
-                            .text_for_range(selection.start..selection.end)
+                            .text_for_range(range.start..range.end)
                             .flat_map(|s| s.chars())
                             .flat_map(|c| {
                                 if c.is_lowercase() {
@@ -37,28 +59,46 @@ pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Works
                         buffer.edit([(range, text)], None, cx)
                     })
                 }
+                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                    s.select_ranges(cursor_positions)
+                })
             });
-            editor.set_clip_at_line_ends(true, cx);
         });
+        vim.switch_mode(Mode::Normal, true, cx)
     })
 }
-
 #[cfg(test)]
 mod test {
-    use crate::{state::Mode, test::VimTestContext};
-    use indoc::indoc;
+    use crate::{state::Mode, test::NeovimBackedTestContext};
 
     #[gpui::test]
     async fn test_change_case(cx: &mut gpui::TestAppContext) {
-        let mut cx = VimTestContext::new(cx, true).await;
-        cx.set_state(indoc! {"ˇabC\n"}, Mode::Normal);
-        cx.simulate_keystrokes(["~"]);
-        cx.assert_editor_state("AˇbC\n");
-        cx.simulate_keystrokes(["2", "~"]);
-        cx.assert_editor_state("ABcˇ\n");
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.set_shared_state("ˇabC\n").await;
+        cx.simulate_shared_keystrokes(["~"]).await;
+        cx.assert_shared_state("AˇbC\n").await;
+        cx.simulate_shared_keystrokes(["2", "~"]).await;
+        cx.assert_shared_state("ABˇc\n").await;
+
+        // works in visual mode
+        cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
+        cx.simulate_shared_keystrokes(["~"]).await;
+        cx.assert_shared_state("a😀CˇDé1*F\n").await;
+
+        // works with multibyte characters
+        cx.simulate_shared_keystrokes(["~"]).await;
+        cx.set_shared_state("aˇC😀é1*F\n").await;
+        cx.simulate_shared_keystrokes(["4", "~"]).await;
+        cx.assert_shared_state("ac😀É1ˇ*F\n").await;
+
+        // works with line selections
+        cx.set_shared_state("abˇC\n").await;
+        cx.simulate_shared_keystrokes(["shift-v", "~"]).await;
+        cx.assert_shared_state("ˇABc\n").await;
 
-        cx.set_state(indoc! {"a😀C«dÉ1*fˇ»\n"}, Mode::Normal);
-        cx.simulate_keystrokes(["~"]);
-        cx.assert_editor_state("a😀CDé1*Fˇ\n");
+        // works with multiple cursors (zed only)
+        cx.set_state("aˇßcdˇe\n", Mode::Normal);
+        cx.simulate_keystroke("~");
+        cx.assert_state("aSSˇcdˇE\n", Mode::Normal);
     }
 }

crates/vim/src/test.rs 🔗

@@ -4,6 +4,7 @@ mod neovim_connection;
 mod vim_binding_test_context;
 mod vim_test_context;
 
+use command_palette::CommandPalette;
 pub use neovim_backed_binding_test_context::*;
 pub use neovim_backed_test_context::*;
 pub use vim_binding_test_context::*;
@@ -139,3 +140,16 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
     cx.simulate_keystrokes(["shift-v", "down", ">", ">"]);
     cx.assert_editor_state("aa\n    b«b\n    cˇ»c");
 }
+
+#[gpui::test]
+async fn test_escape_command_palette(cx: &mut gpui::TestAppContext) {
+    let mut cx = VimTestContext::new(cx, true).await;
+
+    cx.set_state("aˇbc\n", Mode::Normal);
+    cx.simulate_keystrokes(["i", "cmd-shift-p"]);
+
+    assert!(cx.workspace(|workspace, _| workspace.modal::<CommandPalette>().is_some()));
+    cx.simulate_keystroke("escape");
+    assert!(!cx.workspace(|workspace, _| workspace.modal::<CommandPalette>().is_some()));
+    cx.assert_state("aˇbc\n", Mode::Insert);
+}

crates/vim/src/test/neovim_backed_test_context.rs 🔗

@@ -1,9 +1,10 @@
-use std::ops::{Deref, DerefMut};
+use indoc::indoc;
+use std::ops::{Deref, DerefMut, Range};
 
 use collections::{HashMap, HashSet};
 use gpui::ContextHandle;
 use language::OffsetRangeExt;
-use util::test::marked_text_offsets;
+use util::test::{generate_marked_text, marked_text_offsets};
 
 use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
 use crate::state::Mode;
@@ -112,6 +113,43 @@ impl<'a> NeovimBackedTestContext<'a> {
         context_handle
     }
 
+    pub async fn assert_shared_state(&mut self, marked_text: &str) {
+        let neovim = self.neovim_state().await;
+        if neovim != marked_text {
+            panic!(
+                indoc! {"Test is incorrect (currently expected != neovim state)
+
+                # currently expected:
+                {}
+                # neovim state:
+                {}
+                # zed state:
+                {}"},
+                marked_text,
+                neovim,
+                self.editor_state(),
+            )
+        }
+        self.assert_editor_state(marked_text)
+    }
+
+    pub async fn neovim_state(&mut self) -> String {
+        generate_marked_text(
+            self.neovim.text().await.as_str(),
+            &vec![self.neovim_selection().await],
+            true,
+        )
+    }
+
+    async fn neovim_selection(&mut self) -> Range<usize> {
+        let mut neovim_selection = self.neovim.selection().await;
+        // Zed selections adjust themselves to make the end point visually make sense
+        if neovim_selection.start > neovim_selection.end {
+            neovim_selection.start.column += 1;
+        }
+        neovim_selection.to_offset(&self.buffer_snapshot())
+    }
+
     pub async fn assert_state_matches(&mut self) {
         assert_eq!(
             self.neovim.text().await,
@@ -120,13 +158,8 @@ impl<'a> NeovimBackedTestContext<'a> {
             self.assertion_context()
         );
 
-        let mut neovim_selection = self.neovim.selection().await;
-        // Zed selections adjust themselves to make the end point visually make sense
-        if neovim_selection.start > neovim_selection.end {
-            neovim_selection.start.column += 1;
-        }
-        let neovim_selection = neovim_selection.to_offset(&self.buffer_snapshot());
-        self.assert_editor_selections(vec![neovim_selection]);
+        let selections = vec![self.neovim_selection().await];
+        self.assert_editor_selections(selections);
 
         if let Some(neovim_mode) = self.neovim.mode().await {
             assert_eq!(neovim_mode, self.mode(), "{}", self.assertion_context(),);

crates/vim/src/test/neovim_connection.rs 🔗

@@ -11,8 +11,6 @@ use gpui::keymap_matcher::Keystroke;
 
 use language::Point;
 
-#[cfg(feature = "neovim")]
-use lazy_static::lazy_static;
 #[cfg(feature = "neovim")]
 use nvim_rs::{
     create::tokio::new_child_cmd, error::LoopError, Handler, Neovim, UiAttachOptions, Value,
@@ -32,9 +30,7 @@ use collections::VecDeque;
 // Neovim doesn't like to be started simultaneously from multiple threads. We use this lock
 // to ensure we are only constructing one neovim connection at a time.
 #[cfg(feature = "neovim")]
-lazy_static! {
-    static ref NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(());
-}
+static NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(());
 
 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
 pub enum NeovimData {
@@ -171,15 +167,25 @@ impl NeovimConnection {
             .await
             .expect("Could not get neovim window");
 
-        if !selection.is_empty() {
-            panic!("Setting neovim state with non empty selection not yet supported");
-        }
         let cursor = selection.start;
         nvim_window
             .set_cursor((cursor.row as i64 + 1, cursor.column as i64))
             .await
             .expect("Could not set nvim cursor position");
 
+        if !selection.is_empty() {
+            self.nvim
+                .input("v")
+                .await
+                .expect("could not enter visual mode");
+
+            let cursor = selection.end;
+            nvim_window
+                .set_cursor((cursor.row as i64 + 1, cursor.column as i64))
+                .await
+                .expect("Could not set nvim cursor position");
+        }
+
         if let Some(NeovimData::Get { mode, state }) = self.data.back() {
             if *mode == Some(Mode::Normal) && *state == marked_text {
                 return;

crates/vim/src/test/vim_test_context.rs 🔗

@@ -21,12 +21,14 @@ impl<'a> VimTestContext<'a> {
         cx.update(|cx| {
             search::init(cx);
             crate::init(cx);
+            command_palette::init(cx);
         });
 
         cx.update(|cx| {
             cx.update_global(|store: &mut SettingsStore, cx| {
                 store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled));
             });
+            settings::KeymapFile::load_asset("keymaps/default.json", cx).unwrap();
             settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap();
         });
 

crates/vim/src/vim.rs 🔗

@@ -12,7 +12,7 @@ mod visual;
 
 use anyhow::Result;
 use collections::CommandPaletteFilter;
-use editor::{Bias, Cancel, Editor, EditorMode, Event};
+use editor::{Bias, Editor, EditorMode, Event};
 use gpui::{
     actions, impl_actions, AppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle,
     WindowContext,
@@ -64,22 +64,6 @@ pub fn init(cx: &mut AppContext) {
         Vim::update(cx, |vim, cx| vim.push_number(n, cx));
     });
 
-    // Editor Actions
-    cx.add_action(|_: &mut Editor, _: &Cancel, cx| {
-        // If we are in aren't in 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() {
-            WindowContext::defer(cx, |cx| {
-                Vim::update(cx, |state, cx| {
-                    state.switch_mode(Mode::Normal, false, cx);
-                });
-            });
-        } else {
-            cx.propagate_action();
-        }
-    });
-
     cx.add_action(|_: &mut Workspace, _: &Tab, cx| {
         Vim::active_editor_input_ignored(" ".into(), cx)
     });
@@ -109,10 +93,7 @@ pub fn observe_keystrokes(cx: &mut WindowContext) {
     cx.observe_keystrokes(|_keystroke, _result, handled_by, cx| {
         if let Some(handled_by) = handled_by {
             // Keystroke is handled by the vim system, so continue forward
-            // Also short circuit if it is the special cancel action
-            if handled_by.namespace() == "vim"
-                || (handled_by.namespace() == "editor" && handled_by.name() == "Cancel")
-            {
+            if handled_by.namespace() == "vim" {
                 return true;
             }
         }

crates/vim/test_data/test_change_case.json 🔗

@@ -0,0 +1,18 @@
+{"Put":{"state":"ˇabC\n"}}
+{"Key":"~"}
+{"Get":{"state":"AˇbC\n","mode":"Normal"}}
+{"Key":"2"}
+{"Key":"~"}
+{"Get":{"state":"ABˇc\n","mode":"Normal"}}
+{"Put":{"state":"a😀C«dÉ1*fˇ»\n"}}
+{"Key":"~"}
+{"Get":{"state":"a😀CˇDé1*F\n","mode":"Normal"}}
+{"Key":"~"}
+{"Put":{"state":"aˇC😀é1*F\n"}}
+{"Key":"4"}
+{"Key":"~"}
+{"Get":{"state":"ac😀É1ˇ*F\n","mode":"Normal"}}
+{"Put":{"state":"abˇC\n"}}
+{"Key":"shift-v"}
+{"Key":"~"}
+{"Get":{"state":"ˇABc\n","mode":"Normal"}}

crates/vim/test_data/test_matching.json 🔗

@@ -0,0 +1,17 @@
+{"Put":{"state":"func ˇ(a string) {\n    do(something(with<Types>.and_arrays[0, 2]))\n}"}}
+{"Key":"%"}
+{"Get":{"state":"func (a stringˇ) {\n    do(something(with<Types>.and_arrays[0, 2]))\n}","mode":"Normal"}}
+{"Put":{"state":"func (a string) ˇ{\ndo(something(with<Types>.and_arrays[0, 2]))\n}"}}
+{"Key":"%"}
+{"Get":{"state":"func (a string) {\ndo(something(with<Types>.and_arrays[0, 2]))\nˇ}","mode":"Normal"}}
+{"Put":{"state":"ˇ{()}"}}
+{"Key":"%"}
+{"Get":{"state":"{()ˇ}","mode":"Normal"}}
+{"Key":"%"}
+{"Get":{"state":"ˇ{()}","mode":"Normal"}}
+{"Put":{"state":"{\n    ˇ{()}\n}"}}
+{"Key":"%"}
+{"Get":{"state":"{\n    {()ˇ}\n}","mode":"Normal"}}
+{"Put":{"state":"func ˇboop() {\n}"}}
+{"Key":"%"}
+{"Get":{"state":"func boop(ˇ) {\n}","mode":"Normal"}}

crates/vim/test_data/test_start_end_of_paragraph.json 🔗

@@ -0,0 +1,13 @@
+{"Put":{"state":"ˇabc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinal"}}
+{"Key":"}"}
+{"Get":{"state":"abc\ndef\nˇ\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}}
+{"Key":"{"}
+{"Get":{"state":"ˇabc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}}
+{"Key":"2"}
+{"Key":"}"}
+{"Get":{"state":"abc\ndef\n\nparagraph\nthe second\nˇ\n\n\nthird and\nfinal","mode":"Normal"}}
+{"Key":"}"}
+{"Get":{"state":"abc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinaˇl","mode":"Normal"}}
+{"Key":"2"}
+{"Key":"{"}
+{"Get":{"state":"abc\ndef\nˇ\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}}

crates/workspace/src/item.rs 🔗

@@ -27,7 +27,7 @@ use std::{
 };
 use theme::Theme;
 
-#[derive(Eq, PartialEq, Hash)]
+#[derive(Eq, PartialEq, Hash, Debug)]
 pub enum ItemEvent {
     CloseItem,
     UpdateTab,

crates/workspace/src/pane.rs 🔗

@@ -2316,6 +2316,7 @@ mod tests {
             cx.set_global(SettingsStore::test(cx));
             theme::init((), cx);
             crate::init_settings(cx);
+            Project::init_settings(cx);
         });
     }
 

crates/zed/src/main.rs 🔗

@@ -57,8 +57,9 @@ use staff_mode::StaffMode;
 use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
 use workspace::{item::ItemHandle, notifications::NotifyResultExt, AppState, Workspace};
 use zed::{
-    assets::Assets, build_window_options, handle_keymap_file_changes, initialize_workspace,
-    languages, menus,
+    assets::Assets,
+    build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
+    only_instance::{ensure_only_instance, IsOnlyInstance},
 };
 
 fn main() {
@@ -66,6 +67,10 @@ fn main() {
     init_paths();
     init_logger();
 
+    if ensure_only_instance() != IsOnlyInstance::Yes {
+        return;
+    }
+
     log::info!("========== starting zed ==========");
     let mut app = gpui::App::new(Assets).unwrap();
 

crates/zed/src/only_instance.rs 🔗

@@ -0,0 +1,103 @@
+use std::{
+    io::{Read, Write},
+    net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener, TcpStream},
+    thread,
+    time::Duration,
+};
+
+use util::channel::ReleaseChannel;
+
+const LOCALHOST: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1);
+const CONNECT_TIMEOUT: Duration = Duration::from_millis(10);
+const RECEIVE_TIMEOUT: Duration = Duration::from_millis(35);
+const SEND_TIMEOUT: Duration = Duration::from_millis(20);
+
+fn address() -> SocketAddr {
+    let port = match *util::channel::RELEASE_CHANNEL {
+        ReleaseChannel::Dev => 43737,
+        ReleaseChannel::Preview => 43738,
+        ReleaseChannel::Stable => 43739,
+    };
+
+    SocketAddr::V4(SocketAddrV4::new(LOCALHOST, port))
+}
+
+fn instance_handshake() -> &'static str {
+    match *util::channel::RELEASE_CHANNEL {
+        ReleaseChannel::Dev => "Zed Editor Dev Instance Running",
+        ReleaseChannel::Preview => "Zed Editor Preview Instance Running",
+        ReleaseChannel::Stable => "Zed Editor Stable Instance Running",
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum IsOnlyInstance {
+    Yes,
+    No,
+}
+
+pub fn ensure_only_instance() -> IsOnlyInstance {
+    if *db::ZED_STATELESS {
+        return IsOnlyInstance::Yes;
+    }
+
+    if check_got_handshake() {
+        return IsOnlyInstance::No;
+    }
+
+    let listener = match TcpListener::bind(address()) {
+        Ok(listener) => listener,
+
+        Err(err) => {
+            log::warn!("Error binding to single instance port: {err}");
+            if check_got_handshake() {
+                return IsOnlyInstance::No;
+            }
+
+            // Avoid failing to start when some other application by chance already has
+            // a claim on the port. This is sub-par as any other instance that gets launched
+            // will be unable to communicate with this instance and will duplicate
+            log::warn!("Backup handshake request failed, continuing without handshake");
+            return IsOnlyInstance::Yes;
+        }
+    };
+
+    thread::spawn(move || {
+        for stream in listener.incoming() {
+            let mut stream = match stream {
+                Ok(stream) => stream,
+                Err(_) => return,
+            };
+
+            _ = stream.set_nodelay(true);
+            _ = stream.set_read_timeout(Some(SEND_TIMEOUT));
+            _ = stream.write_all(instance_handshake().as_bytes());
+        }
+    });
+
+    IsOnlyInstance::Yes
+}
+
+fn check_got_handshake() -> bool {
+    match TcpStream::connect_timeout(&address(), CONNECT_TIMEOUT) {
+        Ok(mut stream) => {
+            let mut buf = vec![0u8; instance_handshake().len()];
+
+            stream.set_read_timeout(Some(RECEIVE_TIMEOUT)).unwrap();
+            if let Err(err) = stream.read_exact(&mut buf) {
+                log::warn!("Connected to single instance port but failed to read: {err}");
+                return false;
+            }
+
+            if buf == instance_handshake().as_bytes() {
+                log::info!("Got instance handshake");
+                return true;
+            }
+
+            log::warn!("Got wrong instance handshake value");
+            false
+        }
+
+        Err(_) => false,
+    }
+}

crates/zed/src/zed.rs 🔗

@@ -1,6 +1,7 @@
 pub mod assets;
 pub mod languages;
 pub mod menus;
+pub mod only_instance;
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
 

styles/src/component/tab_bar_button.ts 🔗

@@ -0,0 +1,55 @@
+import { Theme, StyleSets } from "../common"
+import { interactive } from "../element"
+import { InteractiveState } from "../element/interactive"
+import { background, foreground } from "../style_tree/components"
+
+interface TabBarButtonOptions {
+    icon: string
+    color?: StyleSets
+}
+
+type TabBarButtonProps = TabBarButtonOptions & {
+    state?: Partial<Record<InteractiveState, Partial<TabBarButtonOptions>>>
+}
+
+export function tab_bar_button(theme: Theme, { icon, color = "base" }: TabBarButtonProps) {
+    const button_spacing = 8
+
+    return (
+        interactive({
+            base: {
+                icon: {
+                    color: foreground(theme.middle, color),
+                    asset: icon,
+                    dimensions: {
+                        width: 15,
+                        height: 15,
+                    },
+                },
+                container: {
+                    corner_radius: 4,
+                    padding: {
+                        top: 4, bottom: 4, left: 4, right: 4
+                    },
+                    margin: {
+                        left: button_spacing / 2,
+                        right: button_spacing / 2,
+                    },
+                },
+            },
+            state: {
+                hovered: {
+                    container: {
+                        background: background(theme.middle, color, "hovered"),
+
+                    }
+                },
+                clicked: {
+                    container: {
+                        background: background(theme.middle, color, "pressed"),
+                    }
+                },
+            },
+        })
+    )
+}

styles/src/style_tree/assistant.ts 🔗

@@ -1,233 +1,133 @@
-import { text, border, background, foreground } from "./components"
-import { interactive } from "../element"
-import { useTheme } from "../theme"
+import { text, border, background, foreground, TextStyle } from "./components"
+import { Interactive, interactive } from "../element"
+import { tab_bar_button } from "../component/tab_bar_button"
+import { StyleSets, useTheme } from "../theme"
+
+type RoleCycleButton = TextStyle & {
+    background?: string
+}
+// TODO: Replace these with zed types
+type RemainingTokens = TextStyle & {
+    background: string,
+    margin: { top: number, right: number },
+    padding: {
+        right: number,
+        left: number,
+        top: number,
+        bottom: number,
+    },
+    corner_radius: number,
+}
 
 export default function assistant(): any {
     const theme = useTheme()
 
+    const interactive_role = (color: StyleSets): Interactive<RoleCycleButton> => {
+        return (
+            interactive({
+                base: {
+                    ...text(theme.highest, "sans", color, { size: "sm" }),
+                },
+                state: {
+                    hovered: {
+                        ...text(theme.highest, "sans", color, { size: "sm" }),
+                        background: background(theme.highest, color, "hovered"),
+                    },
+                    clicked: {
+                        ...text(theme.highest, "sans", color, { size: "sm" }),
+                        background: background(theme.highest, color, "pressed"),
+                    }
+                },
+            })
+        )
+    }
+
+    const tokens_remaining = (color: StyleSets): RemainingTokens => {
+        return (
+            {
+                ...text(theme.highest, "mono", color, { size: "xs" }),
+                background: background(theme.highest, "on", "default"),
+                margin: { top: 12, right: 20 },
+                padding: { right: 4, left: 4, top: 1, bottom: 1 },
+                corner_radius: 6,
+            }
+        )
+    }
+
     return {
         container: {
             background: background(theme.highest),
             padding: { left: 12 },
         },
         message_header: {
-            margin: { bottom: 6, top: 6 },
+            margin: { bottom: 4, top: 4 },
             background: background(theme.highest),
         },
-        hamburger_button: interactive({
-            base: {
-                icon: {
-                    color: foreground(theme.highest, "variant"),
-                    asset: "icons/hamburger_15.svg",
-                    dimensions: {
-                        width: 15,
-                        height: 15,
-                    },
-                },
-                container: {
-                    padding: { left: 12, right: 8.5 },
-                },
-            },
-            state: {
-                hovered: {
-                    icon: {
-                        color: foreground(theme.highest, "hovered"),
-                    },
-                },
-            },
+        hamburger_button: tab_bar_button(theme, {
+            icon: "icons/hamburger_15.svg",
         }),
-        split_button: interactive({
-            base: {
-                icon: {
-                    color: foreground(theme.highest, "variant"),
-                    asset: "icons/split_message_15.svg",
-                    dimensions: {
-                        width: 15,
-                        height: 15,
-                    },
-                },
-                container: {
-                    padding: { left: 8.5, right: 8.5 },
-                },
-            },
-            state: {
-                hovered: {
-                    icon: {
-                        color: foreground(theme.highest, "hovered"),
-                    },
-                },
-            },
+
+        split_button: tab_bar_button(theme, {
+            icon: "icons/split_message_15.svg",
         }),
-        quote_button: interactive({
-            base: {
-                icon: {
-                    color: foreground(theme.highest, "variant"),
-                    asset: "icons/quote_15.svg",
-                    dimensions: {
-                        width: 15,
-                        height: 15,
-                    },
-                },
-                container: {
-                    padding: { left: 8.5, right: 8.5 },
-                },
-            },
-            state: {
-                hovered: {
-                    icon: {
-                        color: foreground(theme.highest, "hovered"),
-                    },
-                },
-            },
+        quote_button: tab_bar_button(theme, {
+            icon: "icons/radix/quote.svg",
         }),
-        assist_button: interactive({
-            base: {
-                icon: {
-                    color: foreground(theme.highest, "variant"),
-                    asset: "icons/assist_15.svg",
-                    dimensions: {
-                        width: 15,
-                        height: 15,
-                    },
-                },
-                container: {
-                    padding: { left: 8.5, right: 8.5 },
-                },
-            },
-            state: {
-                hovered: {
-                    icon: {
-                        color: foreground(theme.highest, "hovered"),
-                    },
-                },
-            },
+        assist_button: tab_bar_button(theme, {
+            icon: "icons/radix/magic-wand.svg",
         }),
-        zoom_in_button: interactive({
-            base: {
-                icon: {
-                    color: foreground(theme.highest, "variant"),
-                    asset: "icons/maximize_8.svg",
-                    dimensions: {
-                        width: 12,
-                        height: 12,
-                    },
-                },
-                container: {
-                    padding: { left: 10, right: 10 },
-                },
-            },
-            state: {
-                hovered: {
-                    icon: {
-                        color: foreground(theme.highest, "hovered"),
-                    },
-                },
-            },
+        zoom_in_button: tab_bar_button(theme, {
+            icon: "icons/radix/maximize.svg",
         }),
-        zoom_out_button: interactive({
-            base: {
-                icon: {
-                    color: foreground(theme.highest, "variant"),
-                    asset: "icons/minimize_8.svg",
-                    dimensions: {
-                        width: 12,
-                        height: 12,
-                    },
-                },
-                container: {
-                    padding: { left: 10, right: 10 },
-                },
-            },
-            state: {
-                hovered: {
-                    icon: {
-                        color: foreground(theme.highest, "hovered"),
-                    },
-                },
-            },
+        zoom_out_button: tab_bar_button(theme, {
+            icon: "icons/radix/minimize.svg",
         }),
-        plus_button: interactive({
-            base: {
-                icon: {
-                    color: foreground(theme.highest, "variant"),
-                    asset: "icons/plus_12.svg",
-                    dimensions: {
-                        width: 12,
-                        height: 12,
-                    },
-                },
-                container: {
-                    padding: { left: 10, right: 10 },
-                },
-            },
-            state: {
-                hovered: {
-                    icon: {
-                        color: foreground(theme.highest, "hovered"),
-                    },
-                },
-            },
+        plus_button: tab_bar_button(theme, {
+            icon: "icons/radix/plus.svg",
         }),
         title: {
-            ...text(theme.highest, "sans", "default", { size: "sm" }),
+            ...text(theme.highest, "sans", "default", { size: "xs" }),
         },
         saved_conversation: {
             container: interactive({
                 base: {
-                    background: background(theme.highest, "on"),
+                    background: background(theme.middle),
                     padding: { top: 4, bottom: 4 },
+                    border: border(theme.middle, "default", { top: true, overlay: true }),
                 },
                 state: {
                     hovered: {
-                        background: background(theme.highest, "on", "hovered"),
+                        background: background(theme.middle, "hovered"),
                     },
+                    clicked: {
+                        background: background(theme.middle, "pressed"),
+                    }
                 },
             }),
             saved_at: {
                 margin: { left: 8 },
-                ...text(theme.highest, "sans", "default", { size: "xs" }),
+                ...text(theme.highest, "sans", "variant", { size: "xs" }),
             },
             title: {
-                margin: { left: 16 },
-                ...text(theme.highest, "sans", "default", {
-                    size: "sm",
-                    weight: "bold",
-                }),
-            },
-        },
-        user_sender: {
-            default: {
+                margin: { left: 12 },
                 ...text(theme.highest, "sans", "default", {
                     size: "sm",
                     weight: "bold",
                 }),
             },
         },
-        assistant_sender: {
-            default: {
-                ...text(theme.highest, "sans", "accent", {
-                    size: "sm",
-                    weight: "bold",
-                }),
-            },
-        },
-        system_sender: {
-            default: {
-                ...text(theme.highest, "sans", "variant", {
-                    size: "sm",
-                    weight: "bold",
-                }),
-            },
-        },
+        user_sender: interactive_role("base"),
+        assistant_sender: interactive_role("accent"),
+        system_sender: interactive_role("warning"),
         sent_at: {
             margin: { top: 2, left: 8 },
-            ...text(theme.highest, "sans", "default", { size: "2xs" }),
+            ...text(theme.highest, "sans", "variant", { size: "2xs" }),
         },
         model: interactive({
             base: {
-                background: background(theme.highest, "on"),
-                margin: { left: 12, right: 12, top: 12 },
-                padding: 4,
+                background: background(theme.highest),
+                margin: { left: 12, right: 4, top: 12 },
+                padding: { right: 4, left: 4, top: 1, bottom: 1 },
                 corner_radius: 4,
                 ...text(theme.highest, "sans", "default", { size: "xs" }),
             },
@@ -238,20 +138,9 @@ export default function assistant(): any {
                 },
             },
         }),
-        remaining_tokens: {
-            background: background(theme.highest, "on"),
-            margin: { top: 12, right: 24 },
-            padding: 4,
-            corner_radius: 4,
-            ...text(theme.highest, "sans", "positive", { size: "xs" }),
-        },
-        no_remaining_tokens: {
-            background: background(theme.highest, "on"),
-            margin: { top: 12, right: 24 },
-            padding: 4,
-            corner_radius: 4,
-            ...text(theme.highest, "sans", "negative", { size: "xs" }),
-        },
+        remaining_tokens: tokens_remaining("positive"),
+        low_remaining_tokens: tokens_remaining("warning"),
+        no_remaining_tokens: tokens_remaining("negative"),
         error_icon: {
             margin: { left: 8 },
             color: foreground(theme.highest, "negative"),
@@ -259,7 +148,7 @@ export default function assistant(): any {
         },
         api_key_editor: {
             background: background(theme.highest, "on"),
-            corner_radius: 6,
+            corner_radius: 4,
             text: text(theme.highest, "mono", "on"),
             placeholder_text: text(theme.highest, "mono", "on", "disabled", {
                 size: "xs",

styles/src/style_tree/titlebar.ts 🔗

@@ -84,7 +84,7 @@ function user_menu() {
                 base: {
                     corner_radius: 6,
                     height: button_height,
-                    width: online ? 37 : 24,
+                    width: 20,
                     padding: {
                         top: 2,
                         bottom: 2,
@@ -153,6 +153,7 @@ function user_menu() {
             },
         }
     }
+
     return {
         user_menu_button_online: build_button({ online: true }),
         user_menu_button_offline: build_button({ online: false }),

styles/src/theme/create_theme.ts 🔗

@@ -12,8 +12,17 @@ export interface Theme {
     name: string
     is_light: boolean
 
+    /**
+    * App background, other elements that should sit directly on top of the background.
+    */
     lowest: Layer
+    /**
+    * Panels, tabs, other UI surfaces that sit on top of the background.
+    */
     middle: Layer
+    /**
+    * Editors like code buffers, conversation editors, etc.
+    */
     highest: Layer
 
     ramps: RampSet