Merge branch 'main' into update-assistant-styles

Nate Butler created

Change summary

.github/workflows/release_actions.yml                   |   4 
Cargo.lock                                              | 488 ++++++++
Cargo.toml                                              |   2 
assets/keymaps/atom.json                                |   4 
assets/keymaps/sublime_text.json                        |   4 
assets/keymaps/textmate.json                            |   6 
assets/keymaps/vim.json                                 |  18 
assets/settings/default.json                            |   8 
assets/sounds/joined_call.wav                           |   0 
assets/sounds/leave_call.wav                            |   0 
assets/sounds/mute.wav                                  |   0 
assets/sounds/start_screenshare.wav                     |   0 
assets/sounds/stop_screenshare.wav                      |   0 
assets/sounds/unmute.wav                                |   0 
crates/ai/src/ai.rs                                     |   4 
crates/ai/src/assistant.rs                              |   7 
crates/audio/Cargo.toml                                 |  23 
crates/audio/src/assets.rs                              |  44 
crates/audio/src/audio.rs                               |  67 +
crates/call/Cargo.toml                                  |   1 
crates/call/src/room.rs                                 | 100 +
crates/cli/src/main.rs                                  |   1 
crates/collab/Cargo.toml                                |   5 
crates/collab/src/db.rs                                 |   5 
crates/collab/src/tests.rs                              |   1 
crates/collab/src/tests/integration_tests.rs            |  55 
crates/collab/src/tests/randomized_integration_tests.rs |   4 
crates/collab_ui/Cargo.toml                             |   2 
crates/collab_ui/src/branch_list.rs                     | 238 ++++
crates/collab_ui/src/collab_titlebar_item.rs            | 266 ++++
crates/collab_ui/src/collab_ui.rs                       |   2 
crates/context_menu/src/context_menu.rs                 |  53 
crates/copilot_button/src/copilot_button.rs             |   7 
crates/db/src/db.rs                                     |   5 
crates/editor/src/display_map.rs                        |   7 
crates/editor/src/display_map/inlay_map.rs              | 256 +++-
crates/editor/src/editor.rs                             | 141 +
crates/editor/src/editor_settings.rs                    |   2 
crates/editor/src/editor_tests.rs                       | 105 +
crates/editor/src/element.rs                            |  72 +
crates/editor/src/inlay_hint_cache.rs                   | 596 ++++++++--
crates/editor/src/movement.rs                           | 112 +
crates/editor/src/test/editor_test_context.rs           |  26 
crates/fs/Cargo.toml                                    |   1 
crates/fs/src/fs.rs                                     |   6 
crates/fs/src/repository.rs                             |  48 
crates/go_to_line/src/go_to_line.rs                     |  11 
crates/gpui/src/app.rs                                  |  23 
crates/gpui/src/app/window.rs                           |  63 
crates/gpui/src/elements/mouse_event_handler.rs         |  13 
crates/gpui/src/gpui.rs                                 |   2 
crates/gpui/src/platform/event.rs                       |   2 
crates/gpui/src/platform/mac/window.rs                  |  59 
crates/gpui/src/scene/mouse_event.rs                    |  22 
crates/gpui/src/scene/mouse_region.rs                   |  37 
crates/language/src/syntax_map.rs                       |   5 
crates/live_kit_client/Cargo.toml                       |   3 
crates/live_kit_client/src/test.rs                      |   7 
crates/picker/src/picker.rs                             |  21 
crates/project/Cargo.toml                               |   2 
crates/project/src/lsp_command.rs                       |  24 
crates/project/src/project.rs                           |  28 
crates/project/src/worktree.rs                          | 123 +
crates/project/src/worktree_tests.rs                    | 163 ++
crates/project_panel/Cargo.toml                         |   1 
crates/project_panel/src/project_panel.rs               | 151 ++
crates/recent_projects/Cargo.toml                       |   1 
crates/recent_projects/src/recent_projects.rs           |  22 
crates/search/src/project_search.rs                     |   3 
crates/settings/Cargo.toml                              |   2 
crates/settings/src/keymap_file.rs                      |  47 
crates/terminal_view/src/terminal_element.rs            |  21 
crates/terminal_view/src/terminal_panel.rs              |   2 
crates/text/src/text.rs                                 |   7 
crates/theme/src/theme.rs                               |   9 
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/pane.rs                            |  24 
crates/workspace/src/workspace.rs                       |  53 
crates/zed/Cargo.toml                                   |   3 
crates/zed/src/assets.rs                                |   1 
crates/zed/src/main.rs                                  |  11 
crates/zed/src/only_instance.rs                         | 103 +
crates/zed/src/zed.rs                                   | 163 +++
docs/zed/syntax-highlighting.md                         |   2 
styles/package-lock.json                                |  64 +
styles/package.json                                     |  15 
styles/src/build_themes.ts                              |  24 
styles/src/build_tokens.ts                              |  19 
styles/src/component/icon_button.ts                     |  25 
styles/src/component/text_button.ts                     |  29 
styles/src/styleTree/editor.ts                          |   0 
styles/src/style_tree/app.ts                            |  71 
styles/src/style_tree/assistant.ts                      |   9 
styles/src/style_tree/command_palette.ts                |   6 
styles/src/style_tree/components.ts                     |   2 
styles/src/style_tree/contact_finder.ts                 |  10 
styles/src/style_tree/contact_list.ts                   |   6 
styles/src/style_tree/contact_notification.ts           |   6 
styles/src/style_tree/contacts_popover.ts               |   6 
styles/src/style_tree/context_menu.ts                   |   6 
styles/src/style_tree/copilot.ts                        |   6 
styles/src/style_tree/editor.ts                         |  12 
styles/src/style_tree/feedback.ts                       |   6 
styles/src/style_tree/hover_popover.ts                  |   6 
styles/src/style_tree/incoming_call_notification.ts     |   8 
styles/src/style_tree/picker.ts                         |  24 
styles/src/style_tree/project_diagnostics.ts            |   6 
styles/src/style_tree/project_panel.ts                  |  23 
styles/src/style_tree/project_shared_notification.ts    |   8 
styles/src/style_tree/search.ts                         |   6 
styles/src/style_tree/shared_screen.ts                  |   6 
styles/src/style_tree/simple_message_notification.ts    |   6 
styles/src/style_tree/status_bar.ts                     |   6 
styles/src/style_tree/tab_bar.ts                        |   6 
styles/src/style_tree/terminal.ts                       |   6 
styles/src/style_tree/titlebar.ts                       |  30 
styles/src/style_tree/toolbar_dropdown_menu.ts          |   6 
styles/src/style_tree/tooltip.ts                        |   6 
styles/src/style_tree/update_notification.ts            |   6 
styles/src/style_tree/welcome.ts                        |   6 
styles/src/style_tree/workspace.ts                      |  14 
styles/src/theme/create_theme.ts                        |   4 
styles/src/theme/index.ts                               |  23 
styles/src/theme/ramps.ts                               |   2 
styles/src/theme/syntax.ts                              |  86 
styles/src/theme/theme_config.ts                        |  67 -
styles/src/theme/tokens/layer.ts                        |   2 
styles/src/theme/tokens/players.ts                      |  28 
styles/src/theme/tokens/theme.ts                        |  28 
styles/tsconfig.json                                    |  16 
140 files changed, 4,099 insertions(+), 1,105 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 🔗

@@ -177,6 +177,28 @@ version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
 
+[[package]]
+name = "alsa"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8512c9117059663fb5606788fbca3619e2a91dac0e3fe516242eab1fa6be5e44"
+dependencies = [
+ "alsa-sys",
+ "bitflags",
+ "libc",
+ "nix",
+]
+
+[[package]]
+name = "alsa-sys"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
 [[package]]
 name = "ambient-authority"
 version = "0.0.1"
@@ -590,6 +612,19 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "audio"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "collections",
+ "gpui",
+ "log",
+ "parking_lot 0.11.2",
+ "rodio",
+ "util",
+]
+
 [[package]]
 name = "auto_update"
 version = "0.1.0"
@@ -756,6 +791,26 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "bindgen"
+version = "0.64.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4"
+dependencies = [
+ "bitflags",
+ "cexpr",
+ "clang-sys",
+ "lazy_static",
+ "lazycell",
+ "peeking_take_while",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "bindgen"
 version = "0.65.1"
@@ -857,7 +912,7 @@ checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7"
 dependencies = [
  "borsh-derive-internal",
  "borsh-schema-derive-internal",
- "proc-macro-crate",
+ "proc-macro-crate 0.1.5",
  "proc-macro2",
  "syn 1.0.109",
 ]
@@ -986,6 +1041,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "async-broadcast",
+ "audio",
  "client",
  "collections",
  "fs",
@@ -1082,6 +1138,12 @@ dependencies = [
  "jobserver",
 ]
 
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
 [[package]]
 name = "cexpr"
 version = "0.6.0"
@@ -1155,7 +1217,7 @@ dependencies = [
  "bitflags",
  "clap_derive 3.2.25",
  "clap_lex 0.2.4",
- "indexmap",
+ "indexmap 1.9.3",
  "once_cell",
  "strsim",
  "termcolor",
@@ -1226,6 +1288,12 @@ version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
 
+[[package]]
+name = "claxon"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688"
+
 [[package]]
 name = "cli"
 version = "0.1.0"
@@ -1333,10 +1401,11 @@ dependencies = [
 
 [[package]]
 name = "collab"
-version = "0.15.0"
+version = "0.16.0"
 dependencies = [
  "anyhow",
  "async-tungstenite",
+ "audio",
  "axum",
  "axum-extra",
  "base64 0.13.1",
@@ -1415,6 +1484,7 @@ dependencies = [
  "picker",
  "postage",
  "project",
+ "recent_projects",
  "serde",
  "serde_derive",
  "settings",
@@ -1444,6 +1514,16 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
 
+[[package]]
+name = "combine"
+version = "4.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4"
+dependencies = [
+ "bytes 1.4.0",
+ "memchr",
+]
+
 [[package]]
 name = "command_palette"
 version = "0.1.0"
@@ -1540,11 +1620,17 @@ name = "core-foundation"
 version = "0.9.3"
 source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85"
 dependencies = [
- "core-foundation-sys",
+ "core-foundation-sys 0.8.3",
  "libc",
  "uuid 0.5.1",
 ]
 
+[[package]]
+name = "core-foundation-sys"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b"
+
 [[package]]
 name = "core-foundation-sys"
 version = "0.8.3"
@@ -1594,6 +1680,51 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "coreaudio-rs"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb17e2d1795b1996419648915df94bc7103c28f7b48062d7acf4652fc371b2ff"
+dependencies = [
+ "bitflags",
+ "core-foundation-sys 0.6.2",
+ "coreaudio-sys",
+]
+
+[[package]]
+name = "coreaudio-sys"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f034b2258e6c4ade2f73bf87b21047567fb913ee9550837c2316d139b0262b24"
+dependencies = [
+ "bindgen 0.64.0",
+]
+
+[[package]]
+name = "cpal"
+version = "0.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d959d90e938c5493000514b446987c07aed46c668faaa7d34d6c7a67b1a578c"
+dependencies = [
+ "alsa",
+ "core-foundation-sys 0.8.3",
+ "coreaudio-rs",
+ "dasp_sample",
+ "jni 0.19.0",
+ "js-sys",
+ "libc",
+ "mach2",
+ "ndk",
+ "ndk-context",
+ "oboe",
+ "once_cell",
+ "parking_lot 0.12.1",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "windows 0.46.0",
+]
+
 [[package]]
 name = "cpp_demangle"
 version = "0.3.5"
@@ -1924,6 +2055,12 @@ dependencies = [
  "parking_lot_core 0.9.7",
 ]
 
+[[package]]
+name = "dasp_sample"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
+
 [[package]]
 name = "data-url"
 version = "0.1.1"
@@ -2233,6 +2370,12 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "equivalent"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
+
 [[package]]
 name = "erased-serde"
 version = "0.3.25"
@@ -2549,6 +2692,7 @@ dependencies = [
  "smol",
  "sum_tree",
  "tempfile",
+ "time 0.3.21",
  "util",
 ]
 
@@ -2793,7 +2937,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
 dependencies = [
  "fallible-iterator",
- "indexmap",
+ "indexmap 1.9.3",
  "stable_deref_trait",
 ]
 
@@ -2889,7 +3033,7 @@ dependencies = [
  "anyhow",
  "async-task",
  "backtrace",
- "bindgen",
+ "bindgen 0.65.1",
  "block",
  "cc",
  "cocoa",
@@ -2961,7 +3105,7 @@ dependencies = [
  "futures-sink",
  "futures-util",
  "http",
- "indexmap",
+ "indexmap 1.9.3",
  "slab",
  "tokio",
  "tokio-util 0.7.8",
@@ -2995,6 +3139,12 @@ dependencies = [
  "ahash 0.8.3",
 ]
 
+[[package]]
+name = "hashbrown"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
+
 [[package]]
 name = "hashlink"
 version = "0.8.1"
@@ -3105,6 +3255,12 @@ dependencies = [
  "digest 0.10.6",
 ]
 
+[[package]]
+name = "hound"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d13cdbd5dbb29f9c88095bbdc2590c9cba0d0a1269b983fef6b2cdd7e9f4db1"
+
 [[package]]
 name = "http"
 version = "0.2.9"
@@ -3213,11 +3369,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c"
 dependencies = [
  "android_system_properties",
- "core-foundation-sys",
+ "core-foundation-sys 0.8.3",
  "iana-time-zone-haiku",
  "js-sys",
  "wasm-bindgen",
- "windows",
+ "windows 0.48.0",
 ]
 
 [[package]]
@@ -3287,6 +3443,16 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "indexmap"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.14.0",
+]
+
 [[package]]
 name = "indoc"
 version = "1.0.9"
@@ -3459,6 +3625,40 @@ dependencies = [
  "cc",
 ]
 
+[[package]]
+name = "jni"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec"
+dependencies = [
+ "cesu8",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror",
+ "walkdir",
+]
+
+[[package]]
+name = "jni"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c"
+dependencies = [
+ "cesu8",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror",
+ "walkdir",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
+
 [[package]]
 name = "jobserver"
 version = "0.1.26"
@@ -3661,6 +3861,17 @@ version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
 
+[[package]]
+name = "lewton"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030"
+dependencies = [
+ "byteorder",
+ "ogg",
+ "tinyvec",
+]
+
 [[package]]
 name = "libc"
 version = "0.2.144"
@@ -3793,7 +4004,6 @@ dependencies = [
  "gpui",
  "hmac 0.12.1",
  "jwt",
- "lazy_static",
  "live_kit_server",
  "log",
  "media",
@@ -3893,6 +4103,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "mach2"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "malloc_buf"
 version = "0.0.6"
@@ -3949,7 +4168,7 @@ name = "media"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "bindgen",
+ "bindgen 0.65.1",
  "block",
  "bytes 1.4.0",
  "core-foundation",
@@ -4207,6 +4426,35 @@ dependencies = [
  "tempfile",
 ]
 
+[[package]]
+name = "ndk"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0"
+dependencies = [
+ "bitflags",
+ "jni-sys",
+ "ndk-sys",
+ "num_enum",
+ "raw-window-handle",
+ "thiserror",
+]
+
+[[package]]
+name = "ndk-context"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
+
+[[package]]
+name = "ndk-sys"
+version = "0.4.1+23.1.7779620"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3"
+dependencies = [
+ "jni-sys",
+]
+
 [[package]]
 name = "net2"
 version = "0.2.38"
@@ -4315,6 +4563,17 @@ dependencies = [
  "zeroize",
 ]
 
+[[package]]
+name = "num-derive"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "num-integer"
 version = "0.1.45"
@@ -4367,6 +4626,27 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "num_enum"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9"
+dependencies = [
+ "num_enum_derive",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799"
+dependencies = [
+ "proc-macro-crate 1.3.1",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "nvim-rs"
 version = "0.5.0"
@@ -4409,7 +4689,7 @@ checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424"
 dependencies = [
  "crc32fast",
  "hashbrown 0.11.2",
- "indexmap",
+ "indexmap 1.9.3",
  "memchr",
 ]
 
@@ -4422,6 +4702,38 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "oboe"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8868cc237ee02e2d9618539a23a8d228b9bb3fc2e7a5b11eed3831de77c395d0"
+dependencies = [
+ "jni 0.20.0",
+ "ndk",
+ "ndk-context",
+ "num-derive",
+ "num-traits",
+ "oboe-sys",
+]
+
+[[package]]
+name = "oboe-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f44155e7fb718d3cfddcf70690b2b51ac4412f347cd9e4fbe511abe9cd7b5f2"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "ogg"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e"
+dependencies = [
+ "byteorder",
+]
+
 [[package]]
 name = "once_cell"
 version = "1.17.1"
@@ -4711,7 +5023,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4"
 dependencies = [
  "fixedbitset",
- "indexmap",
+ "indexmap 1.9.3",
 ]
 
 [[package]]
@@ -4788,7 +5100,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590"
 dependencies = [
  "base64 0.21.0",
- "indexmap",
+ "indexmap 1.9.3",
  "line-wrap",
  "quick-xml",
  "serde",
@@ -4921,6 +5233,16 @@ dependencies = [
  "toml",
 ]
 
+[[package]]
+name = "proc-macro-crate"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+dependencies = [
+ "once_cell",
+ "toml_edit",
+]
+
 [[package]]
 name = "proc-macro-error"
 version = "1.0.4"
@@ -5033,6 +5355,7 @@ dependencies = [
  "language",
  "menu",
  "postage",
+ "pretty_assertions",
  "project",
  "schemars",
  "serde",
@@ -5332,6 +5655,12 @@ dependencies = [
  "rand_core 0.5.1",
 ]
 
+[[package]]
+name = "raw-window-handle"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9"
+
 [[package]]
 name = "rayon"
 version = "1.7.0"
@@ -5375,6 +5704,7 @@ version = "0.1.0"
 dependencies = [
  "db",
  "editor",
+ "futures 0.3.28",
  "fuzzy",
  "gpui",
  "language",
@@ -5615,6 +5945,19 @@ dependencies = [
  "rmp",
 ]
 
+[[package]]
+name = "rodio"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdf1d4dea18dff2e9eb6dca123724f8b60ef44ad74a9ad283cdfe025df7e73fa"
+dependencies = [
+ "claxon",
+ "cpal",
+ "hound",
+ "lewton",
+ "symphonia",
+]
+
 [[package]]
 name = "rope"
 version = "0.1.0"
@@ -6116,7 +6459,7 @@ checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254"
 dependencies = [
  "bitflags",
  "core-foundation",
- "core-foundation-sys",
+ "core-foundation-sys 0.8.3",
  "libc",
  "security-framework-sys",
 ]
@@ -6127,7 +6470,7 @@ version = "2.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4"
 dependencies = [
- "core-foundation-sys",
+ "core-foundation-sys 0.8.3",
  "libc",
 ]
 
@@ -6201,7 +6544,7 @@ version = "1.0.96"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
 dependencies = [
- "indexmap",
+ "indexmap 1.9.3",
  "itoa 1.0.6",
  "ryu",
  "serde",
@@ -6213,7 +6556,7 @@ version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7d7b9ce5b0a63c6269b9623ed828b39259545a6ec0d8a35d6135ad6af6232add"
 dependencies = [
- "indexmap",
+ "indexmap 1.9.3",
  "itoa 0.4.8",
  "ryu",
  "serde",
@@ -6248,7 +6591,7 @@ version = "0.8.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b"
 dependencies = [
- "indexmap",
+ "indexmap 1.9.3",
  "ryu",
  "serde",
  "yaml-rust",
@@ -6622,7 +6965,7 @@ dependencies = [
  "hex",
  "hkdf",
  "hmac 0.12.1",
- "indexmap",
+ "indexmap 1.9.3",
  "itoa 1.0.6",
  "libc",
  "libsqlite3-sys",
@@ -6773,6 +7116,56 @@ dependencies = [
  "siphasher",
 ]
 
+[[package]]
+name = "symphonia"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62e48dba70095f265fdb269b99619b95d04c89e619538138383e63310b14d941"
+dependencies = [
+ "lazy_static",
+ "symphonia-bundle-mp3",
+ "symphonia-core",
+ "symphonia-metadata",
+]
+
+[[package]]
+name = "symphonia-bundle-mp3"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f31d7fece546f1e6973011a9eceae948133bbd18fd3d52f6073b1e38ae6368a"
+dependencies = [
+ "bitflags",
+ "lazy_static",
+ "log",
+ "symphonia-core",
+ "symphonia-metadata",
+]
+
+[[package]]
+name = "symphonia-core"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7c73eb88fee79705268cc7b742c7bc93a7b76e092ab751d0833866970754142"
+dependencies = [
+ "arrayvec 0.7.2",
+ "bitflags",
+ "bytemuck",
+ "lazy_static",
+ "log",
+]
+
+[[package]]
+name = "symphonia-metadata"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89c3e1937e31d0e068bbe829f66b2f2bfaa28d056365279e0ef897172c3320c0"
+dependencies = [
+ "encoding_rs",
+ "lazy_static",
+ "log",
+ "symphonia-core",
+]
+
 [[package]]
 name = "syn"
 version = "1.0.109"
@@ -6818,7 +7211,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a902e9050fca0a5d6877550b769abd2bd1ce8c04634b941dbe2809735e1a1e33"
 dependencies = [
  "cfg-if 1.0.0",
- "core-foundation-sys",
+ "core-foundation-sys 0.8.3",
  "libc",
  "ntapi 0.4.1",
  "once_cell",
@@ -6987,7 +7380,7 @@ dependencies = [
  "anyhow",
  "fs",
  "gpui",
- "indexmap",
+ "indexmap 1.9.3",
  "parking_lot 0.11.2",
  "schemars",
  "serde",
@@ -7293,6 +7686,23 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "toml_datetime"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
+
+[[package]]
+name = "toml_edit"
+version = "0.19.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7"
+dependencies = [
+ "indexmap 2.0.0",
+ "toml_datetime",
+ "winnow",
+]
+
 [[package]]
 name = "tonic"
 version = "0.6.2"
@@ -7332,7 +7742,7 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
 dependencies = [
  "futures-core",
  "futures-util",
- "indexmap",
+ "indexmap 1.9.3",
  "pin-project",
  "pin-project-lite 0.2.9",
  "rand 0.8.5",
@@ -7987,7 +8397,6 @@ dependencies = [
  "indoc",
  "itertools",
  "language",
- "lazy_static",
  "log",
  "nvim-rs",
  "parking_lot 0.11.2",
@@ -8189,7 +8598,7 @@ version = "0.85.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "570460c58b21e9150d2df0eaaedbb7816c34bcec009ae0dcc976e40ba81463e7"
 dependencies = [
- "indexmap",
+ "indexmap 1.9.3",
 ]
 
 [[package]]
@@ -8203,7 +8612,7 @@ dependencies = [
  "backtrace",
  "bincode",
  "cfg-if 1.0.0",
- "indexmap",
+ "indexmap 1.9.3",
  "lazy_static",
  "libc",
  "log",
@@ -8277,7 +8686,7 @@ dependencies = [
  "anyhow",
  "cranelift-entity",
  "gimli 0.26.2",
- "indexmap",
+ "indexmap 1.9.3",
  "log",
  "more-asserts",
  "object 0.28.4",
@@ -8347,7 +8756,7 @@ dependencies = [
  "backtrace",
  "cc",
  "cfg-if 1.0.0",
- "indexmap",
+ "indexmap 1.9.3",
  "libc",
  "log",
  "mach",
@@ -8603,6 +9012,15 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
+[[package]]
+name = "windows"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdacb41e6a96a052c6cb63a144f24900236121c6f63f4f8219fef5977ecb0c25"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
 [[package]]
 name = "windows"
 version = "0.48.0"
@@ -8759,6 +9177,15 @@ version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
 
+[[package]]
+name = "winnow"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "winreg"
 version = "0.10.1"
@@ -8910,7 +9337,7 @@ dependencies = [
 
 [[package]]
 name = "zed"
-version = "0.94.0"
+version = "0.95.0"
 dependencies = [
  "activity_indicator",
  "ai",
@@ -8919,6 +9346,7 @@ dependencies = [
  "async-recursion 0.3.2",
  "async-tar",
  "async-trait",
+ "audio",
  "auto_update",
  "backtrace",
  "breadcrumbs",
@@ -8948,7 +9376,7 @@ dependencies = [
  "gpui",
  "ignore",
  "image",
- "indexmap",
+ "indexmap 1.9.3",
  "install_cli",
  "isahc",
  "journal",

Cargo.toml 🔗

@@ -2,6 +2,7 @@
 members = [
     "crates/activity_indicator",
     "crates/ai",
+    "crates/audio",
     "crates/auto_update",
     "crates/breadcrumbs",
     "crates/call",
@@ -101,6 +102,7 @@ time = { version = "0.3", features = ["serde", "serde-well-known"] }
 toml = { version = "0.5" }
 tree-sitter = "0.20"
 unindent = { version = "0.1.7" }
+pretty_assertions = "1.3.0"
 
 [patch.crates-io]
 tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "49226023693107fba9a1191136a4f47f38cdca73" }

assets/keymaps/atom.json 🔗

@@ -24,9 +24,7 @@
       ],
       "ctrl-shift-down": "editor::AddSelectionBelow",
       "ctrl-shift-up": "editor::AddSelectionAbove",
-      "cmd-shift-backspace": "editor::DeleteToBeginningOfLine",
-      "cmd-shift-enter": "editor::NewlineAbove",
-      "cmd-enter": "editor::NewlineBelow"
+      "cmd-shift-backspace": "editor::DeleteToBeginningOfLine"
     }
   },
   {

assets/keymaps/sublime_text.json 🔗

@@ -24,9 +24,7 @@
       "ctrl-.": "editor::GoToHunk",
       "ctrl-,": "editor::GoToPrevHunk",
       "ctrl-backspace": "editor::DeleteToPreviousWordStart",
-      "ctrl-delete": "editor::DeleteToNextWordEnd",
-      "cmd-shift-enter": "editor::NewlineAbove",
-      "cmd-enter": "editor::NewlineBelow"
+      "ctrl-delete": "editor::DeleteToNextWordEnd"
     }
   },
   {

assets/keymaps/textmate.json 🔗

@@ -12,8 +12,6 @@
       "ctrl-shift-d": "editor::DuplicateLine",
       "cmd-b": "editor::GoToDefinition",
       "cmd-j": "editor::ScrollCursorCenter",
-      "cmd-alt-enter": "editor::NewlineAbove",
-      "cmd-enter": "editor::NewlineBelow",
       "cmd-shift-l": "editor::SelectLine",
       "cmd-shift-t": "outline::Toggle",
       "alt-backspace": "editor::DeleteToPreviousWordStart",
@@ -56,7 +54,9 @@
   },
   {
     "context": "Editor && mode == full",
-    "bindings": {}
+    "bindings": {
+      "cmd-alt-enter": "editor::NewlineAbove"
+    }
   },
   {
     "context": "BufferSearchBar",

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"
+      ]
     }
   }
 ]

assets/settings/default.json 🔗

@@ -71,15 +71,17 @@
     //    "never"
     "show": "auto",
     // Whether to show git diff indicators in the scrollbar.
-    "git_diff": true
+    "git_diff": true,
+    // Whether to show selections in the scrollbar.
+    "selections": true
   },
   // Inlay hint related settings
   "inlay_hints": {
     // Global switch to toggle hints on and off, switched off by default.
-    "enabled":  false,
+    "enabled": false,
     // Toggle certain types of hints on and off, all switched on by default.
     "show_type_hints": true,
-    "show_parameter_hints":  true,
+    "show_parameter_hints": true,
     // Corresponds to null/None LSP hint type value.
     "show_other_hints": true
   },

crates/ai/src/ai.rs 🔗

@@ -12,6 +12,7 @@ use regex::Regex;
 use serde::{Deserialize, Serialize};
 use std::{
     cmp::Reverse,
+    ffi::OsStr,
     fmt::{self, Display},
     path::PathBuf,
     sync::Arc,
@@ -80,6 +81,9 @@ impl SavedConversationMetadata {
         let mut conversations = Vec::<SavedConversationMetadata>::new();
         while let Some(path) = paths.next().await {
             let path = path?;
+            if path.extension() != Some(OsStr::new("json")) {
+                continue;
+            }
 
             let pattern = r" - \d+.zed.json$";
             let re = Regex::new(pattern).unwrap();

crates/ai/src/assistant.rs 🔗

@@ -147,8 +147,9 @@ impl AssistantPanel {
                                 .await
                                 .log_err()
                                 .unwrap_or_default();
-                            this.update(&mut cx, |this, _| {
-                                this.saved_conversations = saved_conversations
+                            this.update(&mut cx, |this, cx| {
+                                this.saved_conversations = saved_conversations;
+                                cx.notify();
                             })
                             .ok();
                         }
@@ -1911,7 +1912,7 @@ impl ConversationEditor {
         let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
             return;
         };
-        let Some(editor) = workspace.active_item(cx).and_then(|item| item.downcast::<Editor>()) else {
+        let Some(editor) = workspace.active_item(cx).and_then(|item| item.act_as::<Editor>(cx)) else {
             return;
         };
 

crates/audio/Cargo.toml 🔗

@@ -0,0 +1,23 @@
+[package]
+name = "audio"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/audio.rs"
+doctest = false
+
+[dependencies]
+gpui = { path = "../gpui" }
+collections = { path = "../collections" }
+util = { path = "../util" }
+
+rodio = "0.17.1"
+
+log.workspace = true
+
+anyhow.workspace = true
+parking_lot.workspace = true
+
+[dev-dependencies]

crates/audio/src/assets.rs 🔗

@@ -0,0 +1,44 @@
+use std::{io::Cursor, sync::Arc};
+
+use anyhow::Result;
+use collections::HashMap;
+use gpui::{AppContext, AssetSource};
+use rodio::{
+    source::{Buffered, SamplesConverter},
+    Decoder, Source,
+};
+
+type Sound = Buffered<SamplesConverter<Decoder<Cursor<Vec<u8>>>, f32>>;
+
+pub struct SoundRegistry {
+    cache: Arc<parking_lot::Mutex<HashMap<String, Sound>>>,
+    assets: Box<dyn AssetSource>,
+}
+
+impl SoundRegistry {
+    pub fn new(source: impl AssetSource) -> Arc<Self> {
+        Arc::new(Self {
+            cache: Default::default(),
+            assets: Box::new(source),
+        })
+    }
+
+    pub fn global(cx: &AppContext) -> Arc<Self> {
+        cx.global::<Arc<Self>>().clone()
+    }
+
+    pub fn get(&self, name: &str) -> Result<impl Source<Item = f32>> {
+        if let Some(wav) = self.cache.lock().get(name) {
+            return Ok(wav.clone());
+        }
+
+        let path = format!("sounds/{}.wav", name);
+        let bytes = self.assets.load(&path)?.into_owned();
+        let cursor = Cursor::new(bytes);
+        let source = Decoder::new(cursor)?.convert_samples::<f32>().buffered();
+
+        self.cache.lock().insert(name.to_string(), source.clone());
+
+        Ok(source)
+    }
+}

crates/audio/src/audio.rs 🔗

@@ -0,0 +1,67 @@
+use assets::SoundRegistry;
+use gpui::{AppContext, AssetSource};
+use rodio::{OutputStream, OutputStreamHandle};
+use util::ResultExt;
+
+mod assets;
+
+pub fn init(source: impl AssetSource, cx: &mut AppContext) {
+    cx.set_global(SoundRegistry::new(source));
+    cx.set_global(Audio::new());
+}
+
+pub enum Sound {
+    Joined,
+    Leave,
+    Mute,
+    Unmute,
+    StartScreenshare,
+    StopScreenshare,
+}
+
+impl Sound {
+    fn file(&self) -> &'static str {
+        match self {
+            Self::Joined => "joined_call",
+            Self::Leave => "leave_call",
+            Self::Mute => "mute",
+            Self::Unmute => "unmute",
+            Self::StartScreenshare => "start_screenshare",
+            Self::StopScreenshare => "stop_screenshare",
+        }
+    }
+}
+
+pub struct Audio {
+    _output_stream: Option<OutputStream>,
+    output_handle: Option<OutputStreamHandle>,
+}
+
+impl Audio {
+    pub fn new() -> Self {
+        let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip();
+
+        Self {
+            _output_stream,
+            output_handle,
+        }
+    }
+
+    pub fn play_sound(sound: Sound, cx: &AppContext) {
+        if !cx.has_global::<Self>() {
+            return;
+        }
+
+        let this = cx.global::<Self>();
+
+        let Some(output_handle) = this.output_handle.as_ref() else {
+            return;
+        };
+
+        let Some(source) = SoundRegistry::global(cx).get(sound.file()).log_err() else {
+        return;
+    };
+
+        output_handle.play_raw(source).log_err();
+    }
+}

crates/call/Cargo.toml 🔗

@@ -19,6 +19,7 @@ test-support = [
 ]
 
 [dependencies]
+audio = { path = "../audio" }
 client = { path = "../client" }
 collections = { path = "../collections" }
 gpui = { path = "../gpui" }

crates/call/src/room.rs 🔗

@@ -3,6 +3,7 @@ use crate::{
     IncomingCall,
 };
 use anyhow::{anyhow, Result};
+use audio::{Audio, Sound};
 use client::{
     proto::{self, PeerId},
     Client, TypedEnvelope, User, UserStore,
@@ -151,6 +152,7 @@ impl Room {
             let connect = room.connect(&connection_info.server_url, &connection_info.token);
             cx.spawn(|this, mut cx| async move {
                 connect.await?;
+
                 this.update(&mut cx, |this, cx| this.share_microphone(cx))
                     .await?;
 
@@ -176,6 +178,8 @@ impl Room {
         let maintain_connection =
             cx.spawn_weak(|this, cx| Self::maintain_connection(this, client.clone(), cx).log_err());
 
+        Audio::play_sound(Sound::Joined, cx);
+
         Self {
             id,
             live_kit: live_kit_room,
@@ -265,6 +269,7 @@ impl Room {
                 room.apply_room_update(room_proto, cx)?;
                 anyhow::Ok(())
             })?;
+
             Ok(room)
         })
     }
@@ -306,6 +311,8 @@ impl Room {
             }
         }
 
+        Audio::play_sound(Sound::Leave, cx);
+
         self.status = RoomStatus::Offline;
         self.remote_participants.clear();
         self.pending_participants.clear();
@@ -656,6 +663,8 @@ impl Room {
                                 },
                             );
 
+                            Audio::play_sound(Sound::Joined, cx);
+
                             if let Some(live_kit) = this.live_kit.as_ref() {
                                 let video_tracks =
                                     live_kit.room.remote_video_tracks(&user.id.to_string());
@@ -922,6 +931,7 @@ impl Room {
         cx.spawn(|this, mut cx| async move {
             let project =
                 Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?;
+
             this.update(&mut cx, |this, cx| {
                 this.joined_projects.retain(|project| {
                     if let Some(project) = project.upgrade(cx) {
@@ -1212,6 +1222,9 @@ impl Room {
                                 };
                                 cx.notify();
                             }
+
+                            Audio::play_sound(Sound::StartScreenshare, cx);
+
                             Ok(())
                         }
                         Err(error) => {
@@ -1227,38 +1240,20 @@ impl Room {
                 })
         })
     }
-    fn set_mute(
-        live_kit: &mut LiveKitRoom,
-        should_mute: bool,
-        cx: &mut ModelContext<Self>,
-    ) -> Result<Task<Result<()>>> {
-        if !should_mute {
-            // clear user muting state.
-            live_kit.muted_by_user = false;
-        }
-        match &mut live_kit.microphone_track {
-            LocalTrack::None => Err(anyhow!("microphone was not shared")),
-            LocalTrack::Pending { muted, .. } => {
-                *muted = should_mute;
-                cx.notify();
-                Ok(Task::Ready(Some(Ok(()))))
-            }
-            LocalTrack::Published {
-                track_publication,
-                muted,
-            } => {
-                *muted = should_mute;
-                cx.notify();
-                Ok(cx.background().spawn(track_publication.set_mute(*muted)))
-            }
-        }
-    }
+
     pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> {
         let should_mute = !self.is_muted();
         if let Some(live_kit) = self.live_kit.as_mut() {
-            let ret = Self::set_mute(live_kit, should_mute, cx);
+            let (ret_task, old_muted) = live_kit.set_mute(should_mute, cx)?;
             live_kit.muted_by_user = should_mute;
-            ret
+
+            if old_muted == true && live_kit.deafened == true {
+                if let Some(task) = self.toggle_deafen(cx).ok() {
+                    task.detach();
+                }
+            }
+
+            Ok(ret_task)
         } else {
             Err(anyhow!("LiveKit not started"))
         }
@@ -1274,7 +1269,7 @@ impl Room {
             // When deafening, mute user's mic as well.
             // When undeafening, unmute user's mic unless it was manually muted prior to deafening.
             if live_kit.deafened || !live_kit.muted_by_user {
-                mute_task = Some(Self::set_mute(live_kit, live_kit.deafened, cx)?);
+                mute_task = Some(live_kit.set_mute(live_kit.deafened, cx)?.0);
             };
             for participant in self.remote_participants.values() {
                 for track in live_kit
@@ -1319,6 +1314,8 @@ impl Room {
             } => {
                 live_kit.room.unpublish_track(track_publication);
                 cx.notify();
+
+                Audio::play_sound(Sound::StopScreenshare, cx);
                 Ok(())
             }
         }
@@ -1347,6 +1344,51 @@ struct LiveKitRoom {
     _maintain_tracks: [Task<()>; 2],
 }
 
+impl LiveKitRoom {
+    fn set_mute(
+        self: &mut LiveKitRoom,
+        should_mute: bool,
+        cx: &mut ModelContext<Room>,
+    ) -> Result<(Task<Result<()>>, bool)> {
+        if !should_mute {
+            // clear user muting state.
+            self.muted_by_user = false;
+        }
+
+        let (result, old_muted) = match &mut self.microphone_track {
+            LocalTrack::None => Err(anyhow!("microphone was not shared")),
+            LocalTrack::Pending { muted, .. } => {
+                let old_muted = *muted;
+                *muted = should_mute;
+                cx.notify();
+                Ok((Task::Ready(Some(Ok(()))), old_muted))
+            }
+            LocalTrack::Published {
+                track_publication,
+                muted,
+            } => {
+                let old_muted = *muted;
+                *muted = should_mute;
+                cx.notify();
+                Ok((
+                    cx.background().spawn(track_publication.set_mute(*muted)),
+                    old_muted,
+                ))
+            }
+        }?;
+
+        if old_muted != should_mute {
+            if should_mute {
+                Audio::play_sound(Sound::Mute, cx);
+            } else {
+                Audio::play_sound(Sound::Unmute, cx);
+            }
+        }
+
+        Ok((result, old_muted))
+    }
+}
+
 enum LocalTrack {
     None,
     Pending {

crates/cli/src/main.rs 🔗

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

crates/collab/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
 default-run = "collab"
 edition = "2021"
 name = "collab"
-version = "0.15.0"
+version = "0.16.0"
 publish = false
 
 [[bin]]
@@ -57,6 +57,7 @@ tracing-log = "0.1.3"
 tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
 
 [dev-dependencies]
+audio = { path = "../audio" }
 collections = { path = "../collections", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 call = { path = "../call", features = ["test-support"] }
@@ -67,7 +68,7 @@ fs = { path = "../fs", features = ["test-support"] }
 git = { path = "../git", features = ["test-support"] }
 live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
 lsp = { path = "../lsp", features = ["test-support"] }
-pretty_assertions = "1.3.0"
+pretty_assertions.workspace = true
 project = { path = "../project", features = ["test-support"] }
 rpc = { path = "../rpc", features = ["test-support"] }
 settings = { path = "../settings", features = ["test-support"] }

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.rs 🔗

@@ -203,6 +203,7 @@ impl TestServer {
             language::init(cx);
             editor::init_settings(cx);
             workspace::init(app_state.clone(), cx);
+            audio::init((), cx);
             call::init(client.clone(), user_store.clone(), cx);
         });
 

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

@@ -18,7 +18,7 @@ use gpui::{
 };
 use indoc::indoc;
 use language::{
-    language_settings::{AllLanguageSettings, Formatter, InlayHintKind, InlayHintSettings},
+    language_settings::{AllLanguageSettings, Formatter, InlayHintSettings},
     tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
     LanguageConfig, OffsetRangeExt, Point, Rope,
 };
@@ -7843,7 +7843,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
             });
         });
     });
-    let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
 
     let mut language = Language::new(
         LanguageConfig {
@@ -7955,10 +7954,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
             "Host should get its first hints when opens an editor"
         );
         let inlay_cache = editor.inlay_hint_cache();
-        assert_eq!(
-            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
-            "Cache should use editor settings to get the allowed hint kinds"
-        );
         assert_eq!(
             inlay_cache.version, edits_made,
             "Host editor update the cache version after every cache/view change",
@@ -7982,10 +7977,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
             "Client should get its first hints when opens an editor"
         );
         let inlay_cache = editor.inlay_hint_cache();
-        assert_eq!(
-            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
-            "Cache should use editor settings to get the allowed hint kinds"
-        );
         assert_eq!(
             inlay_cache.version, edits_made,
             "Guest editor update the cache version after every cache/view change"
@@ -8007,10 +7998,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
             "Host should get hints from the 1st edit and 1st LSP query"
         );
         let inlay_cache = editor.inlay_hint_cache();
-        assert_eq!(
-            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
-            "Inlay kinds settings never change during the test"
-        );
         assert_eq!(inlay_cache.version, edits_made);
     });
     editor_b.update(cx_b, |editor, _| {
@@ -8025,10 +8012,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
             "Guest should get hints the 1st edit and 2nd LSP query"
         );
         let inlay_cache = editor.inlay_hint_cache();
-        assert_eq!(
-            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
-            "Inlay kinds settings never change during the test"
-        );
         assert_eq!(inlay_cache.version, edits_made);
     });
 
@@ -8054,10 +8037,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
 4th query was made by guest (but not applied) due to cache invalidation logic"
         );
         let inlay_cache = editor.inlay_hint_cache();
-        assert_eq!(
-            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
-            "Inlay kinds settings never change during the test"
-        );
         assert_eq!(inlay_cache.version, edits_made);
     });
     editor_b.update(cx_b, |editor, _| {
@@ -8074,10 +8053,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
             "Guest should get hints from 3rd edit, 6th LSP query"
         );
         let inlay_cache = editor.inlay_hint_cache();
-        assert_eq!(
-            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
-            "Inlay kinds settings never change during the test"
-        );
         assert_eq!(inlay_cache.version, edits_made);
     });
 
@@ -8103,10 +8078,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
             "Host should react to /refresh LSP request and get new hints from 7th LSP query"
         );
         let inlay_cache = editor.inlay_hint_cache();
-        assert_eq!(
-            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
-            "Inlay kinds settings never change during the test"
-        );
         assert_eq!(
             inlay_cache.version, edits_made,
             "Host should accepted all edits and bump its cache version every time"
@@ -8128,10 +8099,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
             "Guest should get a /refresh LSP request propagated by host and get new hints from 8th LSP query"
         );
         let inlay_cache = editor.inlay_hint_cache();
-        assert_eq!(
-            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
-            "Inlay kinds settings never change during the test"
-        );
         assert_eq!(
             inlay_cache.version,
             edits_made,
@@ -8164,9 +8131,9 @@ async fn test_inlay_hint_refresh_is_forwarded(
             store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
                 settings.defaults.inlay_hints = Some(InlayHintSettings {
                     enabled: false,
-                    show_type_hints: true,
+                    show_type_hints: false,
                     show_parameter_hints: false,
-                    show_other_hints: true,
+                    show_other_hints: false,
                 })
             });
         });
@@ -8177,13 +8144,12 @@ async fn test_inlay_hint_refresh_is_forwarded(
                 settings.defaults.inlay_hints = Some(InlayHintSettings {
                     enabled: true,
                     show_type_hints: true,
-                    show_parameter_hints: false,
+                    show_parameter_hints: true,
                     show_other_hints: true,
                 })
             });
         });
     });
-    let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
 
     let mut language = Language::new(
         LanguageConfig {
@@ -8299,10 +8265,6 @@ async fn test_inlay_hint_refresh_is_forwarded(
             "Host should get no hints due to them turned off"
         );
         let inlay_cache = editor.inlay_hint_cache();
-        assert_eq!(
-            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
-            "Host should have allowed hint kinds set despite hints are off"
-        );
         assert_eq!(
             inlay_cache.version, 0,
             "Host should not increment its cache version due to no changes",
@@ -8318,10 +8280,6 @@ async fn test_inlay_hint_refresh_is_forwarded(
             "Client should get its first hints when opens an editor"
         );
         let inlay_cache = editor.inlay_hint_cache();
-        assert_eq!(
-            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
-            "Cache should use editor settings to get the allowed hint kinds"
-        );
         assert_eq!(
             inlay_cache.version, edits_made,
             "Guest editor update the cache version after every cache/view change"
@@ -8339,7 +8297,6 @@ async fn test_inlay_hint_refresh_is_forwarded(
             "Host should get nop hints due to them turned off, even after the /refresh"
         );
         let inlay_cache = editor.inlay_hint_cache();
-        assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
         assert_eq!(
             inlay_cache.version, 0,
             "Host should not increment its cache version due to no changes",
@@ -8355,10 +8312,6 @@ async fn test_inlay_hint_refresh_is_forwarded(
             "Guest should get a /refresh LSP request propagated by host despite host hints are off"
         );
         let inlay_cache = editor.inlay_hint_cache();
-        assert_eq!(
-            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
-            "Inlay kinds settings never change during the test"
-        );
         assert_eq!(
             inlay_cache.version, edits_made,
             "Guest should accepted all edits and bump its cache version every time"

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(

crates/collab_ui/Cargo.toml 🔗

@@ -35,6 +35,7 @@ gpui = { path = "../gpui" }
 menu = { path = "../menu" }
 picker = { path = "../picker" }
 project = { path = "../project" }
+recent_projects = {path = "../recent_projects"}
 settings = { path = "../settings" }
 theme = { path = "../theme" }
 theme_selector = { path = "../theme_selector" }
@@ -42,6 +43,7 @@ util = { path = "../util" }
 workspace = { path = "../workspace" }
 zed-actions = {path = "../zed-actions"}
 
+
 anyhow.workspace = true
 futures.workspace = true
 log.workspace = true

crates/collab_ui/src/branch_list.rs 🔗

@@ -0,0 +1,238 @@
+use anyhow::{anyhow, bail};
+use fuzzy::{StringMatch, StringMatchCandidate};
+use gpui::{elements::*, AppContext, MouseState, Task, ViewContext, ViewHandle};
+use picker::{Picker, PickerDelegate, PickerEvent};
+use std::{ops::Not, sync::Arc};
+use util::ResultExt;
+use workspace::{Toast, Workspace};
+
+pub fn init(cx: &mut AppContext) {
+    Picker::<BranchListDelegate>::init(cx);
+}
+
+pub type BranchList = Picker<BranchListDelegate>;
+
+pub fn build_branch_list(
+    workspace: ViewHandle<Workspace>,
+    cx: &mut ViewContext<BranchList>,
+) -> BranchList {
+    Picker::new(
+        BranchListDelegate {
+            matches: vec![],
+            workspace,
+            selected_index: 0,
+            last_query: String::default(),
+        },
+        cx,
+    )
+    .with_theme(|theme| theme.picker.clone())
+}
+
+pub struct BranchListDelegate {
+    matches: Vec<StringMatch>,
+    workspace: ViewHandle<Workspace>,
+    selected_index: usize,
+    last_query: String,
+}
+
+impl PickerDelegate for BranchListDelegate {
+    fn placeholder_text(&self) -> Arc<str> {
+        "Select branch...".into()
+    }
+
+    fn match_count(&self) -> usize {
+        self.matches.len()
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_index
+    }
+
+    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
+        self.selected_index = ix;
+    }
+
+    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
+        cx.spawn(move |picker, mut cx| async move {
+            let Some(candidates) = picker
+                .read_with(&mut cx, |view, cx| {
+                    let delegate = view.delegate();
+                    let project = delegate.workspace.read(cx).project().read(&cx);
+                    let mut cwd =
+                    project
+                        .visible_worktrees(cx)
+                        .next()
+                        .unwrap()
+                        .read(cx)
+                        .abs_path()
+                        .to_path_buf();
+                    cwd.push(".git");
+                    let Some(repo) = project.fs().open_repo(&cwd) else {bail!("Project does not have associated git repository.")};
+                    let mut branches = repo
+                        .lock()
+                        .branches()?;
+                    const RECENT_BRANCHES_COUNT: usize = 10;
+                    if query.is_empty() && branches.len() > RECENT_BRANCHES_COUNT {
+                        // Truncate list of recent branches
+                        // Do a partial sort to show recent-ish branches first.
+                        branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
+                            rhs.unix_timestamp.cmp(&lhs.unix_timestamp)
+                        });
+                        branches.truncate(RECENT_BRANCHES_COUNT);
+                        branches.sort_unstable_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
+                    }
+                    Ok(branches
+                        .iter()
+                        .cloned()
+                        .enumerate()
+                        .map(|(ix, command)| StringMatchCandidate {
+                            id: ix,
+                            char_bag: command.name.chars().collect(),
+                            string: command.name.into(),
+                        })
+                        .collect::<Vec<_>>())
+                })
+                .log_err() else { return; };
+            let Some(candidates) = candidates.log_err() else {return;};
+            let matches = if query.is_empty() {
+                candidates
+                    .into_iter()
+                    .enumerate()
+                    .map(|(index, candidate)| StringMatch {
+                        candidate_id: index,
+                        string: candidate.string,
+                        positions: Vec::new(),
+                        score: 0.0,
+                    })
+                    .collect()
+            } else {
+                fuzzy::match_strings(
+                    &candidates,
+                    &query,
+                    true,
+                    10000,
+                    &Default::default(),
+                    cx.background(),
+                )
+                .await
+            };
+            picker
+                .update(&mut cx, |picker, _| {
+                    let delegate = picker.delegate_mut();
+                    delegate.matches = matches;
+                    if delegate.matches.is_empty() {
+                        delegate.selected_index = 0;
+                    } else {
+                        delegate.selected_index =
+                            core::cmp::min(delegate.selected_index, delegate.matches.len() - 1);
+                    }
+                    delegate.last_query = query;
+                })
+                .log_err();
+        })
+    }
+
+    fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+        let current_pick = self.selected_index();
+        let current_pick = self.matches[current_pick].string.clone();
+        cx.spawn(|picker, mut cx| async move {
+            picker.update(&mut cx, |this, cx| {
+                let project = this.delegate().workspace.read(cx).project().read(cx);
+                let mut cwd = project
+                .visible_worktrees(cx)
+                .next()
+                .ok_or_else(|| anyhow!("There are no visisible worktrees."))?
+                .read(cx)
+                .abs_path()
+                .to_path_buf();
+                cwd.push(".git");
+                let status = project
+                    .fs()
+                    .open_repo(&cwd)
+                    .ok_or_else(|| anyhow!("Could not open repository at path `{}`", cwd.as_os_str().to_string_lossy()))?
+                    .lock()
+                    .change_branch(&current_pick);
+                if status.is_err() {
+                    const GIT_CHECKOUT_FAILURE_ID: usize = 2048;
+                    this.delegate().workspace.update(cx, |model, ctx| {
+                        model.show_toast(
+                            Toast::new(
+                                GIT_CHECKOUT_FAILURE_ID,
+                                format!("Failed to checkout branch '{current_pick}', check for conflicts or unstashed files"),
+                            ),
+                            ctx,
+                        )
+                    });
+                    status?;
+                }
+                cx.emit(PickerEvent::Dismiss);
+
+                Ok::<(), anyhow::Error>(())
+            }).log_err();
+        }).detach();
+    }
+
+    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+        cx.emit(PickerEvent::Dismiss);
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        mouse_state: &mut MouseState,
+        selected: bool,
+        cx: &gpui::AppContext,
+    ) -> AnyElement<Picker<Self>> {
+        const DISPLAYED_MATCH_LEN: usize = 29;
+        let theme = &theme::current(cx);
+        let hit = &self.matches[ix];
+        let shortened_branch_name = util::truncate_and_trailoff(&hit.string, DISPLAYED_MATCH_LEN);
+        let highlights = hit
+            .positions
+            .iter()
+            .copied()
+            .filter(|index| index < &DISPLAYED_MATCH_LEN)
+            .collect();
+        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
+        Flex::row()
+            .with_child(
+                Label::new(shortened_branch_name.clone(), style.label.clone())
+                    .with_highlights(highlights)
+                    .contained()
+                    .aligned()
+                    .left(),
+            )
+            .contained()
+            .with_style(style.container)
+            .constrained()
+            .with_height(theme.contact_finder.row_height)
+            .into_any()
+    }
+    fn render_header(
+        &self,
+        cx: &mut ViewContext<Picker<Self>>,
+    ) -> Option<AnyElement<Picker<Self>>> {
+        let theme = &theme::current(cx);
+        let style = theme.picker.header.clone();
+        let label = if self.last_query.is_empty() {
+            Flex::row()
+                .with_child(Label::new("Recent branches", style.label.clone()))
+                .contained()
+                .with_style(style.container)
+        } else {
+            Flex::row()
+                .with_child(Label::new("Branches", style.label.clone()))
+                .with_children(self.matches.is_empty().not().then(|| {
+                    let suffix = if self.matches.len() == 1 { "" } else { "es" };
+                    Label::new(
+                        format!("{} match{}", self.matches.len(), suffix),
+                        style.label,
+                    )
+                    .flex_float()
+                }))
+                .contained()
+                .with_style(style.container)
+        };
+        Some(label.into_any())
+    }
+}

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -1,5 +1,8 @@
 use crate::{
-    contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
+    branch_list::{build_branch_list, BranchList},
+    contact_notification::ContactNotification,
+    contacts_popover,
+    face_pile::FacePile,
     toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute,
     ToggleScreenSharing,
 };
@@ -18,19 +21,25 @@ use gpui::{
     AppContext, Entity, ImageData, LayoutContext, ModelHandle, SceneBuilder, Subscription, View,
     ViewContext, ViewHandle, WeakViewHandle,
 };
+use picker::PickerEvent;
 use project::{Project, RepositoryEntry};
+use recent_projects::{build_recent_projects, RecentProjects};
 use std::{ops::Range, sync::Arc};
 use theme::{AvatarStyle, Theme};
 use util::ResultExt;
-use workspace::{FollowNextCollaborator, Workspace};
+use workspace::{FollowNextCollaborator, Workspace, WORKSPACE_DB};
 
-// const MAX_TITLE_LENGTH: usize = 75;
+const MAX_PROJECT_NAME_LENGTH: usize = 40;
+const MAX_BRANCH_NAME_LENGTH: usize = 40;
 
 actions!(
     collab,
     [
         ToggleContactsMenu,
         ToggleUserMenu,
+        ToggleVcsMenu,
+        ToggleProjectMenu,
+        SwitchBranch,
         ShareProject,
         UnshareProject,
     ]
@@ -41,6 +50,8 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(CollabTitlebarItem::share_project);
     cx.add_action(CollabTitlebarItem::unshare_project);
     cx.add_action(CollabTitlebarItem::toggle_user_menu);
+    cx.add_action(CollabTitlebarItem::toggle_vcs_menu);
+    cx.add_action(CollabTitlebarItem::toggle_project_menu);
 }
 
 pub struct CollabTitlebarItem {
@@ -49,6 +60,8 @@ pub struct CollabTitlebarItem {
     client: Arc<Client>,
     workspace: WeakViewHandle<Workspace>,
     contacts_popover: Option<ViewHandle<ContactsPopover>>,
+    branch_popover: Option<ViewHandle<BranchList>>,
+    project_popover: Option<ViewHandle<recent_projects::RecentProjects>>,
     user_menu: ViewHandle<ContextMenu>,
     _subscriptions: Vec<Subscription>,
 }
@@ -69,12 +82,11 @@ impl View for CollabTitlebarItem {
             return Empty::new().into_any();
         };
 
-        let project = self.project.read(cx);
         let theme = theme::current(cx).clone();
         let mut left_container = Flex::row();
         let mut right_container = Flex::row().align_children_center();
 
-        left_container.add_child(self.collect_title_root_names(&project, theme.clone(), cx));
+        left_container.add_child(self.collect_title_root_names(theme.clone(), cx));
 
         let user = self.user_store.read(cx).current_user();
         let peer_id = self.client.peer_id();
@@ -182,52 +194,105 @@ impl CollabTitlebarItem {
                 menu.set_position_mode(OverlayPositionMode::Local);
                 menu
             }),
+            branch_popover: None,
+            project_popover: None,
             _subscriptions: subscriptions,
         }
     }
 
     fn collect_title_root_names(
         &self,
-        project: &Project,
         theme: Arc<Theme>,
-        cx: &ViewContext<Self>,
+        cx: &mut ViewContext<Self>,
     ) -> AnyElement<Self> {
-        let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| {
-            let worktree = worktree.read(cx);
-            (worktree.root_name(), worktree.root_git_entry())
-        });
+        let project = self.project.read(cx);
 
-        let (name, entry) = names_and_branches.next().unwrap_or(("", None));
+        let (name, entry) = {
+            let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| {
+                let worktree = worktree.read(cx);
+                (worktree.root_name(), worktree.root_git_entry())
+            });
+
+            names_and_branches.next().unwrap_or(("", None))
+        };
+
+        let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
         let branch_prepended = entry
             .as_ref()
             .and_then(RepositoryEntry::branch)
-            .map(|branch| format!("/{branch}"));
-        let text_style = theme.titlebar.title.clone();
+            .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH));
+        let project_style = theme.titlebar.project_menu_button.clone();
+        let git_style = theme.titlebar.git_menu_button.clone();
+        let divider_style = theme.titlebar.project_name_divider.clone();
         let item_spacing = theme.titlebar.item_spacing;
 
-        let mut highlight = text_style.clone();
-        highlight.color = theme.titlebar.highlight_color;
-
-        let style = LabelStyle {
-            text: text_style,
-            highlight_text: Some(highlight),
-        };
         let mut ret = Flex::row().with_child(
-            Label::new(name.to_owned(), style.clone())
-                .with_highlights((0..name.len()).into_iter().collect())
-                .contained()
-                .aligned()
-                .left()
-                .into_any_named("title-project-name"),
+            Stack::new()
+                .with_child(
+                    MouseEventHandler::<ToggleProjectMenu, Self>::new(0, cx, |mouse_state, _| {
+                        let style = project_style
+                            .in_state(self.project_popover.is_some())
+                            .style_for(mouse_state);
+                        Label::new(name, style.text.clone())
+                            .contained()
+                            .with_style(style.container)
+                            .aligned()
+                            .left()
+                            .into_any_named("title-project-name")
+                    })
+                    .with_cursor_style(CursorStyle::PointingHand)
+                    .on_down(MouseButton::Left, move |_, this, cx| {
+                        this.toggle_project_menu(&Default::default(), cx)
+                    })
+                    .on_click(MouseButton::Left, move |_, _, _| {}),
+                )
+                .with_children(self.render_project_popover_host(&theme.titlebar, cx)),
         );
         if let Some(git_branch) = branch_prepended {
             ret = ret.with_child(
-                Label::new(git_branch, style)
-                    .contained()
-                    .with_margin_right(item_spacing)
-                    .aligned()
-                    .left()
-                    .into_any_named("title-project-branch"),
+                Flex::row()
+                    .with_child(
+                        Label::new("/", divider_style.text)
+                            .contained()
+                            .with_style(divider_style.container)
+                            .aligned()
+                            .left(),
+                    )
+                    .with_child(
+                        Stack::new()
+                            .with_child(
+                                MouseEventHandler::<ToggleVcsMenu, Self>::new(
+                                    0,
+                                    cx,
+                                    |mouse_state, cx| {
+                                        enum BranchPopoverTooltip {}
+                                        let style = git_style
+                                            .in_state(self.branch_popover.is_some())
+                                            .style_for(mouse_state);
+                                        Label::new(git_branch, style.text.clone())
+                                            .contained()
+                                            .with_style(style.container.clone())
+                                            .with_margin_right(item_spacing)
+                                            .aligned()
+                                            .left()
+                                            .with_tooltip::<BranchPopoverTooltip>(
+                                                0,
+                                                "Recent branches".into(),
+                                                None,
+                                                theme.tooltip.clone(),
+                                                cx,
+                                            )
+                                            .into_any_named("title-project-branch")
+                                    },
+                                )
+                                .with_cursor_style(CursorStyle::PointingHand)
+                                .on_down(MouseButton::Left, move |_, this, cx| {
+                                    this.toggle_vcs_menu(&Default::default(), cx)
+                                })
+                                .on_click(MouseButton::Left, move |_, _, _| {}),
+                            )
+                            .with_children(self.render_branches_popover_host(&theme.titlebar, cx)),
+                    ),
             )
         }
         ret.into_any()
@@ -317,10 +382,138 @@ impl CollabTitlebarItem {
                     ),
                 ]
             };
-            user_menu.show(Default::default(), AnchorCorner::TopRight, items, cx);
+            user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
         });
     }
+    fn render_branches_popover_host<'a>(
+        &'a self,
+        _theme: &'a theme::Titlebar,
+        cx: &'a mut ViewContext<Self>,
+    ) -> Option<AnyElement<Self>> {
+        self.branch_popover.as_ref().map(|child| {
+            let theme = theme::current(cx).clone();
+            let child = ChildView::new(child, cx);
+            let child = MouseEventHandler::<BranchList, Self>::new(0, cx, |_, _| {
+                child
+                    .flex(1., true)
+                    .contained()
+                    .constrained()
+                    .with_width(theme.contacts_popover.width)
+                    .with_height(theme.contacts_popover.height)
+            })
+            .on_click(MouseButton::Left, |_, _, _| {})
+            .on_down_out(MouseButton::Left, move |_, this, cx| {
+                this.branch_popover.take();
+                cx.emit(());
+                cx.notify();
+            })
+            .contained()
+            .into_any();
+
+            Overlay::new(child)
+                .with_fit_mode(OverlayFitMode::SwitchAnchor)
+                .with_anchor_corner(AnchorCorner::TopLeft)
+                .with_z_index(999)
+                .aligned()
+                .bottom()
+                .left()
+                .into_any()
+        })
+    }
+    fn render_project_popover_host<'a>(
+        &'a self,
+        _theme: &'a theme::Titlebar,
+        cx: &'a mut ViewContext<Self>,
+    ) -> Option<AnyElement<Self>> {
+        self.project_popover.as_ref().map(|child| {
+            let theme = theme::current(cx).clone();
+            let child = ChildView::new(child, cx);
+            let child = MouseEventHandler::<RecentProjects, Self>::new(0, cx, |_, _| {
+                child
+                    .flex(1., true)
+                    .contained()
+                    .constrained()
+                    .with_width(theme.contacts_popover.width)
+                    .with_height(theme.contacts_popover.height)
+            })
+            .on_click(MouseButton::Left, |_, _, _| {})
+            .on_down_out(MouseButton::Left, move |_, this, cx| {
+                this.project_popover.take();
+                cx.emit(());
+                cx.notify();
+            })
+            .into_any();
+
+            Overlay::new(child)
+                .with_fit_mode(OverlayFitMode::SwitchAnchor)
+                .with_anchor_corner(AnchorCorner::TopLeft)
+                .with_z_index(999)
+                .aligned()
+                .bottom()
+                .left()
+                .into_any()
+        })
+    }
+    pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
+        if self.branch_popover.take().is_none() {
+            if let Some(workspace) = self.workspace.upgrade(cx) {
+                let view = cx.add_view(|cx| build_branch_list(workspace, cx));
+                cx.subscribe(&view, |this, _, event, cx| {
+                    match event {
+                        PickerEvent::Dismiss => {
+                            this.branch_popover = None;
+                        }
+                    }
+
+                    cx.notify();
+                })
+                .detach();
+                self.project_popover.take();
+                cx.focus(&view);
+                self.branch_popover = Some(view);
+            }
+        }
+
+        cx.notify();
+    }
 
+    pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext<Self>) {
+        let workspace = self.workspace.clone();
+        if self.project_popover.take().is_none() {
+            cx.spawn(|this, mut cx| async move {
+                let workspaces = WORKSPACE_DB
+                    .recent_workspaces_on_disk()
+                    .await
+                    .unwrap_or_default()
+                    .into_iter()
+                    .map(|(_, location)| location)
+                    .collect();
+
+                let workspace = workspace.clone();
+                this.update(&mut cx, move |this, cx| {
+                    let view = cx.add_view(|cx| build_recent_projects(workspace, workspaces, cx));
+
+                    cx.subscribe(&view, |this, _, event, cx| {
+                        match event {
+                            PickerEvent::Dismiss => {
+                                this.project_popover = None;
+                            }
+                        }
+
+                        cx.notify();
+                    })
+                    .detach();
+                    cx.focus(&view);
+                    this.branch_popover.take();
+                    this.project_popover = Some(view);
+                    cx.notify();
+                })
+                .log_err();
+            })
+            .detach();
+        }
+        cx.notify();
+    }
     fn render_toggle_contacts_button(
         &self,
         theme: &Theme,
@@ -683,6 +876,9 @@ impl CollabTitlebarItem {
                         .into_any()
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
+                .on_down(MouseButton::Left, move |_, this, cx| {
+                    this.user_menu.update(cx, |menu, _| menu.delay_cancel());
+                })
                 .on_click(MouseButton::Left, move |_, this, cx| {
                     this.toggle_user_menu(&Default::default(), cx)
                 })
@@ -730,7 +926,7 @@ impl CollabTitlebarItem {
         self.contacts_popover.as_ref().map(|popover| {
             Overlay::new(ChildView::new(popover, cx))
                 .with_fit_mode(OverlayFitMode::SwitchAnchor)
-                .with_anchor_corner(AnchorCorner::TopRight)
+                .with_anchor_corner(AnchorCorner::TopLeft)
                 .with_z_index(999)
                 .aligned()
                 .bottom()

crates/collab_ui/src/collab_ui.rs 🔗

@@ -1,3 +1,4 @@
+mod branch_list;
 mod collab_titlebar_item;
 mod contact_finder;
 mod contact_list;
@@ -28,6 +29,7 @@ actions!(
 );
 
 pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
+    branch_list::init(cx);
     collab_titlebar_item::init(cx);
     contact_list::init(cx);
     contact_finder::init(cx);

crates/context_menu/src/context_menu.rs 🔗

@@ -124,6 +124,7 @@ pub struct ContextMenu {
     items: Vec<ContextMenuItem>,
     selected_index: Option<usize>,
     visible: bool,
+    delay_cancel: bool,
     previously_focused_view_id: Option<usize>,
     parent_view_id: usize,
     _actions_observation: Subscription,
@@ -178,6 +179,7 @@ impl ContextMenu {
     pub fn new(parent_view_id: usize, cx: &mut ViewContext<Self>) -> Self {
         Self {
             show_count: 0,
+            delay_cancel: false,
             anchor_position: Default::default(),
             anchor_corner: AnchorCorner::TopLeft,
             position_mode: OverlayPositionMode::Window,
@@ -232,15 +234,22 @@ impl ContextMenu {
         }
     }
 
+    pub fn delay_cancel(&mut self) {
+        self.delay_cancel = true;
+    }
+
     fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
-        self.reset(cx);
-        let show_count = self.show_count;
-        cx.defer(move |this, cx| {
-            if cx.handle().is_focused(cx) && this.show_count == show_count {
-                let window_id = cx.window_id();
-                (**cx).focus(window_id, this.previously_focused_view_id.take());
-            }
-        });
+        if !self.delay_cancel {
+            self.reset(cx);
+            let show_count = self.show_count;
+            cx.defer(move |this, cx| {
+                if cx.handle().is_focused(cx) && this.show_count == show_count {
+                    (**cx).focus(this.previously_focused_view_id.take());
+                }
+            });
+        } else {
+            self.delay_cancel = false;
+        }
     }
 
     fn reset(&mut self, cx: &mut ViewContext<Self>) {
@@ -293,6 +302,34 @@ impl ContextMenu {
         }
     }
 
+    pub fn toggle(
+        &mut self,
+        anchor_position: Vector2F,
+        anchor_corner: AnchorCorner,
+        items: Vec<ContextMenuItem>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if self.visible() {
+            self.cancel(&Cancel, cx);
+        } else {
+            let mut items = items.into_iter().peekable();
+            if items.peek().is_some() {
+                self.items = items.collect();
+                self.anchor_position = anchor_position;
+                self.anchor_corner = anchor_corner;
+                self.visible = true;
+                self.show_count += 1;
+                if !cx.is_self_focused() {
+                    self.previously_focused_view_id = cx.focused_view_id();
+                }
+                cx.focus_self();
+            } else {
+                self.visible = false;
+            }
+        }
+        cx.notify();
+    }
+
     pub fn show(
         &mut self,
         anchor_position: Vector2F,

crates/copilot_button/src/copilot_button.rs 🔗

@@ -102,6 +102,9 @@ impl View for CopilotButton {
                     }
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
+                .on_down(MouseButton::Left, |_, this, cx| {
+                    this.popup_menu.update(cx, |menu, _| menu.delay_cancel());
+                })
                 .on_click(MouseButton::Left, {
                     let status = status.clone();
                     move |_, this, cx| match status {
@@ -186,7 +189,7 @@ impl CopilotButton {
         }));
 
         self.popup_menu.update(cx, |menu, cx| {
-            menu.show(
+            menu.toggle(
                 Default::default(),
                 AnchorCorner::BottomRight,
                 menu_options,
@@ -266,7 +269,7 @@ impl CopilotButton {
         menu_options.push(ContextMenuItem::action("Sign Out", SignOut));
 
         self.popup_menu.update(cx, |menu, cx| {
-            menu.show(
+            menu.toggle(
                 Default::default(),
                 AnchorCorner::BottomRight,
                 menu_options,

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/display_map.rs 🔗

@@ -20,7 +20,6 @@ use language::{
 use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
 use sum_tree::{Bias, TreeMap};
 use tab_map::TabMap;
-use text::Rope;
 use wrap_map::WrapMap;
 
 pub use block_map::{
@@ -28,7 +27,7 @@ pub use block_map::{
     BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
 };
 
-pub use self::inlay_map::{Inlay, InlayProperties};
+pub use self::inlay_map::Inlay;
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub enum FoldStatus {
@@ -246,10 +245,10 @@ impl DisplayMap {
         self.inlay_map.current_inlays()
     }
 
-    pub fn splice_inlays<T: Into<Rope>>(
+    pub fn splice_inlays(
         &mut self,
         to_remove: Vec<InlayId>,
-        to_insert: Vec<(InlayId, InlayProperties<T>)>,
+        to_insert: Vec<Inlay>,
         cx: &mut ModelContext<Self>,
     ) {
         if to_remove.is_empty() && to_insert.is_empty() {

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

@@ -2,9 +2,9 @@ use crate::{
     multi_buffer::{MultiBufferChunks, MultiBufferRows},
     Anchor, InlayId, MultiBufferSnapshot, ToOffset,
 };
-use collections::{BTreeMap, BTreeSet, HashMap};
+use collections::{BTreeMap, BTreeSet};
 use gpui::fonts::HighlightStyle;
-use language::{Chunk, Edit, Point, Rope, TextSummary};
+use language::{Chunk, Edit, Point, TextSummary};
 use std::{
     any::TypeId,
     cmp,
@@ -13,13 +13,12 @@ use std::{
     vec,
 };
 use sum_tree::{Bias, Cursor, SumTree};
-use text::Patch;
+use text::{Patch, Rope};
 
 use super::TextHighlights;
 
 pub struct InlayMap {
     snapshot: InlaySnapshot,
-    inlays_by_id: HashMap<InlayId, Inlay>,
     inlays: Vec<Inlay>,
 }
 
@@ -43,10 +42,29 @@ pub struct Inlay {
     pub text: text::Rope,
 }
 
-#[derive(Debug, Clone)]
-pub struct InlayProperties<T> {
-    pub position: Anchor,
-    pub text: T,
+impl Inlay {
+    pub fn hint(id: usize, position: Anchor, hint: &project::InlayHint) -> Self {
+        let mut text = hint.text();
+        if hint.padding_right && !text.ends_with(' ') {
+            text.push(' ');
+        }
+        if hint.padding_left && !text.starts_with(' ') {
+            text.insert(0, ' ');
+        }
+        Self {
+            id: InlayId::Hint(id),
+            position,
+            text: text.into(),
+        }
+    }
+
+    pub fn suggestion<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
+        Self {
+            id: InlayId::Suggestion(id),
+            position,
+            text: text.into(),
+        }
+    }
 }
 
 impl sum_tree::Item for Transform {
@@ -368,7 +386,6 @@ impl InlayMap {
         (
             Self {
                 snapshot: snapshot.clone(),
-                inlays_by_id: HashMap::default(),
                 inlays: Vec::new(),
             },
             snapshot,
@@ -510,45 +527,40 @@ impl InlayMap {
         }
     }
 
-    pub fn splice<T: Into<Rope>>(
+    pub fn splice(
         &mut self,
         to_remove: Vec<InlayId>,
-        to_insert: Vec<(InlayId, InlayProperties<T>)>,
+        to_insert: Vec<Inlay>,
     ) -> (InlaySnapshot, Vec<InlayEdit>) {
         let snapshot = &mut self.snapshot;
         let mut edits = BTreeSet::new();
 
-        self.inlays.retain(|inlay| !to_remove.contains(&inlay.id));
-        for inlay_id in to_remove {
-            if let Some(inlay) = self.inlays_by_id.remove(&inlay_id) {
+        self.inlays.retain(|inlay| {
+            let retain = !to_remove.contains(&inlay.id);
+            if !retain {
                 let offset = inlay.position.to_offset(&snapshot.buffer);
                 edits.insert(offset);
             }
-        }
-
-        for (existing_id, properties) in to_insert {
-            let inlay = Inlay {
-                id: existing_id,
-                position: properties.position,
-                text: properties.text.into(),
-            };
+            retain
+        });
 
+        for inlay_to_insert in to_insert {
             // Avoid inserting empty inlays.
-            if inlay.text.is_empty() {
+            if inlay_to_insert.text.is_empty() {
                 continue;
             }
 
-            self.inlays_by_id.insert(inlay.id, inlay.clone());
-            match self
-                .inlays
-                .binary_search_by(|probe| probe.position.cmp(&inlay.position, &snapshot.buffer))
-            {
+            let offset = inlay_to_insert.position.to_offset(&snapshot.buffer);
+            match self.inlays.binary_search_by(|probe| {
+                probe
+                    .position
+                    .cmp(&inlay_to_insert.position, &snapshot.buffer)
+            }) {
                 Ok(ix) | Err(ix) => {
-                    self.inlays.insert(ix, inlay.clone());
+                    self.inlays.insert(ix, inlay_to_insert);
                 }
             }
 
-            let offset = inlay.position.to_offset(&snapshot.buffer);
             edits.insert(offset);
         }
 
@@ -606,15 +618,19 @@ impl InlayMap {
                 } else {
                     InlayId::Suggestion(post_inc(next_inlay_id))
                 };
-                to_insert.push((
-                    inlay_id,
-                    InlayProperties {
-                        position: snapshot.buffer.anchor_at(position, bias),
-                        text,
-                    },
-                ));
+                to_insert.push(Inlay {
+                    id: inlay_id,
+                    position: snapshot.buffer.anchor_at(position, bias),
+                    text: text.into(),
+                });
             } else {
-                to_remove.push(*self.inlays_by_id.keys().choose(rng).unwrap());
+                to_remove.push(
+                    self.inlays
+                        .iter()
+                        .choose(rng)
+                        .map(|inlay| inlay.id)
+                        .unwrap(),
+                );
             }
         }
         log::info!("removing inlays: {:?}", to_remove);
@@ -1095,6 +1111,7 @@ mod tests {
     use super::*;
     use crate::{InlayId, MultiBuffer};
     use gpui::AppContext;
+    use project::{InlayHint, InlayHintLabel};
     use rand::prelude::*;
     use settings::SettingsStore;
     use std::{cmp::Reverse, env, sync::Arc};
@@ -1102,6 +1119,89 @@ mod tests {
     use text::Patch;
     use util::post_inc;
 
+    #[test]
+    fn test_inlay_properties_label_padding() {
+        assert_eq!(
+            Inlay::hint(
+                0,
+                Anchor::min(),
+                &InlayHint {
+                    label: InlayHintLabel::String("a".to_string()),
+                    buffer_id: 0,
+                    position: text::Anchor::default(),
+                    padding_left: false,
+                    padding_right: false,
+                    tooltip: None,
+                    kind: None,
+                },
+            )
+            .text
+            .to_string(),
+            "a",
+            "Should not pad label if not requested"
+        );
+
+        assert_eq!(
+            Inlay::hint(
+                0,
+                Anchor::min(),
+                &InlayHint {
+                    label: InlayHintLabel::String("a".to_string()),
+                    buffer_id: 0,
+                    position: text::Anchor::default(),
+                    padding_left: true,
+                    padding_right: true,
+                    tooltip: None,
+                    kind: None,
+                },
+            )
+            .text
+            .to_string(),
+            " a ",
+            "Should pad label for every side requested"
+        );
+
+        assert_eq!(
+            Inlay::hint(
+                0,
+                Anchor::min(),
+                &InlayHint {
+                    label: InlayHintLabel::String(" a ".to_string()),
+                    buffer_id: 0,
+                    position: text::Anchor::default(),
+                    padding_left: false,
+                    padding_right: false,
+                    tooltip: None,
+                    kind: None,
+                },
+            )
+            .text
+            .to_string(),
+            " a ",
+            "Should not change already padded label"
+        );
+
+        assert_eq!(
+            Inlay::hint(
+                0,
+                Anchor::min(),
+                &InlayHint {
+                    label: InlayHintLabel::String(" a ".to_string()),
+                    buffer_id: 0,
+                    position: text::Anchor::default(),
+                    padding_left: true,
+                    padding_right: true,
+                    tooltip: None,
+                    kind: None,
+                },
+            )
+            .text
+            .to_string(),
+            " a ",
+            "Should not change already padded label"
+        );
+    }
+
     #[gpui::test]
     fn test_basic_inlays(cx: &mut AppContext) {
         let buffer = MultiBuffer::build_simple("abcdefghi", cx);
@@ -1112,13 +1212,11 @@ mod tests {
 
         let (inlay_snapshot, _) = inlay_map.splice(
             Vec::new(),
-            vec![(
-                InlayId::Hint(post_inc(&mut next_inlay_id)),
-                InlayProperties {
-                    position: buffer.read(cx).snapshot(cx).anchor_after(3),
-                    text: "|123|",
-                },
-            )],
+            vec![Inlay {
+                id: InlayId::Hint(post_inc(&mut next_inlay_id)),
+                position: buffer.read(cx).snapshot(cx).anchor_after(3),
+                text: "|123|".into(),
+            }],
         );
         assert_eq!(inlay_snapshot.text(), "abc|123|defghi");
         assert_eq!(
@@ -1191,20 +1289,16 @@ mod tests {
         let (inlay_snapshot, _) = inlay_map.splice(
             Vec::new(),
             vec![
-                (
-                    InlayId::Hint(post_inc(&mut next_inlay_id)),
-                    InlayProperties {
-                        position: buffer.read(cx).snapshot(cx).anchor_before(3),
-                        text: "|123|",
-                    },
-                ),
-                (
-                    InlayId::Suggestion(post_inc(&mut next_inlay_id)),
-                    InlayProperties {
-                        position: buffer.read(cx).snapshot(cx).anchor_after(3),
-                        text: "|456|",
-                    },
-                ),
+                Inlay {
+                    id: InlayId::Hint(post_inc(&mut next_inlay_id)),
+                    position: buffer.read(cx).snapshot(cx).anchor_before(3),
+                    text: "|123|".into(),
+                },
+                Inlay {
+                    id: InlayId::Suggestion(post_inc(&mut next_inlay_id)),
+                    position: buffer.read(cx).snapshot(cx).anchor_after(3),
+                    text: "|456|".into(),
+                },
             ],
         );
         assert_eq!(inlay_snapshot.text(), "abx|123||456|yDzefghi");
@@ -1389,8 +1483,10 @@ mod tests {
         );
 
         // The inlays can be manually removed.
-        let (inlay_snapshot, _) = inlay_map
-            .splice::<String>(inlay_map.inlays_by_id.keys().copied().collect(), Vec::new());
+        let (inlay_snapshot, _) = inlay_map.splice(
+            inlay_map.inlays.iter().map(|inlay| inlay.id).collect(),
+            Vec::new(),
+        );
         assert_eq!(inlay_snapshot.text(), "abxJKLyDzefghi");
     }
 
@@ -1404,27 +1500,21 @@ mod tests {
         let (inlay_snapshot, _) = inlay_map.splice(
             Vec::new(),
             vec![
-                (
-                    InlayId::Hint(post_inc(&mut next_inlay_id)),
-                    InlayProperties {
-                        position: buffer.read(cx).snapshot(cx).anchor_before(0),
-                        text: "|123|\n",
-                    },
-                ),
-                (
-                    InlayId::Hint(post_inc(&mut next_inlay_id)),
-                    InlayProperties {
-                        position: buffer.read(cx).snapshot(cx).anchor_before(4),
-                        text: "|456|",
-                    },
-                ),
-                (
-                    InlayId::Suggestion(post_inc(&mut next_inlay_id)),
-                    InlayProperties {
-                        position: buffer.read(cx).snapshot(cx).anchor_before(7),
-                        text: "\n|567|\n",
-                    },
-                ),
+                Inlay {
+                    id: InlayId::Hint(post_inc(&mut next_inlay_id)),
+                    position: buffer.read(cx).snapshot(cx).anchor_before(0),
+                    text: "|123|\n".into(),
+                },
+                Inlay {
+                    id: InlayId::Hint(post_inc(&mut next_inlay_id)),
+                    position: buffer.read(cx).snapshot(cx).anchor_before(4),
+                    text: "|456|".into(),
+                },
+                Inlay {
+                    id: InlayId::Suggestion(post_inc(&mut next_inlay_id)),
+                    position: buffer.read(cx).snapshot(cx).anchor_before(7),
+                    text: "\n|567|\n".into(),
+                },
             ],
         );
         assert_eq!(inlay_snapshot.text(), "|123|\nabc\n|456|def\n|567|\n\nghi");
@@ -1514,7 +1604,7 @@ mod tests {
                     (offset, inlay.clone())
                 })
                 .collect::<Vec<_>>();
-            let mut expected_text = Rope::from(buffer_snapshot.text().as_str());
+            let mut expected_text = Rope::from(buffer_snapshot.text());
             for (offset, inlay) in inlays.into_iter().rev() {
                 expected_text.replace(offset..offset, &inlay.text.to_string());
             }

crates/editor/src/editor.rs 🔗

@@ -26,7 +26,7 @@ use aho_corasick::AhoCorasick;
 use anyhow::{anyhow, Result};
 use blink_manager::BlinkManager;
 use client::{ClickhouseEvent, TelemetrySettings};
-use clock::ReplicaId;
+use clock::{Global, ReplicaId};
 use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
 use copilot::Copilot;
 pub use display_map::DisplayPoint;
@@ -190,6 +190,15 @@ pub enum InlayId {
     Hint(usize),
 }
 
+impl InlayId {
+    fn id(&self) -> usize {
+        match self {
+            Self::Suggestion(id) => *id,
+            Self::Hint(id) => *id,
+        }
+    }
+}
+
 actions!(
     editor,
     [
@@ -1195,11 +1204,11 @@ enum GotoDefinitionKind {
     Type,
 }
 
-#[derive(Debug, Copy, Clone)]
+#[derive(Debug, Clone)]
 enum InlayRefreshReason {
     SettingsChange(InlayHintSettings),
     NewLinesShown,
-    ExcerptEdited,
+    BufferEdited(HashSet<Arc<Language>>),
     RefreshRequested,
 }
 
@@ -2026,6 +2035,7 @@ impl Editor {
         }
 
         let selections = self.selections.all_adjusted(cx);
+        let mut brace_inserted = false;
         let mut edits = Vec::new();
         let mut new_selections = Vec::with_capacity(selections.len());
         let mut new_autoclose_regions = Vec::new();
@@ -2084,6 +2094,7 @@ impl Editor {
                                     selection.range(),
                                     format!("{}{}", text, bracket_pair.end).into(),
                                 ));
+                                brace_inserted = true;
                                 continue;
                             }
                         }
@@ -2110,6 +2121,7 @@ impl Editor {
                             selection.end..selection.end,
                             bracket_pair.end.as_str().into(),
                         ));
+                        brace_inserted = true;
                         new_selections.push((
                             Selection {
                                 id: selection.id,
@@ -2177,8 +2189,7 @@ impl Editor {
             let had_active_copilot_suggestion = this.has_active_copilot_suggestion(cx);
             this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
 
-            // When buffer contents is updated and caret is moved, try triggering on type formatting.
-            if settings::get::<EditorSettings>(cx).use_on_type_format {
+            if !brace_inserted && settings::get::<EditorSettings>(cx).use_on_type_format {
                 if let Some(on_type_format_task) =
                     this.trigger_on_type_formatting(text.to_string(), cx)
                 {
@@ -2617,7 +2628,7 @@ impl Editor {
             return;
         }
 
-        let invalidate_cache = match reason {
+        let (invalidate_cache, required_languages) = match reason {
             InlayRefreshReason::SettingsChange(new_settings) => {
                 match self.inlay_hint_cache.update_settings(
                     &self.buffer,
@@ -2633,16 +2644,18 @@ impl Editor {
                         return;
                     }
                     ControlFlow::Break(None) => return,
-                    ControlFlow::Continue(()) => InvalidationStrategy::RefreshRequested,
+                    ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None),
                 }
             }
-            InlayRefreshReason::NewLinesShown => InvalidationStrategy::None,
-            InlayRefreshReason::ExcerptEdited => InvalidationStrategy::ExcerptEdited,
-            InlayRefreshReason::RefreshRequested => InvalidationStrategy::RefreshRequested,
+            InlayRefreshReason::NewLinesShown => (InvalidationStrategy::None, None),
+            InlayRefreshReason::BufferEdited(buffer_languages) => {
+                (InvalidationStrategy::BufferEdited, Some(buffer_languages))
+            }
+            InlayRefreshReason::RefreshRequested => (InvalidationStrategy::RefreshRequested, None),
         };
 
         self.inlay_hint_cache.refresh_inlay_hints(
-            self.excerpt_visible_offsets(cx),
+            self.excerpt_visible_offsets(required_languages.as_ref(), cx),
             invalidate_cache,
             cx,
         )
@@ -2661,8 +2674,9 @@ impl Editor {
 
     fn excerpt_visible_offsets(
         &self,
+        restrict_to_languages: Option<&HashSet<Arc<Language>>>,
         cx: &mut ViewContext<'_, '_, Editor>,
-    ) -> HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)> {
+    ) -> HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)> {
         let multi_buffer = self.buffer().read(cx);
         let multi_buffer_snapshot = multi_buffer.snapshot(cx);
         let multi_buffer_visible_start = self
@@ -2680,8 +2694,22 @@ impl Editor {
             .range_to_buffer_ranges(multi_buffer_visible_range, cx)
             .into_iter()
             .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
-            .map(|(buffer, excerpt_visible_range, excerpt_id)| {
-                (excerpt_id, (buffer, excerpt_visible_range))
+            .filter_map(|(buffer_handle, excerpt_visible_range, excerpt_id)| {
+                let buffer = buffer_handle.read(cx);
+                let language = buffer.language()?;
+                if let Some(restrict_to_languages) = restrict_to_languages {
+                    if !restrict_to_languages.contains(language) {
+                        return None;
+                    }
+                }
+                Some((
+                    excerpt_id,
+                    (
+                        buffer_handle,
+                        buffer.version().clone(),
+                        excerpt_visible_range,
+                    ),
+                ))
             })
             .collect()
     }
@@ -2689,26 +2717,11 @@ impl Editor {
     fn splice_inlay_hints(
         &self,
         to_remove: Vec<InlayId>,
-        to_insert: Vec<(Anchor, InlayId, project::InlayHint)>,
+        to_insert: Vec<Inlay>,
         cx: &mut ViewContext<Self>,
     ) {
-        let buffer = self.buffer.read(cx).read(cx);
-        let new_inlays = to_insert
-            .into_iter()
-            .map(|(position, id, hint)| {
-                let mut text = hint.text();
-                if hint.padding_right {
-                    text.push(' ');
-                }
-                if hint.padding_left {
-                    text.insert(0, ' ');
-                }
-                (id, InlayProperties { position, text })
-            })
-            .collect();
-        drop(buffer);
         self.display_map.update(cx, |display_map, cx| {
-            display_map.splice_inlays(to_remove, new_inlays, cx);
+            display_map.splice_inlays(to_remove, to_insert, cx);
         });
     }
 
@@ -3393,7 +3406,7 @@ impl Editor {
             }
 
             self.display_map.update(cx, |map, cx| {
-                map.splice_inlays::<&str>(vec![suggestion.id], Vec::new(), cx)
+                map.splice_inlays(vec![suggestion.id], Vec::new(), cx)
             });
             cx.notify();
             true
@@ -3426,7 +3439,7 @@ impl Editor {
     fn take_active_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<Inlay> {
         let suggestion = self.copilot_state.suggestion.take()?;
         self.display_map.update(cx, |map, cx| {
-            map.splice_inlays::<&str>(vec![suggestion.id], Default::default(), cx);
+            map.splice_inlays(vec![suggestion.id], Default::default(), cx);
         });
         let buffer = self.buffer.read(cx).read(cx);
 
@@ -3457,21 +3470,11 @@ impl Editor {
                 to_remove.push(suggestion.id);
             }
 
-            let suggestion_inlay_id = InlayId::Suggestion(post_inc(&mut self.next_inlay_id));
-            let to_insert = vec![(
-                suggestion_inlay_id,
-                InlayProperties {
-                    position: cursor,
-                    text: text.clone(),
-                },
-            )];
+            let suggestion_inlay =
+                Inlay::suggestion(post_inc(&mut self.next_inlay_id), cursor, text);
+            self.copilot_state.suggestion = Some(suggestion_inlay.clone());
             self.display_map.update(cx, move |map, cx| {
-                map.splice_inlays(to_remove, to_insert, cx)
-            });
-            self.copilot_state.suggestion = Some(Inlay {
-                id: suggestion_inlay_id,
-                position: cursor,
-                text,
+                map.splice_inlays(to_remove, vec![suggestion_inlay], cx)
             });
             cx.notify();
         } else {
@@ -5120,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,
                 )
             });
@@ -5140,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,
                 )
             });
@@ -5159,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,
+                )
             });
         })
     }
@@ -5176,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,
+                )
             });
         })
     }
@@ -7256,7 +7265,7 @@ impl Editor {
 
     fn on_buffer_event(
         &mut self,
-        _: ModelHandle<MultiBuffer>,
+        multibuffer: ModelHandle<MultiBuffer>,
         event: &multi_buffer::Event,
         cx: &mut ViewContext<Self>,
     ) {
@@ -7268,7 +7277,33 @@ impl Editor {
                     self.update_visible_copilot_suggestion(cx);
                 }
                 cx.emit(Event::BufferEdited);
-                self.refresh_inlays(InlayRefreshReason::ExcerptEdited, cx);
+
+                if let Some(project) = &self.project {
+                    let project = project.read(cx);
+                    let languages_affected = multibuffer
+                        .read(cx)
+                        .all_buffers()
+                        .into_iter()
+                        .filter_map(|buffer| {
+                            let buffer = buffer.read(cx);
+                            let language = buffer.language()?;
+                            if project.is_local()
+                                && project.language_servers_for_buffer(buffer, cx).count() == 0
+                            {
+                                None
+                            } else {
+                                Some(language)
+                            }
+                        })
+                        .cloned()
+                        .collect::<HashSet<_>>();
+                    if !languages_affected.is_empty() {
+                        self.refresh_inlays(
+                            InlayRefreshReason::BufferEdited(languages_affected),
+                            cx,
+                        );
+                    }
+                }
             }
             multi_buffer::Event::ExcerptsAdded {
                 buffer,

crates/editor/src/editor_settings.rs 🔗

@@ -15,6 +15,7 @@ pub struct EditorSettings {
 pub struct Scrollbar {
     pub show: ShowScrollbar,
     pub git_diff: bool,
+    pub selections: bool,
 }
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -39,6 +40,7 @@ pub struct EditorSettingsContent {
 pub struct ScrollbarContent {
     pub show: Option<ShowScrollbar>,
     pub git_diff: Option<bool>,
+    pub selections: Option<bool>,
 }
 
 impl Setting for EditorSettings {

crates/editor/src/editor_tests.rs 🔗

@@ -6979,6 +6979,111 @@ async fn test_copilot_disabled_globs(
     assert!(copilot_requests.try_next().is_ok());
 }
 
+#[gpui::test]
+async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            brackets: BracketPairConfig {
+                pairs: vec![BracketPair {
+                    start: "{".to_string(),
+                    end: "}".to_string(),
+                    close: true,
+                    newline: true,
+                }],
+                disabled_scopes_by_bracket_ix: Vec::new(),
+            },
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_servers = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            capabilities: lsp::ServerCapabilities {
+                document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
+                    first_trigger_character: "{".to_string(),
+                    more_trigger_character: None,
+                }),
+                ..Default::default()
+            },
+            ..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 worktree_id = workspace.update(cx, |workspace, cx| {
+        workspace.project().read_with(cx, |project, cx| {
+            project.worktrees(cx).next().unwrap().read(cx).id()
+        })
+    });
+
+    let buffer = project
+        .update(cx, |project, cx| {
+            project.open_local_buffer("/a/main.rs", cx)
+        })
+        .await
+        .unwrap();
+    cx.foreground().run_until_parked();
+    cx.foreground().start_waiting();
+    let fake_server = fake_servers.next().await.unwrap();
+    let editor_handle = workspace
+        .update(cx, |workspace, cx| {
+            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
+        assert_eq!(
+            params.text_document_position.text_document.uri,
+            lsp::Url::from_file_path("/a/main.rs").unwrap(),
+        );
+        assert_eq!(
+            params.text_document_position.position,
+            lsp::Position::new(0, 21),
+        );
+
+        Ok(Some(vec![lsp::TextEdit {
+            new_text: "]".to_string(),
+            range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
+        }]))
+    });
+
+    editor_handle.update(cx, |editor, cx| {
+        cx.focus(&editor_handle);
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
+        });
+        editor.handle_input("{", cx);
+    });
+
+    cx.foreground().run_until_parked();
+
+    buffer.read_with(cx, |buffer, _| {
+        assert_eq!(
+            buffer.text(),
+            "fn main() { let a = {5}; }",
+            "No extra braces from on type formatting should appear in the buffer"
+        )
+    });
+}
+
 fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
     let point = DisplayPoint::new(row as u32, column as u32);
     point..point

crates/editor/src/element.rs 🔗

@@ -1008,6 +1008,7 @@ impl EditorElement {
         bounds: RectF,
         layout: &mut LayoutState,
         cx: &mut ViewContext<Editor>,
+        editor: &Editor,
     ) {
         enum ScrollbarMouseHandlers {}
         if layout.mode != EditorMode::Full {
@@ -1050,9 +1051,74 @@ impl EditorElement {
                 background: style.track.background_color,
                 ..Default::default()
             });
+            let scrollbar_settings = settings::get::<EditorSettings>(cx).scrollbar;
+            let theme = theme::current(cx);
+            let scrollbar_theme = &theme.editor.scrollbar;
+            if layout.is_singleton && scrollbar_settings.selections {
+                let start_anchor = Anchor::min();
+                let end_anchor = Anchor::max();
+                let mut start_row = None;
+                let mut end_row = None;
+                let color = scrollbar_theme.selections;
+                let border = Border {
+                    width: 1.,
+                    color: style.thumb.border.color,
+                    overlay: false,
+                    top: false,
+                    right: true,
+                    bottom: false,
+                    left: true,
+                };
+                let mut push_region = |start, end| {
+                    if let (Some(start_display), Some(end_display)) = (start, end) {
+                        let start_y = y_for_row(start_display as f32);
+                        let mut end_y = y_for_row(end_display as f32);
+                        if end_y - start_y < 1. {
+                            end_y = start_y + 1.;
+                        }
+                        let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y));
+
+                        scene.push_quad(Quad {
+                            bounds,
+                            background: Some(color),
+                            border,
+                            corner_radius: style.thumb.corner_radius,
+                        })
+                    }
+                };
+                for (row, _) in &editor.background_highlights_in_range(
+                    start_anchor..end_anchor,
+                    &layout.position_map.snapshot,
+                    &theme,
+                ) {
+                    let start_display = row.start;
+                    let end_display = row.end;
+
+                    if start_row.is_none() {
+                        assert_eq!(end_row, None);
+                        start_row = Some(start_display.row());
+                        end_row = Some(end_display.row());
+                        continue;
+                    }
+                    if let Some(current_end) = end_row.as_mut() {
+                        if start_display.row() > *current_end + 1 {
+                            push_region(start_row, end_row);
+                            start_row = Some(start_display.row());
+                            end_row = Some(end_display.row());
+                        } else {
+                            // Merge two hunks.
+                            *current_end = end_display.row();
+                        }
+                    } else {
+                        unreachable!();
+                    }
+                }
+                // We might still have a hunk that was not rendered (if there was a search hit on the last line)
+                push_region(start_row, end_row);
+            }
 
-            if layout.is_singleton && settings::get::<EditorSettings>(cx).scrollbar.git_diff {
-                let diff_style = theme::current(cx).editor.scrollbar.git.clone();
+            if layout.is_singleton && scrollbar_settings.git_diff {
+                let diff_style = scrollbar_theme.git.clone();
                 for hunk in layout
                     .position_map
                     .snapshot
@@ -2368,7 +2434,7 @@ impl Element<Editor> for EditorElement {
         if !layout.blocks.is_empty() {
             self.paint_blocks(scene, bounds, visible_bounds, layout, editor, cx);
         }
-        self.paint_scrollbar(scene, bounds, layout, cx);
+        self.paint_scrollbar(scene, bounds, layout, cx, &editor);
         scene.pop_layer();
 
         scene.pop_layer();

crates/editor/src/inlay_hint_cache.rs 🔗

@@ -38,14 +38,14 @@ pub struct CachedExcerptHints {
 #[derive(Debug, Clone, Copy)]
 pub enum InvalidationStrategy {
     RefreshRequested,
-    ExcerptEdited,
+    BufferEdited,
     None,
 }
 
 #[derive(Debug, Default)]
 pub struct InlaySplice {
     pub to_remove: Vec<InlayId>,
-    pub to_insert: Vec<(Anchor, InlayId, InlayHint)>,
+    pub to_insert: Vec<Inlay>,
 }
 
 struct UpdateTask {
@@ -94,7 +94,7 @@ impl InvalidationStrategy {
     fn should_invalidate(&self) -> bool {
         matches!(
             self,
-            InvalidationStrategy::RefreshRequested | InvalidationStrategy::ExcerptEdited
+            InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited
         )
     }
 }
@@ -197,7 +197,7 @@ impl InlayHintCache {
 
     pub fn refresh_inlay_hints(
         &mut self,
-        mut excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)>,
+        mut excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
         invalidate: InvalidationStrategy,
         cx: &mut ViewContext<Editor>,
     ) {
@@ -285,13 +285,13 @@ impl InlayHintCache {
                                     if !old_kinds.contains(&cached_hint.kind)
                                         && new_kinds.contains(&cached_hint.kind)
                                     {
-                                        to_insert.push((
+                                        to_insert.push(Inlay::hint(
+                                            cached_hint_id.id(),
                                             multi_buffer_snapshot.anchor_in_excerpt(
                                                 *excerpt_id,
                                                 cached_hint.position,
                                             ),
-                                            *cached_hint_id,
-                                            cached_hint.clone(),
+                                            &cached_hint,
                                         ));
                                     }
                                     excerpt_cache.next();
@@ -307,11 +307,11 @@ impl InlayHintCache {
             for (cached_hint_id, maybe_missed_cached_hint) in excerpt_cache {
                 let cached_hint_kind = maybe_missed_cached_hint.kind;
                 if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) {
-                    to_insert.push((
+                    to_insert.push(Inlay::hint(
+                        cached_hint_id.id(),
                         multi_buffer_snapshot
                             .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position),
-                        *cached_hint_id,
-                        maybe_missed_cached_hint.clone(),
+                        &maybe_missed_cached_hint,
                     ));
                 }
             }
@@ -342,105 +342,114 @@ impl InlayHintCache {
 
 fn spawn_new_update_tasks(
     editor: &mut Editor,
-    excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)>,
+    excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
     invalidate: InvalidationStrategy,
     update_cache_version: usize,
     cx: &mut ViewContext<'_, '_, Editor>,
 ) {
     let visible_hints = Arc::new(editor.visible_inlay_hints(cx));
-    for (excerpt_id, (buffer_handle, excerpt_visible_range)) in excerpts_to_query {
-        if !excerpt_visible_range.is_empty() {
-            let buffer = buffer_handle.read(cx);
-            let buffer_snapshot = buffer.snapshot();
-            let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned();
-            if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
-                let new_task_buffer_version = buffer_snapshot.version();
-                let cached_excerpt_hints = cached_excerpt_hints.read();
-                let cached_buffer_version = &cached_excerpt_hints.buffer_version;
-                if cached_excerpt_hints.version > update_cache_version
-                    || cached_buffer_version.changed_since(new_task_buffer_version)
-                {
-                    return;
-                }
-                if !new_task_buffer_version.changed_since(&cached_buffer_version)
-                    && !matches!(invalidate, InvalidationStrategy::RefreshRequested)
-                {
-                    return;
-                }
-            };
+    for (excerpt_id, (buffer_handle, new_task_buffer_version, excerpt_visible_range)) in
+        excerpts_to_query
+    {
+        if excerpt_visible_range.is_empty() {
+            continue;
+        }
+        let buffer = buffer_handle.read(cx);
+        let buffer_snapshot = buffer.snapshot();
+        if buffer_snapshot
+            .version()
+            .changed_since(&new_task_buffer_version)
+        {
+            continue;
+        }
 
-            let buffer_id = buffer.remote_id();
-            let excerpt_visible_range_start = buffer.anchor_before(excerpt_visible_range.start);
-            let excerpt_visible_range_end = buffer.anchor_after(excerpt_visible_range.end);
-
-            let (multi_buffer_snapshot, full_excerpt_range) =
-                editor.buffer.update(cx, |multi_buffer, cx| {
-                    let multi_buffer_snapshot = multi_buffer.snapshot(cx);
-                    (
-                        multi_buffer_snapshot,
-                        multi_buffer
-                            .excerpts_for_buffer(&buffer_handle, cx)
-                            .into_iter()
-                            .find(|(id, _)| id == &excerpt_id)
-                            .map(|(_, range)| range.context),
-                    )
-                });
+        let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned();
+        if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
+            let cached_excerpt_hints = cached_excerpt_hints.read();
+            let cached_buffer_version = &cached_excerpt_hints.buffer_version;
+            if cached_excerpt_hints.version > update_cache_version
+                || cached_buffer_version.changed_since(&new_task_buffer_version)
+            {
+                continue;
+            }
+            if !new_task_buffer_version.changed_since(&cached_buffer_version)
+                && !matches!(invalidate, InvalidationStrategy::RefreshRequested)
+            {
+                continue;
+            }
+        };
 
-            if let Some(full_excerpt_range) = full_excerpt_range {
-                let query = ExcerptQuery {
-                    buffer_id,
-                    excerpt_id,
-                    dimensions: ExcerptDimensions {
-                        excerpt_range_start: full_excerpt_range.start,
-                        excerpt_range_end: full_excerpt_range.end,
-                        excerpt_visible_range_start,
-                        excerpt_visible_range_end,
-                    },
-                    cache_version: update_cache_version,
-                    invalidate,
-                };
+        let buffer_id = buffer.remote_id();
+        let excerpt_visible_range_start = buffer.anchor_before(excerpt_visible_range.start);
+        let excerpt_visible_range_end = buffer.anchor_after(excerpt_visible_range.end);
+
+        let (multi_buffer_snapshot, full_excerpt_range) =
+            editor.buffer.update(cx, |multi_buffer, cx| {
+                let multi_buffer_snapshot = multi_buffer.snapshot(cx);
+                (
+                    multi_buffer_snapshot,
+                    multi_buffer
+                        .excerpts_for_buffer(&buffer_handle, cx)
+                        .into_iter()
+                        .find(|(id, _)| id == &excerpt_id)
+                        .map(|(_, range)| range.context),
+                )
+            });
 
-                let new_update_task = |is_refresh_after_regular_task| {
-                    new_update_task(
-                        query,
-                        multi_buffer_snapshot,
-                        buffer_snapshot,
-                        Arc::clone(&visible_hints),
-                        cached_excerpt_hints,
-                        is_refresh_after_regular_task,
-                        cx,
-                    )
-                };
-                match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
-                    hash_map::Entry::Occupied(mut o) => {
-                        let update_task = o.get_mut();
-                        match (update_task.invalidate, invalidate) {
-                            (_, InvalidationStrategy::None) => {}
-                            (
-                                InvalidationStrategy::ExcerptEdited,
-                                InvalidationStrategy::RefreshRequested,
-                            ) if !update_task.task.is_running_rx.is_closed() => {
-                                update_task.pending_refresh = Some(query);
-                            }
-                            _ => {
-                                o.insert(UpdateTask {
-                                    invalidate,
-                                    cache_version: query.cache_version,
-                                    task: new_update_task(false),
-                                    pending_refresh: None,
-                                });
-                            }
+        if let Some(full_excerpt_range) = full_excerpt_range {
+            let query = ExcerptQuery {
+                buffer_id,
+                excerpt_id,
+                dimensions: ExcerptDimensions {
+                    excerpt_range_start: full_excerpt_range.start,
+                    excerpt_range_end: full_excerpt_range.end,
+                    excerpt_visible_range_start,
+                    excerpt_visible_range_end,
+                },
+                cache_version: update_cache_version,
+                invalidate,
+            };
+
+            let new_update_task = |is_refresh_after_regular_task| {
+                new_update_task(
+                    query,
+                    multi_buffer_snapshot,
+                    buffer_snapshot,
+                    Arc::clone(&visible_hints),
+                    cached_excerpt_hints,
+                    is_refresh_after_regular_task,
+                    cx,
+                )
+            };
+            match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
+                hash_map::Entry::Occupied(mut o) => {
+                    let update_task = o.get_mut();
+                    match (update_task.invalidate, invalidate) {
+                        (_, InvalidationStrategy::None) => {}
+                        (
+                            InvalidationStrategy::BufferEdited,
+                            InvalidationStrategy::RefreshRequested,
+                        ) if !update_task.task.is_running_rx.is_closed() => {
+                            update_task.pending_refresh = Some(query);
+                        }
+                        _ => {
+                            o.insert(UpdateTask {
+                                invalidate,
+                                cache_version: query.cache_version,
+                                task: new_update_task(false),
+                                pending_refresh: None,
+                            });
                         }
-                    }
-                    hash_map::Entry::Vacant(v) => {
-                        v.insert(UpdateTask {
-                            invalidate,
-                            cache_version: query.cache_version,
-                            task: new_update_task(false),
-                            pending_refresh: None,
-                        });
                     }
                 }
+                hash_map::Entry::Vacant(v) => {
+                    v.insert(UpdateTask {
+                        invalidate,
+                        cache_version: query.cache_version,
+                        task: new_update_task(false),
+                        pending_refresh: None,
+                    });
+                }
             }
         }
     }
@@ -648,18 +657,22 @@ async fn fetch_and_update_hints(
                 for new_hint in new_update.add_to_cache {
                     let new_hint_position = multi_buffer_snapshot
                         .anchor_in_excerpt(query.excerpt_id, new_hint.position);
-                    let new_inlay_id = InlayId::Hint(post_inc(&mut editor.next_inlay_id));
+                    let new_inlay_id = post_inc(&mut editor.next_inlay_id);
                     if editor
                         .inlay_hint_cache
                         .allowed_hint_kinds
                         .contains(&new_hint.kind)
                     {
-                        splice
-                            .to_insert
-                            .push((new_hint_position, new_inlay_id, new_hint.clone()));
+                        splice.to_insert.push(Inlay::hint(
+                            new_inlay_id,
+                            new_hint_position,
+                            &new_hint,
+                        ));
                     }
 
-                    cached_excerpt_hints.hints.push((new_inlay_id, new_hint));
+                    cached_excerpt_hints
+                        .hints
+                        .push((InlayId::Hint(new_inlay_id), new_hint));
                 }
 
                 cached_excerpt_hints
@@ -820,7 +833,7 @@ mod tests {
     use crate::{
         scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount},
         serde_json::json,
-        ExcerptRange, InlayHintSettings,
+        ExcerptRange,
     };
     use futures::StreamExt;
     use gpui::{executor::Deterministic, TestAppContext, ViewHandle};
@@ -961,6 +974,348 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: true,
+                show_parameter_hints: true,
+                show_other_hints: true,
+            })
+        });
+
+        let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+        let lsp_request_count = Arc::new(AtomicU32::new(0));
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_count = Arc::clone(&lsp_request_count);
+                async move {
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path(file_with_hints).unwrap(),
+                    );
+                    let current_call_id =
+                        Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: lsp::Position::new(0, current_call_id),
+                        label: lsp::InlayHintLabel::String(current_call_id.to_string()),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+
+        let mut edits_made = 1;
+        editor.update(cx, |editor, cx| {
+            let expected_layers = vec!["0".to_string()];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "Should get its first hints when opening the editor"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(
+                inlay_cache.version, edits_made,
+                "The editor update the cache version after every cache/view change"
+            );
+        });
+
+        let progress_token = "test_progress_token";
+        fake_server
+            .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
+                token: lsp::ProgressToken::String(progress_token.to_string()),
+            })
+            .await
+            .expect("work done progress create request failed");
+        cx.foreground().run_until_parked();
+        fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
+            token: lsp::ProgressToken::String(progress_token.to_string()),
+            value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
+                lsp::WorkDoneProgressBegin::default(),
+            )),
+        });
+        cx.foreground().run_until_parked();
+
+        editor.update(cx, |editor, cx| {
+            let expected_layers = vec!["0".to_string()];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "Should not update hints while the work task is running"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(
+                inlay_cache.version, edits_made,
+                "Should not update the cache while the work task is running"
+            );
+        });
+
+        fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
+            token: lsp::ProgressToken::String(progress_token.to_string()),
+            value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
+                lsp::WorkDoneProgressEnd::default(),
+            )),
+        });
+        cx.foreground().run_until_parked();
+
+        edits_made += 1;
+        editor.update(cx, |editor, cx| {
+            let expected_layers = vec!["1".to_string()];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "New hints should be queried after the work task is done"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(
+                inlay_cache.version, edits_made,
+                "Cache version should udpate once after the work task is done"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: true,
+                show_parameter_hints: true,
+                show_other_hints: true,
+            })
+        });
+
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+                    "/a",
+                    json!({
+                        "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+                        "other.md": "Test md file with some text",
+                    }),
+                )
+                .await;
+        let project = Project::test(fs, ["/a".as_ref()], cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let worktree_id = workspace.update(cx, |workspace, cx| {
+            workspace.project().read_with(cx, |project, cx| {
+                project.worktrees(cx).next().unwrap().read(cx).id()
+            })
+        });
+
+        let mut rs_fake_servers = None;
+        let mut md_fake_servers = None;
+        for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] {
+            let mut language = Language::new(
+                LanguageConfig {
+                    name: name.into(),
+                    path_suffixes: vec![path_suffix.to_string()],
+                    ..Default::default()
+                },
+                Some(tree_sitter_rust::language()),
+            );
+            let fake_servers = language
+                .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+                    name,
+                    capabilities: lsp::ServerCapabilities {
+                        inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                        ..Default::default()
+                    },
+                    ..Default::default()
+                }))
+                .await;
+            match name {
+                "Rust" => rs_fake_servers = Some(fake_servers),
+                "Markdown" => md_fake_servers = Some(fake_servers),
+                _ => unreachable!(),
+            }
+            project.update(cx, |project, _| {
+                project.languages().add(Arc::new(language));
+            });
+        }
+
+        let _rs_buffer = project
+            .update(cx, |project, cx| {
+                project.open_local_buffer("/a/main.rs", cx)
+            })
+            .await
+            .unwrap();
+        cx.foreground().run_until_parked();
+        cx.foreground().start_waiting();
+        let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap();
+        let rs_editor = workspace
+            .update(cx, |workspace, cx| {
+                workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+        let rs_lsp_request_count = Arc::new(AtomicU32::new(0));
+        rs_fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_count = Arc::clone(&rs_lsp_request_count);
+                async move {
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path("/a/main.rs").unwrap(),
+                    );
+                    let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: lsp::Position::new(0, i),
+                        label: lsp::InlayHintLabel::String(i.to_string()),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+        rs_editor.update(cx, |editor, cx| {
+            let expected_layers = vec!["0".to_string()];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "Should get its first hints when opening the editor"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(
+                inlay_cache.version, 1,
+                "Rust editor update the cache version after every cache/view change"
+            );
+        });
+
+        cx.foreground().run_until_parked();
+        let _md_buffer = project
+            .update(cx, |project, cx| {
+                project.open_local_buffer("/a/other.md", cx)
+            })
+            .await
+            .unwrap();
+        cx.foreground().run_until_parked();
+        cx.foreground().start_waiting();
+        let md_fake_server = md_fake_servers.unwrap().next().await.unwrap();
+        let md_editor = workspace
+            .update(cx, |workspace, cx| {
+                workspace.open_path((worktree_id, "other.md"), None, true, cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+        let md_lsp_request_count = Arc::new(AtomicU32::new(0));
+        md_fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_count = Arc::clone(&md_lsp_request_count);
+                async move {
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path("/a/other.md").unwrap(),
+                    );
+                    let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: lsp::Position::new(0, i),
+                        label: lsp::InlayHintLabel::String(i.to_string()),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+        md_editor.update(cx, |editor, cx| {
+            let expected_layers = vec!["0".to_string()];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "Markdown editor should have a separate verison, repeating Rust editor rules"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.version, 1);
+        });
+
+        rs_editor.update(cx, |editor, cx| {
+            editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+            editor.handle_input("some rs change", cx);
+        });
+        cx.foreground().run_until_parked();
+        rs_editor.update(cx, |editor, cx| {
+            let expected_layers = vec!["1".to_string()];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "Rust inlay cache should change after the edit"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(
+                inlay_cache.version, 2,
+                "Every time hint cache changes, cache version should be incremented"
+            );
+        });
+        md_editor.update(cx, |editor, cx| {
+            let expected_layers = vec!["0".to_string()];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "Markdown editor should not be affected by Rust editor changes"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.version, 1);
+        });
+
+        md_editor.update(cx, |editor, cx| {
+            editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+            editor.handle_input("some md change", cx);
+        });
+        cx.foreground().run_until_parked();
+        md_editor.update(cx, |editor, cx| {
+            let expected_layers = vec!["1".to_string()];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "Rust editor should not be affected by Markdown editor changes"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.version, 2);
+        });
+        rs_editor.update(cx, |editor, cx| {
+            let expected_layers = vec!["1".to_string()];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "Markdown editor should also change independently"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.version, 2);
+        });
+    }
+
     #[gpui::test]
     async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
         let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
@@ -1079,7 +1434,6 @@ mod tests {
                 visible_hint_labels(editor, cx)
             );
             let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
             assert_eq!(
                 inlay_cache.version, edits_made,
                 "Should not update cache version due to new loaded hints being the same"
@@ -1215,7 +1569,6 @@ mod tests {
             assert!(cached_hint_labels(editor).is_empty());
             assert!(visible_hint_labels(editor, cx).is_empty());
             let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds);
             assert_eq!(
                 inlay_cache.version, edits_made,
                 "The editor should not update the cache version after /refresh query without updates"
@@ -1289,20 +1642,18 @@ mod tests {
                 visible_hint_labels(editor, cx),
             );
             let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds);
             assert_eq!(inlay_cache.version, edits_made);
         });
     }
 
     #[gpui::test]
     async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
-        let allowed_hint_kinds = HashSet::from_iter([None]);
         init_test(cx, |settings| {
             settings.defaults.inlay_hints = Some(InlayHintSettings {
                 enabled: true,
-                show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
-                show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
-                show_other_hints: allowed_hint_kinds.contains(&None),
+                show_type_hints: true,
+                show_parameter_hints: true,
+                show_other_hints: true,
             })
         });
 
@@ -1370,7 +1721,6 @@ mod tests {
             );
             assert_eq!(expected_hints, visible_hint_labels(editor, cx));
             let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
             assert_eq!(
                 inlay_cache.version, 1,
                 "Only one update should be registered in the cache after all cancellations"
@@ -1417,7 +1767,6 @@ mod tests {
             );
             assert_eq!(expected_hints, visible_hint_labels(editor, cx));
             let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
             assert_eq!(
                 inlay_cache.version, 2,
                 "Should update the cache version once more, for the new change"
@@ -1427,13 +1776,12 @@ mod tests {
 
     #[gpui::test]
     async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
-        let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
         init_test(cx, |settings| {
             settings.defaults.inlay_hints = Some(InlayHintSettings {
                 enabled: true,
-                show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
-                show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
-                show_other_hints: allowed_hint_kinds.contains(&None),
+                show_type_hints: true,
+                show_parameter_hints: true,
+                show_other_hints: true,
             })
         });
 
@@ -1539,7 +1887,6 @@ mod tests {
             );
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
             let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
             assert_eq!(
                 inlay_cache.version, 2,
                 "Both LSP queries should've bumped the cache version"
@@ -1572,7 +1919,6 @@ mod tests {
                 "Should have hints from the new LSP response after edit");
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
             let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
             assert_eq!(inlay_cache.version, 5, "Should update the cache for every LSP response with hints added");
         });
     }
@@ -1582,13 +1928,12 @@ mod tests {
         deterministic: Arc<Deterministic>,
         cx: &mut gpui::TestAppContext,
     ) {
-        let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
         init_test(cx, |settings| {
             settings.defaults.inlay_hints = Some(InlayHintSettings {
                 enabled: true,
-                show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
-                show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
-                show_other_hints: allowed_hint_kinds.contains(&None),
+                show_type_hints: true,
+                show_parameter_hints: true,
+                show_other_hints: true,
             })
         });
 
@@ -1794,7 +2139,6 @@ mod tests {
             );
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
             let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
             assert_eq!(inlay_cache.version, 4, "Every visible excerpt hints should bump the verison");
         });
 
@@ -1826,7 +2170,6 @@ mod tests {
                 "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
             let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
             assert_eq!(inlay_cache.version, 9);
         });
 
@@ -1855,7 +2198,6 @@ mod tests {
                 "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
             let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
             assert_eq!(inlay_cache.version, 12);
         });
 
@@ -1884,7 +2226,6 @@ mod tests {
                 "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
             let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
             assert_eq!(inlay_cache.version, 12, "No updates should happen during scrolling already scolled buffer");
         });
 
@@ -1911,7 +2252,6 @@ mod tests {
 unedited (2nd) buffer should have the same hint");
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
             let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
             assert_eq!(inlay_cache.version, 16);
         });
     }

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;
@@ -263,13 +279,13 @@ pub fn find_preceding_boundary(
 
         if let Some((prev_ch, prev_point)) = prev {
             if is_boundary(ch, prev_ch) {
-                return prev_point;
+                return map.clip_point(prev_point, Bias::Left);
             }
         }
 
         prev = Some((ch, point));
     }
-    DisplayPoint::zero()
+    map.clip_point(DisplayPoint::zero(), Bias::Left)
 }
 
 /// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
@@ -292,7 +308,7 @@ pub fn find_preceding_boundary_in_line(
     for (ch, point) in map.reverse_chars_at(from) {
         if let Some((prev_ch, prev_point)) = prev {
             if is_boundary(ch, prev_ch) {
-                return prev_point;
+                return map.clip_point(prev_point, Bias::Left);
             }
         }
 
@@ -303,7 +319,7 @@ pub fn find_preceding_boundary_in_line(
         prev = Some((ch, point));
     }
 
-    prev.map(|(_, point)| point).unwrap_or(from)
+    map.clip_point(prev.map(|(_, point)| point).unwrap_or(from), Bias::Left)
 }
 
 /// Scans for a boundary following the given start point until a boundary is found, indicated by the
@@ -406,8 +422,12 @@ pub fn split_display_range_by_lines(
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange, MultiBuffer};
+    use crate::{
+        display_map::Inlay, test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange,
+        InlayId, MultiBuffer,
+    };
     use settings::SettingsStore;
+    use util::post_inc;
 
     #[gpui::test]
     fn test_previous_word_start(cx: &mut gpui::AppContext) {
@@ -505,6 +525,80 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    fn test_find_preceding_boundary_with_inlays(cx: &mut gpui::AppContext) {
+        init_test(cx);
+
+        let input_text = "abcdefghijklmnopqrstuvwxys";
+        let family_id = cx
+            .font_cache()
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
+        let font_id = cx
+            .font_cache()
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 14.0;
+        let buffer = MultiBuffer::build_simple(input_text, cx);
+        let buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let display_map =
+            cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
+
+        // add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary
+        let mut id = 0;
+        let inlays = (0..buffer_snapshot.len())
+            .map(|offset| {
+                [
+                    Inlay {
+                        id: InlayId::Suggestion(post_inc(&mut id)),
+                        position: buffer_snapshot.anchor_at(offset, Bias::Left),
+                        text: format!("test").into(),
+                    },
+                    Inlay {
+                        id: InlayId::Suggestion(post_inc(&mut id)),
+                        position: buffer_snapshot.anchor_at(offset, Bias::Right),
+                        text: format!("test").into(),
+                    },
+                    Inlay {
+                        id: InlayId::Hint(post_inc(&mut id)),
+                        position: buffer_snapshot.anchor_at(offset, Bias::Left),
+                        text: format!("test").into(),
+                    },
+                    Inlay {
+                        id: InlayId::Hint(post_inc(&mut id)),
+                        position: buffer_snapshot.anchor_at(offset, Bias::Right),
+                        text: format!("test").into(),
+                    },
+                ]
+            })
+            .flatten()
+            .collect();
+        let snapshot = display_map.update(cx, |map, cx| {
+            map.splice_inlays(Vec::new(), inlays, cx);
+            map.snapshot(cx)
+        });
+
+        assert_eq!(
+            find_preceding_boundary(
+                &snapshot,
+                buffer_snapshot.len().to_display_point(&snapshot),
+                |left, _| left == 'a',
+            ),
+            0.to_display_point(&snapshot),
+            "Should not stop at inlays when looking for boundaries"
+        );
+
+        assert_eq!(
+            find_preceding_boundary_in_line(
+                &snapshot,
+                buffer_snapshot.len().to_display_point(&snapshot),
+                |left, _| left == 'a',
+            ),
+            0.to_display_point(&snapshot),
+            "Should not stop at inlays when looking for boundaries in line"
+        );
+    }
+
     #[gpui::test]
     fn test_next_word_end(cx: &mut gpui::AppContext) {
         init_test(cx);

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/fs/Cargo.toml 🔗

@@ -31,6 +31,7 @@ serde_derive.workspace = true
 serde_json.workspace = true
 log.workspace = true
 libc = "0.2"
+time.workspace = true
 
 [dev-dependencies]
 gpui = { path = "../gpui", features = ["test-support"] }

crates/fs/src/fs.rs 🔗

@@ -279,6 +279,9 @@ impl Fs for RealFs {
 
     async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
         let buffer_size = text.summary().len.min(10 * 1024);
+        if let Some(path) = path.parent() {
+            self.create_dir(path).await?;
+        }
         let file = smol::fs::File::create(path).await?;
         let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
         for chunk in chunks(text, line_ending) {
@@ -1077,6 +1080,9 @@ impl Fs for FakeFs {
         self.simulate_random_delay().await;
         let path = normalize_path(path);
         let content = chunks(text, line_ending).collect();
+        if let Some(path) = path.parent() {
+            self.create_dir(path).await?;
+        }
         self.write_file_internal(path, content)?;
         Ok(())
     }

crates/fs/src/repository.rs 🔗

@@ -1,6 +1,6 @@
 use anyhow::Result;
 use collections::HashMap;
-use git2::ErrorCode;
+use git2::{BranchType, ErrorCode};
 use parking_lot::Mutex;
 use rpc::proto;
 use serde_derive::{Deserialize, Serialize};
@@ -16,6 +16,12 @@ use util::ResultExt;
 
 pub use git2::Repository as LibGitRepository;
 
+#[derive(Clone, Debug, Hash, PartialEq)]
+pub struct Branch {
+    pub name: Box<str>,
+    /// Timestamp of most recent commit, normalized to Unix Epoch format.
+    pub unix_timestamp: Option<i64>,
+}
 #[async_trait::async_trait]
 pub trait GitRepository: Send {
     fn reload_index(&self);
@@ -27,6 +33,12 @@ pub trait GitRepository: Send {
     fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>>;
 
     fn status(&self, path: &RepoPath) -> Result<Option<GitFileStatus>>;
+    fn branches(&self) -> Result<Vec<Branch>> {
+        Ok(vec![])
+    }
+    fn change_branch(&self, _: &str) -> Result<()> {
+        Ok(())
+    }
 }
 
 impl std::fmt::Debug for dyn GitRepository {
@@ -106,6 +118,40 @@ impl GitRepository for LibGitRepository {
             }
         }
     }
+    fn branches(&self) -> Result<Vec<Branch>> {
+        let local_branches = self.branches(Some(BranchType::Local))?;
+        let valid_branches = local_branches
+            .filter_map(|branch| {
+                branch.ok().and_then(|(branch, _)| {
+                    let name = branch.name().ok().flatten().map(Box::from)?;
+                    let timestamp = branch.get().peel_to_commit().ok()?.time();
+                    let unix_timestamp = timestamp.seconds();
+                    let timezone_offset = timestamp.offset_minutes();
+                    let utc_offset =
+                        time::UtcOffset::from_whole_seconds(timezone_offset * 60).ok()?;
+                    let unix_timestamp =
+                        time::OffsetDateTime::from_unix_timestamp(unix_timestamp).ok()?;
+                    Some(Branch {
+                        name,
+                        unix_timestamp: Some(unix_timestamp.to_offset(utc_offset).unix_timestamp()),
+                    })
+                })
+            })
+            .collect();
+        Ok(valid_branches)
+    }
+    fn change_branch(&self, name: &str) -> Result<()> {
+        let revision = self.find_branch(name, BranchType::Local)?;
+        let revision = revision.get();
+        let as_tree = revision.peel_to_tree()?;
+        self.checkout_tree(as_tree.as_object(), None)?;
+        self.set_head(
+            revision
+                .name()
+                .ok_or_else(|| anyhow::anyhow!("Branch name could not be retrieved"))?,
+        )?;
+        Ok(())
+    }
 }
 
 fn read_status(status: git2::Status) -> Option<GitFileStatus> {

crates/go_to_line/src/go_to_line.rs 🔗

@@ -24,6 +24,7 @@ pub struct GoToLine {
     prev_scroll_position: Option<Vector2F>,
     cursor_point: Point,
     max_point: Point,
+    has_focus: bool,
 }
 
 pub enum Event {
@@ -57,6 +58,7 @@ impl GoToLine {
             prev_scroll_position: scroll_position,
             cursor_point,
             max_point,
+            has_focus: false,
         }
     }
 
@@ -178,11 +180,20 @@ impl View for GoToLine {
     }
 
     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+        self.has_focus = true;
         cx.focus(&self.line_editor);
     }
+
+    fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
+        self.has_focus = false;
+    }
 }
 
 impl Modal for GoToLine {
+    fn has_focus(&self) -> bool {
+        self.has_focus
+    }
+
     fn dismiss_on_event(event: &Self::Event) -> bool {
         matches!(event, Event::Dismissed)
     }

crates/gpui/src/app.rs 🔗

@@ -2971,14 +2971,12 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
     }
 
     pub fn focus(&mut self, handle: &AnyViewHandle) {
-        self.window_context
-            .focus(handle.window_id, Some(handle.view_id));
+        self.window_context.focus(Some(handle.view_id));
     }
 
     pub fn focus_self(&mut self) {
-        let window_id = self.window_id;
         let view_id = self.view_id;
-        self.window_context.focus(window_id, Some(view_id));
+        self.window_context.focus(Some(view_id));
     }
 
     pub fn is_self_focused(&self) -> bool {
@@ -2997,8 +2995,7 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
     }
 
     pub fn blur(&mut self) {
-        let window_id = self.window_id;
-        self.window_context.focus(window_id, None);
+        self.window_context.focus(None);
     }
 
     pub fn on_window_should_close<F>(&mut self, mut callback: F)
@@ -3304,11 +3301,15 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
         let region_id = MouseRegionId::new::<Tag>(self.view_id, region_id);
         MouseState {
             hovered: self.window.hovered_region_ids.contains(&region_id),
-            clicked: self
-                .window
-                .clicked_region_ids
-                .get(&region_id)
-                .and_then(|_| self.window.clicked_button),
+            clicked: if let Some((clicked_region_id, button)) = self.window.clicked_region {
+                if region_id == clicked_region_id {
+                    Some(button)
+                } else {
+                    None
+                }
+            } else {
+                None
+            },
             accessed_hovered: false,
             accessed_clicked: false,
         }

crates/gpui/src/app/window.rs 🔗

@@ -8,14 +8,14 @@ use crate::{
         MouseButton, MouseMovedEvent, PromptLevel, WindowBounds,
     },
     scene::{
-        CursorRegion, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover,
-        MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, Scene,
+        CursorRegion, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag, MouseEvent,
+        MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, Scene,
     },
     text_layout::TextLayoutCache,
     util::post_inc,
     Action, AnyView, AnyViewHandle, AppContext, BorrowAppContext, BorrowWindowContext, Effect,
-    Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, SceneBuilder, Subscription,
-    View, ViewContext, ViewHandle, WindowInvalidation,
+    Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, NoAction, SceneBuilder,
+    Subscription, View, ViewContext, ViewHandle, WindowInvalidation,
 };
 use anyhow::{anyhow, bail, Result};
 use collections::{HashMap, HashSet};
@@ -53,7 +53,7 @@ pub struct Window {
     last_mouse_moved_event: Option<Event>,
     pub(crate) hovered_region_ids: HashSet<MouseRegionId>,
     pub(crate) clicked_region_ids: HashSet<MouseRegionId>,
-    pub(crate) clicked_button: Option<MouseButton>,
+    pub(crate) clicked_region: Option<(MouseRegionId, MouseButton)>,
     mouse_position: Vector2F,
     text_layout_cache: TextLayoutCache,
 }
@@ -86,7 +86,7 @@ impl Window {
             last_mouse_moved_event: None,
             hovered_region_ids: Default::default(),
             clicked_region_ids: Default::default(),
-            clicked_button: None,
+            clicked_region: None,
             mouse_position: vec2f(0., 0.),
             titlebar_height,
             appearance,
@@ -434,7 +434,11 @@ impl<'a> WindowContext<'a> {
                 MatchResult::None => false,
                 MatchResult::Pending => true,
                 MatchResult::Matches(matches) => {
+                    let no_action_id = (NoAction {}).id();
                     for (view_id, action) in matches {
+                        if action.id() == no_action_id {
+                            return false;
+                        }
                         if self.dispatch_action(Some(*view_id), action.as_ref()) {
                             self.keystroke_matcher.clear_pending();
                             handled_by = Some(action.boxed_clone());
@@ -480,8 +484,8 @@ impl<'a> WindowContext<'a> {
                 // specific ancestor element that contained both [positions]'
                 // So we need to store the overlapping regions on mouse down.
 
-                // If there is already clicked_button stored, don't replace it.
-                if self.window.clicked_button.is_none() {
+                // If there is already region being clicked, don't replace it.
+                if self.window.clicked_region.is_none() {
                     self.window.clicked_region_ids = self
                         .window
                         .mouse_regions
@@ -495,7 +499,17 @@ impl<'a> WindowContext<'a> {
                         })
                         .collect();
 
-                    self.window.clicked_button = Some(e.button);
+                    let mut highest_z_index = 0;
+                    let mut clicked_region_id = None;
+                    for (region, z_index) in self.window.mouse_regions.iter() {
+                        if region.bounds.contains_point(e.position) && *z_index >= highest_z_index {
+                            highest_z_index = *z_index;
+                            clicked_region_id = Some(region.id());
+                        }
+                    }
+
+                    self.window.clicked_region =
+                        clicked_region_id.map(|region_id| (region_id, e.button));
                 }
 
                 mouse_events.push(MouseEvent::Down(MouseDown {
@@ -524,6 +538,10 @@ impl<'a> WindowContext<'a> {
                     region: Default::default(),
                     platform_event: e.clone(),
                 }));
+                mouse_events.push(MouseEvent::ClickOut(MouseClickOut {
+                    region: Default::default(),
+                    platform_event: e.clone(),
+                }));
             }
 
             Event::MouseMoved(
@@ -556,7 +574,7 @@ impl<'a> WindowContext<'a> {
                             prev_mouse_position: self.window.mouse_position,
                             platform_event: e.clone(),
                         }));
-                    } else if let Some(clicked_button) = self.window.clicked_button {
+                    } else if let Some((_, clicked_button)) = self.window.clicked_region {
                         // Mouse up event happened outside the current window. Simulate mouse up button event
                         let button_event = e.to_button_event(clicked_button);
                         mouse_events.push(MouseEvent::Up(MouseUp {
@@ -679,8 +697,8 @@ impl<'a> WindowContext<'a> {
                     // Only raise click events if the released button is the same as the one stored
                     if self
                         .window
-                        .clicked_button
-                        .map(|clicked_button| clicked_button == e.button)
+                        .clicked_region
+                        .map(|(_, clicked_button)| clicked_button == e.button)
                         .unwrap_or(false)
                     {
                         // Clear clicked regions and clicked button
@@ -688,7 +706,7 @@ impl<'a> WindowContext<'a> {
                             &mut self.window.clicked_region_ids,
                             Default::default(),
                         );
-                        self.window.clicked_button = None;
+                        self.window.clicked_region = None;
 
                         // Find regions which still overlap with the mouse since the last MouseDown happened
                         for (mouse_region, _) in self.window.mouse_regions.iter().rev() {
@@ -712,7 +730,10 @@ impl<'a> WindowContext<'a> {
                     }
                 }
 
-                MouseEvent::MoveOut(_) | MouseEvent::UpOut(_) | MouseEvent::DownOut(_) => {
+                MouseEvent::MoveOut(_)
+                | MouseEvent::UpOut(_)
+                | MouseEvent::DownOut(_)
+                | MouseEvent::ClickOut(_) => {
                     for (mouse_region, _) in self.window.mouse_regions.iter().rev() {
                         // NOT contains
                         if !mouse_region
@@ -860,18 +881,10 @@ impl<'a> WindowContext<'a> {
         }
         for view_id in &invalidation.updated {
             let titlebar_height = self.window.titlebar_height;
-            let hovered_region_ids = self.window.hovered_region_ids.clone();
-            let clicked_region_ids = self
-                .window
-                .clicked_button
-                .map(|button| (self.window.clicked_region_ids.clone(), button));
-
             let element = self
                 .render_view(RenderParams {
                     view_id: *view_id,
                     titlebar_height,
-                    hovered_region_ids,
-                    clicked_region_ids,
                     refreshing: false,
                     appearance,
                 })
@@ -1085,6 +1098,10 @@ impl<'a> WindowContext<'a> {
         self.window.focused_view_id
     }
 
+    pub fn focus(&mut self, view_id: Option<usize>) {
+        self.app_context.focus(self.window_id, view_id);
+    }
+
     pub fn window_bounds(&self) -> WindowBounds {
         self.window.platform_window.bounds()
     }
@@ -1176,8 +1193,6 @@ impl<'a> WindowContext<'a> {
 pub struct RenderParams {
     pub view_id: usize,
     pub titlebar_height: f32,
-    pub hovered_region_ids: HashSet<MouseRegionId>,
-    pub clicked_region_ids: Option<(HashSet<MouseRegionId>, MouseButton)>,
     pub refreshing: bool,
     pub appearance: Appearance,
 }

crates/gpui/src/elements/mouse_event_handler.rs 🔗

@@ -7,8 +7,8 @@ use crate::{
     platform::CursorStyle,
     platform::MouseButton,
     scene::{
-        CursorRegion, HandlerSet, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseHover,
-        MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut,
+        CursorRegion, HandlerSet, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag,
+        MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut,
     },
     AnyElement, Element, EventContext, LayoutContext, MouseRegion, MouseState, SceneBuilder,
     SizeConstraint, View, ViewContext,
@@ -136,6 +136,15 @@ impl<Tag, V: View> MouseEventHandler<Tag, V> {
         self
     }
 
+    pub fn on_click_out(
+        mut self,
+        button: MouseButton,
+        handler: impl Fn(MouseClickOut, &mut V, &mut EventContext<V>) + 'static,
+    ) -> Self {
+        self.handlers = self.handlers.on_click_out(button, handler);
+        self
+    }
+
     pub fn on_down_out(
         mut self,
         button: MouseButton,

crates/gpui/src/gpui.rs 🔗

@@ -31,3 +31,5 @@ pub use window::{Axis, SizeConstraint, Vector2FExt, WindowContext};
 
 pub use anyhow;
 pub use serde_json;
+
+actions!(zed, [NoAction]);

crates/gpui/src/platform/event.rs 🔗

@@ -4,7 +4,7 @@ use pathfinder_geometry::vector::vec2f;
 
 use crate::{geometry::vector::Vector2F, keymap_matcher::Keystroke};
 
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Eq, PartialEq)]
 pub struct KeyDownEvent {
     pub keystroke: Keystroke,
     pub is_held: bool,

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

@@ -232,10 +232,6 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
         sel!(canBecomeKeyWindow),
         yes as extern "C" fn(&Object, Sel) -> BOOL,
     );
-    decl.add_method(
-        sel!(sendEvent:),
-        send_event as extern "C" fn(&Object, Sel, id),
-    );
     decl.add_method(
         sel!(windowDidResize:),
         window_did_resize as extern "C" fn(&Object, Sel, id),
@@ -299,7 +295,7 @@ struct WindowState {
     appearance_changed_callback: Option<Box<dyn FnMut()>>,
     input_handler: Option<Box<dyn InputHandler>>,
     pending_key_down: Option<(KeyDownEvent, Option<InsertText>)>,
-    performed_key_equivalent: bool,
+    last_key_equivalent: Option<KeyDownEvent>,
     synthetic_drag_counter: usize,
     executor: Rc<executor::Foreground>,
     scene_to_render: Option<Scene>,
@@ -521,7 +517,7 @@ impl Window {
                 appearance_changed_callback: None,
                 input_handler: None,
                 pending_key_down: None,
-                performed_key_equivalent: false,
+                last_key_equivalent: None,
                 synthetic_drag_counter: 0,
                 executor,
                 scene_to_render: Default::default(),
@@ -965,36 +961,34 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
     let window_height = window_state_borrow.content_size().y();
     let event = unsafe { Event::from_native(native_event, Some(window_height)) };
 
-    if let Some(event) = event {
+    if let Some(Event::KeyDown(event)) = event {
+        // For certain keystrokes, macOS will first dispatch a "key equivalent" event.
+        // If that event isn't handled, it will then dispatch a "key down" event. GPUI
+        // makes no distinction between these two types of events, so we need to ignore
+        // the "key down" event if we've already just processed its "key equivalent" version.
         if key_equivalent {
-            window_state_borrow.performed_key_equivalent = true;
-        } else if window_state_borrow.performed_key_equivalent {
+            window_state_borrow.last_key_equivalent = Some(event.clone());
+        } else if window_state_borrow.last_key_equivalent.take().as_ref() == Some(&event) {
             return NO;
         }
 
-        let function_is_held;
-        window_state_borrow.pending_key_down = match event {
-            Event::KeyDown(event) => {
-                let keydown = event.keystroke.clone();
-                // Ignore events from held-down keys after some of the initially-pressed keys
-                // were released.
-                if event.is_held {
-                    if window_state_borrow.last_fresh_keydown.as_ref() != Some(&keydown) {
-                        return YES;
-                    }
-                } else {
-                    window_state_borrow.last_fresh_keydown = Some(keydown);
-                }
-                function_is_held = event.keystroke.function;
-                Some((event, None))
+        let keydown = event.keystroke.clone();
+        let fn_modifier = keydown.function;
+        // Ignore events from held-down keys after some of the initially-pressed keys
+        // were released.
+        if event.is_held {
+            if window_state_borrow.last_fresh_keydown.as_ref() != Some(&keydown) {
+                return YES;
             }
-
-            _ => return NO,
-        };
-
+        } else {
+            window_state_borrow.last_fresh_keydown = Some(keydown);
+        }
+        window_state_borrow.pending_key_down = Some((event, None));
         drop(window_state_borrow);
 
-        if !function_is_held {
+        // Send the event to the input context for IME handling, unless the `fn` modifier is
+        // being pressed.
+        if !fn_modifier {
             unsafe {
                 let input_context: id = msg_send![this, inputContext];
                 let _: BOOL = msg_send![input_context, handleEvent: native_event];
@@ -1143,13 +1137,6 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
     }
 }
 
-extern "C" fn send_event(this: &Object, _: Sel, native_event: id) {
-    unsafe {
-        let _: () = msg_send![super(this, class!(NSWindow)), sendEvent: native_event];
-        get_window_state(this).borrow_mut().performed_key_equivalent = false;
-    }
-}
-
 extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) {
     let window_state = unsafe { get_window_state(this) };
     window_state.as_ref().borrow().move_traffic_light();

crates/gpui/src/scene/mouse_event.rs 🔗

@@ -99,6 +99,20 @@ impl Deref for MouseClick {
     }
 }
 
+#[derive(Debug, Default, Clone)]
+pub struct MouseClickOut {
+    pub region: RectF,
+    pub platform_event: MouseButtonEvent,
+}
+
+impl Deref for MouseClickOut {
+    type Target = MouseButtonEvent;
+
+    fn deref(&self) -> &Self::Target {
+        &self.platform_event
+    }
+}
+
 #[derive(Debug, Default, Clone)]
 pub struct MouseDownOut {
     pub region: RectF,
@@ -150,6 +164,7 @@ pub enum MouseEvent {
     Down(MouseDown),
     Up(MouseUp),
     Click(MouseClick),
+    ClickOut(MouseClickOut),
     DownOut(MouseDownOut),
     UpOut(MouseUpOut),
     ScrollWheel(MouseScrollWheel),
@@ -165,6 +180,7 @@ impl MouseEvent {
             MouseEvent::Down(r) => r.region = region,
             MouseEvent::Up(r) => r.region = region,
             MouseEvent::Click(r) => r.region = region,
+            MouseEvent::ClickOut(r) => r.region = region,
             MouseEvent::DownOut(r) => r.region = region,
             MouseEvent::UpOut(r) => r.region = region,
             MouseEvent::ScrollWheel(r) => r.region = region,
@@ -182,6 +198,7 @@ impl MouseEvent {
             MouseEvent::Down(_) => true,
             MouseEvent::Up(_) => true,
             MouseEvent::Click(_) => true,
+            MouseEvent::ClickOut(_) => true,
             MouseEvent::DownOut(_) => false,
             MouseEvent::UpOut(_) => false,
             MouseEvent::ScrollWheel(_) => true,
@@ -222,6 +239,10 @@ impl MouseEvent {
         discriminant(&MouseEvent::Click(Default::default()))
     }
 
+    pub fn click_out_disc() -> Discriminant<MouseEvent> {
+        discriminant(&MouseEvent::ClickOut(Default::default()))
+    }
+
     pub fn down_out_disc() -> Discriminant<MouseEvent> {
         discriminant(&MouseEvent::DownOut(Default::default()))
     }
@@ -239,6 +260,7 @@ impl MouseEvent {
             MouseEvent::Down(e) => HandlerKey::new(Self::down_disc(), Some(e.button)),
             MouseEvent::Up(e) => HandlerKey::new(Self::up_disc(), Some(e.button)),
             MouseEvent::Click(e) => HandlerKey::new(Self::click_disc(), Some(e.button)),
+            MouseEvent::ClickOut(e) => HandlerKey::new(Self::click_out_disc(), Some(e.button)),
             MouseEvent::UpOut(e) => HandlerKey::new(Self::up_out_disc(), Some(e.button)),
             MouseEvent::DownOut(e) => HandlerKey::new(Self::down_out_disc(), Some(e.button)),
             MouseEvent::ScrollWheel(_) => HandlerKey::new(Self::scroll_wheel_disc(), None),

crates/gpui/src/scene/mouse_region.rs 🔗

@@ -14,7 +14,7 @@ use super::{
         MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover, MouseMove, MouseUp,
         MouseUpOut,
     },
-    MouseMoveOut, MouseScrollWheel,
+    MouseClickOut, MouseMoveOut, MouseScrollWheel,
 };
 
 #[derive(Clone)]
@@ -89,6 +89,15 @@ impl MouseRegion {
         self
     }
 
+    pub fn on_click_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
+    where
+        V: View,
+        F: Fn(MouseClickOut, &mut V, &mut EventContext<V>) + 'static,
+    {
+        self.handlers = self.handlers.on_click_out(button, handler);
+        self
+    }
+
     pub fn on_down_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
     where
         V: View,
@@ -246,6 +255,10 @@ impl HandlerSet {
                 HandlerKey::new(MouseEvent::click_disc(), Some(button)),
                 SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
             );
+            set.insert(
+                HandlerKey::new(MouseEvent::click_out_disc(), Some(button)),
+                SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
+            );
             set.insert(
                 HandlerKey::new(MouseEvent::down_out_disc(), Some(button)),
                 SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
@@ -405,6 +418,28 @@ impl HandlerSet {
         self
     }
 
+    pub fn on_click_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
+    where
+        V: View,
+        F: Fn(MouseClickOut, &mut V, &mut EventContext<V>) + 'static,
+    {
+        self.insert(MouseEvent::click_out_disc(), Some(button),
+            Rc::new(move |region_event, view, cx, view_id| {
+                if let MouseEvent::ClickOut(e) = region_event {
+                    let view = view.downcast_mut().unwrap();
+                    let mut cx = ViewContext::mutable(cx, view_id);
+                    let mut cx = EventContext::new(&mut cx);
+                    handler(e, view, &mut cx);
+                    cx.handled
+                } else {
+                    panic!(
+                        "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::ClickOut, found {:?}",
+                        region_event);
+                }
+            }));
+        self
+    }
+
     pub fn on_down_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
     where
         V: View,

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/picker/src/picker.rs 🔗

@@ -25,6 +25,7 @@ pub struct Picker<D: PickerDelegate> {
     theme: Arc<Mutex<Box<dyn Fn(&theme::Theme) -> theme::Picker>>>,
     confirmed: bool,
     pending_update_matches: Task<Option<()>>,
+    has_focus: bool,
 }
 
 pub trait PickerDelegate: Sized + 'static {
@@ -45,10 +46,16 @@ pub trait PickerDelegate: Sized + 'static {
     fn center_selection_after_match_updates(&self) -> bool {
         false
     }
-    fn render_header(&self, _cx: &AppContext) -> Option<AnyElement<Picker<Self>>> {
+    fn render_header(
+        &self,
+        _cx: &mut ViewContext<Picker<Self>>,
+    ) -> Option<AnyElement<Picker<Self>>> {
         None
     }
-    fn render_footer(&self, _cx: &AppContext) -> Option<AnyElement<Picker<Self>>> {
+    fn render_footer(
+        &self,
+        _cx: &mut ViewContext<Picker<Self>>,
+    ) -> Option<AnyElement<Picker<Self>>> {
         None
     }
 }
@@ -140,13 +147,22 @@ impl<D: PickerDelegate> View for Picker<D> {
     }
 
     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+        self.has_focus = true;
         if cx.is_self_focused() {
             cx.focus(&self.query_editor);
         }
     }
+
+    fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
+        self.has_focus = false;
+    }
 }
 
 impl<D: PickerDelegate> Modal for Picker<D> {
+    fn has_focus(&self) -> bool {
+        self.has_focus
+    }
+
     fn dismiss_on_event(event: &Self::Event) -> bool {
         matches!(event, PickerEvent::Dismiss)
     }
@@ -191,6 +207,7 @@ impl<D: PickerDelegate> Picker<D> {
             theme,
             confirmed: false,
             pending_update_matches: Task::ready(None),
+            has_focus: false,
         };
         this.update_matches(String::new(), cx);
         this

crates/project/Cargo.toml 🔗

@@ -64,7 +64,7 @@ itertools = "0.10"
 [dev-dependencies]
 ctor.workspace = true
 env_logger.workspace = true
-pretty_assertions = "1.3.0"
+pretty_assertions.workspace = true
 client = { path = "../client", features = ["test-support"] }
 collections = { path = "../collections", features = ["test-support"] }
 db = { path = "../db", features = ["test-support"] }

crates/project/src/lsp_command.rs 🔗

@@ -1822,11 +1822,21 @@ impl LspCommand for InlayHints {
     async fn response_from_lsp(
         self,
         message: Option<Vec<lsp::InlayHint>>,
-        _: ModelHandle<Project>,
+        project: ModelHandle<Project>,
         buffer: ModelHandle<Buffer>,
-        _: LanguageServerId,
-        cx: AsyncAppContext,
+        server_id: LanguageServerId,
+        mut cx: AsyncAppContext,
     ) -> Result<Vec<InlayHint>> {
+        let (lsp_adapter, _) = language_server_for_buffer(&project, &buffer, server_id, &mut cx)?;
+        // `typescript-language-server` adds padding to the left for type hints, turning
+        // `const foo: boolean` into `const foo : boolean` which looks odd.
+        // `rust-analyzer` does not have the padding for this case, and we have to accomodate both.
+        //
+        // We could trim the whole string, but being pessimistic on par with the situation above,
+        // there might be a hint with multiple whitespaces at the end(s) which we need to display properly.
+        // Hence let's use a heuristic first to handle the most awkward case and look for more.
+        let force_no_type_left_padding =
+            lsp_adapter.name.0.as_ref() == "typescript-language-server";
         cx.read(|cx| {
             let origin_buffer = buffer.read(cx);
             Ok(message
@@ -1840,6 +1850,12 @@ impl LspCommand for InlayHints {
                     });
                     let position = origin_buffer
                         .clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left);
+                    let padding_left =
+                        if force_no_type_left_padding && kind == Some(InlayHintKind::Type) {
+                            false
+                        } else {
+                            lsp_hint.padding_left.unwrap_or(false)
+                        };
                     InlayHint {
                         buffer_id: origin_buffer.remote_id(),
                         position: if kind == Some(InlayHintKind::Parameter) {
@@ -1847,7 +1863,7 @@ impl LspCommand for InlayHints {
                         } else {
                             origin_buffer.anchor_after(position)
                         },
-                        padding_left: lsp_hint.padding_left.unwrap_or(false),
+                        padding_left,
                         padding_right: lsp_hint.padding_right.unwrap_or(false),
                         label: match lsp_hint.label {
                             lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s),

crates/project/src/project.rs 🔗

@@ -777,20 +777,32 @@ 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();
         for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
-            let language = languages.iter().find(|l| {
-                l.lsp_adapters()
+            let language = languages.iter().find_map(|l| {
+                let adapter = l
+                    .lsp_adapters()
                     .iter()
-                    .any(|adapter| &adapter.name == started_lsp_name)
+                    .find(|adapter| &adapter.name == started_lsp_name)?;
+                Some((l, adapter))
             });
-            if let Some(language) = language {
+            if let Some((language, adapter)) = language {
                 let worktree = self.worktree_for_id(*worktree_id, cx);
-                let file = worktree.and_then(|tree| {
+                let file = worktree.as_ref().and_then(|tree| {
                     tree.update(cx, |tree, cx| tree.root_file(cx).map(|f| f as _))
                 });
                 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)));
+                    }
                 }
             }
         }
@@ -807,6 +819,11 @@ impl Project {
             self.start_language_servers(&worktree, worktree_path, language, cx);
         }
 
+        // Restart all language servers with changed initialization options.
+        for (worktree, language) in language_servers_to_restart {
+            self.restart_language_servers(worktree, language, cx);
+        }
+
         if !self.copilot_enabled && Copilot::global(cx).is_some() {
             self.copilot_enabled = true;
             for buffer in self.opened_buffers.values() {
@@ -3397,6 +3414,7 @@ impl Project {
         cx: &mut ModelContext<Self>,
     ) {
         if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
+            cx.emit(Event::RefreshInlays);
             status.pending_work.remove(&token);
             cx.notify();
         }

crates/project/src/worktree.rs 🔗

@@ -981,6 +981,19 @@ impl LocalWorktree {
         })
     }
 
+    /// Find the lowest path in the worktree's datastructures that is an ancestor
+    fn lowest_ancestor(&self, path: &Path) -> PathBuf {
+        let mut lowest_ancestor = None;
+        for path in path.ancestors() {
+            if self.entry_for_path(path).is_some() {
+                lowest_ancestor = Some(path.to_path_buf());
+                break;
+            }
+        }
+
+        lowest_ancestor.unwrap_or_else(|| PathBuf::from(""))
+    }
+
     pub fn create_entry(
         &self,
         path: impl Into<Arc<Path>>,
@@ -988,6 +1001,7 @@ impl LocalWorktree {
         cx: &mut ModelContext<Worktree>,
     ) -> Task<Result<Entry>> {
         let path = path.into();
+        let lowest_ancestor = self.lowest_ancestor(&path);
         let abs_path = self.absolutize(&path);
         let fs = self.fs.clone();
         let write = cx.background().spawn(async move {
@@ -1001,10 +1015,31 @@ impl LocalWorktree {
 
         cx.spawn(|this, mut cx| async move {
             write.await?;
-            this.update(&mut cx, |this, cx| {
-                this.as_local_mut().unwrap().refresh_entry(path, None, cx)
-            })
-            .await
+            let (result, refreshes) = this.update(&mut cx, |this, cx| {
+                let mut refreshes = Vec::<Task<anyhow::Result<Entry>>>::new();
+                let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap();
+                for refresh_path in refresh_paths.ancestors() {
+                    if refresh_path == Path::new("") {
+                        continue;
+                    }
+                    let refresh_full_path = lowest_ancestor.join(refresh_path);
+
+                    refreshes.push(this.as_local_mut().unwrap().refresh_entry(
+                        refresh_full_path.into(),
+                        None,
+                        cx,
+                    ));
+                }
+                (
+                    this.as_local_mut().unwrap().refresh_entry(path, None, cx),
+                    refreshes,
+                )
+            });
+            for refresh in refreshes {
+                refresh.await.log_err();
+            }
+
+            result.await
         })
     }
 
@@ -2140,6 +2175,7 @@ impl LocalSnapshot {
 impl BackgroundScannerState {
     fn should_scan_directory(&self, entry: &Entry) -> bool {
         (!entry.is_external && !entry.is_ignored)
+            || entry.path.file_name() == Some(&*DOT_GIT)
             || self.scanned_dirs.contains(&entry.id) // If we've ever scanned it, keep scanning
             || self
                 .paths_to_scan
@@ -2319,6 +2355,7 @@ impl BackgroundScannerState {
                         .entry_for_id(entry_id)
                         .map(|entry| RepositoryWorkDirectory(entry.path.clone())) else { continue };
 
+                    log::info!("reload git repository {:?}", dot_git_dir);
                     let repository = repository.repo_ptr.lock();
                     let branch = repository.branch_name();
                     repository.reload_index();
@@ -2359,6 +2396,8 @@ impl BackgroundScannerState {
     }
 
     fn build_repository(&mut self, dot_git_path: Arc<Path>, fs: &dyn Fs) -> Option<()> {
+        log::info!("build git repository {:?}", dot_git_path);
+
         let work_dir_path: Arc<Path> = dot_git_path.parent().unwrap().into();
 
         // Guard against repositories inside the repository metadata
@@ -3138,8 +3177,6 @@ impl BackgroundScanner {
     }
 
     async fn process_events(&mut self, mut abs_paths: Vec<PathBuf>) {
-        log::debug!("received fs events {:?}", abs_paths);
-
         let root_path = self.state.lock().snapshot.abs_path.clone();
         let root_canonical_path = match self.fs.canonicalize(&root_path).await {
             Ok(path) => path,
@@ -3150,7 +3187,6 @@ impl BackgroundScanner {
         };
 
         let mut relative_paths = Vec::with_capacity(abs_paths.len());
-        let mut unloaded_relative_paths = Vec::new();
         abs_paths.sort_unstable();
         abs_paths.dedup_by(|a, b| a.starts_with(&b));
         abs_paths.retain(|abs_path| {
@@ -3173,7 +3209,6 @@ impl BackgroundScanner {
                 });
                 if !parent_dir_is_loaded {
                     log::debug!("ignoring event {relative_path:?} within unloaded directory");
-                    unloaded_relative_paths.push(relative_path);
                     return false;
                 }
 
@@ -3182,27 +3217,30 @@ impl BackgroundScanner {
             }
         });
 
-        if !relative_paths.is_empty() {
-            let (scan_job_tx, scan_job_rx) = channel::unbounded();
-            self.reload_entries_for_paths(
-                root_path,
-                root_canonical_path,
-                &relative_paths,
-                abs_paths,
-                Some(scan_job_tx.clone()),
-            )
-            .await;
-            drop(scan_job_tx);
-            self.scan_dirs(false, scan_job_rx).await;
-
-            let (scan_job_tx, scan_job_rx) = channel::unbounded();
-            self.update_ignore_statuses(scan_job_tx).await;
-            self.scan_dirs(false, scan_job_rx).await;
+        if relative_paths.is_empty() {
+            return;
         }
 
+        log::debug!("received fs events {:?}", relative_paths);
+
+        let (scan_job_tx, scan_job_rx) = channel::unbounded();
+        self.reload_entries_for_paths(
+            root_path,
+            root_canonical_path,
+            &relative_paths,
+            abs_paths,
+            Some(scan_job_tx.clone()),
+        )
+        .await;
+        drop(scan_job_tx);
+        self.scan_dirs(false, scan_job_rx).await;
+
+        let (scan_job_tx, scan_job_rx) = channel::unbounded();
+        self.update_ignore_statuses(scan_job_tx).await;
+        self.scan_dirs(false, scan_job_rx).await;
+
         {
             let mut state = self.state.lock();
-            relative_paths.extend(unloaded_relative_paths);
             state.reload_repositories(&relative_paths, self.fs.as_ref());
             state.snapshot.completed_scan_id = state.snapshot.scan_id;
             for (_, entry_id) in mem::take(&mut state.removed_entry_ids) {
@@ -3610,23 +3648,28 @@ impl BackgroundScanner {
                         }
                     }
 
-                    let fs_entry = state.insert_entry(fs_entry, self.fs.as_ref());
-
-                    if let Some(scan_queue_tx) = &scan_queue_tx {
-                        let mut ancestor_inodes = state.snapshot.ancestor_inodes_for_path(&path);
-                        if metadata.is_dir && !ancestor_inodes.contains(&metadata.inode) {
-                            ancestor_inodes.insert(metadata.inode);
-                            smol::block_on(scan_queue_tx.send(ScanJob {
-                                abs_path,
-                                path: path.clone(),
-                                ignore_stack,
-                                ancestor_inodes,
-                                is_external: fs_entry.is_external,
-                                scan_queue: scan_queue_tx.clone(),
-                            }))
-                            .unwrap();
+                    if let (Some(scan_queue_tx), true) = (&scan_queue_tx, fs_entry.is_dir()) {
+                        if state.should_scan_directory(&fs_entry) {
+                            let mut ancestor_inodes =
+                                state.snapshot.ancestor_inodes_for_path(&path);
+                            if !ancestor_inodes.contains(&metadata.inode) {
+                                ancestor_inodes.insert(metadata.inode);
+                                smol::block_on(scan_queue_tx.send(ScanJob {
+                                    abs_path,
+                                    path: path.clone(),
+                                    ignore_stack,
+                                    ancestor_inodes,
+                                    is_external: fs_entry.is_external,
+                                    scan_queue: scan_queue_tx.clone(),
+                                }))
+                                .unwrap();
+                            }
+                        } else {
+                            fs_entry.kind = EntryKind::UnloadedDir;
                         }
                     }
+
+                    state.insert_entry(fs_entry, self.fs.as_ref());
                 }
                 Ok(None) => {
                     self.remove_repo_path(&path, &mut state.snapshot);

crates/project/src/worktree_tests.rs 🔗

@@ -936,6 +936,119 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
     );
 }
 
+#[gpui::test]
+async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
+    let client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+
+    let fs_fake = FakeFs::new(cx.background());
+    fs_fake
+        .insert_tree(
+            "/root",
+            json!({
+                "a": {},
+            }),
+        )
+        .await;
+
+    let tree_fake = Worktree::local(
+        client_fake,
+        "/root".as_ref(),
+        true,
+        fs_fake,
+        Default::default(),
+        &mut cx.to_async(),
+    )
+    .await
+    .unwrap();
+
+    let entry = tree_fake
+        .update(cx, |tree, cx| {
+            tree.as_local_mut()
+                .unwrap()
+                .create_entry("a/b/c/d.txt".as_ref(), false, cx)
+        })
+        .await
+        .unwrap();
+    assert!(entry.is_file());
+
+    cx.foreground().run_until_parked();
+    tree_fake.read_with(cx, |tree, _| {
+        assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
+        assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
+        assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
+    });
+
+    let client_real = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+
+    let fs_real = Arc::new(RealFs);
+    let temp_root = temp_tree(json!({
+        "a": {}
+    }));
+
+    let tree_real = Worktree::local(
+        client_real,
+        temp_root.path(),
+        true,
+        fs_real,
+        Default::default(),
+        &mut cx.to_async(),
+    )
+    .await
+    .unwrap();
+
+    let entry = tree_real
+        .update(cx, |tree, cx| {
+            tree.as_local_mut()
+                .unwrap()
+                .create_entry("a/b/c/d.txt".as_ref(), false, cx)
+        })
+        .await
+        .unwrap();
+    assert!(entry.is_file());
+
+    cx.foreground().run_until_parked();
+    tree_real.read_with(cx, |tree, _| {
+        assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
+        assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
+        assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
+    });
+
+    // Test smallest change
+    let entry = tree_real
+        .update(cx, |tree, cx| {
+            tree.as_local_mut()
+                .unwrap()
+                .create_entry("a/b/c/e.txt".as_ref(), false, cx)
+        })
+        .await
+        .unwrap();
+    assert!(entry.is_file());
+
+    cx.foreground().run_until_parked();
+    tree_real.read_with(cx, |tree, _| {
+        assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
+    });
+
+    // Test largest change
+    let entry = tree_real
+        .update(cx, |tree, cx| {
+            tree.as_local_mut()
+                .unwrap()
+                .create_entry("d/e/f/g.txt".as_ref(), false, cx)
+        })
+        .await
+        .unwrap();
+    assert!(entry.is_file());
+
+    cx.foreground().run_until_parked();
+    tree_real.read_with(cx, |tree, _| {
+        assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
+        assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
+        assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
+        assert!(tree.entry_for_path("d/").unwrap().is_dir());
+    });
+}
+
 #[gpui::test(iterations = 100)]
 async fn test_random_worktree_operations_during_initial_scan(
     cx: &mut TestAppContext,
@@ -1654,37 +1767,37 @@ async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppCont
 
     }));
 
-    let tree = Worktree::local(
-        build_client(cx),
-        root.path(),
-        true,
-        Arc::new(RealFs),
-        Default::default(),
-        &mut cx.to_async(),
-    )
-    .await
-    .unwrap();
-
-    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-        .await;
-
     const A_TXT: &'static str = "a.txt";
     const B_TXT: &'static str = "b.txt";
     const E_TXT: &'static str = "c/d/e.txt";
     const F_TXT: &'static str = "f.txt";
     const DOTGITIGNORE: &'static str = ".gitignore";
     const BUILD_FILE: &'static str = "target/build_file";
-    let project_path: &Path = &Path::new("project");
+    let project_path = Path::new("project");
 
+    // Set up git repository before creating the worktree.
     let work_dir = root.path().join("project");
     let mut repo = git_init(work_dir.as_path());
     repo.add_ignore_rule(IGNORE_RULE).unwrap();
-    git_add(Path::new(A_TXT), &repo);
-    git_add(Path::new(E_TXT), &repo);
-    git_add(Path::new(DOTGITIGNORE), &repo);
+    git_add(A_TXT, &repo);
+    git_add(E_TXT, &repo);
+    git_add(DOTGITIGNORE, &repo);
     git_commit("Initial commit", &repo);
 
+    let tree = Worktree::local(
+        build_client(cx),
+        root.path(),
+        true,
+        Arc::new(RealFs),
+        Default::default(),
+        &mut cx.to_async(),
+    )
+    .await
+    .unwrap();
+
     tree.flush_fs_events(cx).await;
+    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+        .await;
     deterministic.run_until_parked();
 
     // Check that the right git state is observed on startup
@@ -1704,39 +1817,39 @@ async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppCont
         );
     });
 
+    // Modify a file in the working copy.
     std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
-
     tree.flush_fs_events(cx).await;
     deterministic.run_until_parked();
 
+    // The worktree detects that the file's git status has changed.
     tree.read_with(cx, |tree, _cx| {
         let snapshot = tree.snapshot();
-
         assert_eq!(
             snapshot.status_for_file(project_path.join(A_TXT)),
             Some(GitFileStatus::Modified)
         );
     });
 
-    git_add(Path::new(A_TXT), &repo);
-    git_add(Path::new(B_TXT), &repo);
+    // Create a commit in the git repository.
+    git_add(A_TXT, &repo);
+    git_add(B_TXT, &repo);
     git_commit("Committing modified and added", &repo);
     tree.flush_fs_events(cx).await;
     deterministic.run_until_parked();
 
-    // Check that repo only changes are tracked
+    // The worktree detects that the files' git status have changed.
     tree.read_with(cx, |tree, _cx| {
         let snapshot = tree.snapshot();
-
         assert_eq!(
             snapshot.status_for_file(project_path.join(F_TXT)),
             Some(GitFileStatus::Added)
         );
-
         assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
         assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
     });
 
+    // Modify files in the working copy and perform git operations on other files.
     git_reset(0, &repo);
     git_remove_index(Path::new(B_TXT), &repo);
     git_stash(&mut repo);

crates/project_panel/Cargo.toml 🔗

@@ -27,6 +27,7 @@ serde_derive.workspace = true
 serde_json.workspace = true
 anyhow.workspace = true
 schemars.workspace = true
+pretty_assertions.workspace = true
 unicase = "2.6"
 
 [dev-dependencies]

crates/project_panel/src/project_panel.rs 🔗

@@ -64,7 +64,7 @@ pub struct ProjectPanel {
     pending_serialization: Task<Option<()>>,
 }
 
-#[derive(Copy, Clone)]
+#[derive(Copy, Clone, Debug)]
 struct Selection {
     worktree_id: WorktreeId,
     entry_id: ProjectEntryId,
@@ -547,7 +547,7 @@ impl ProjectPanel {
                 worktree_id,
                 entry_id: NEW_ENTRY_ID,
             });
-            let new_path = entry.path.join(&filename);
+            let new_path = entry.path.join(&filename.trim_start_matches("/"));
             if path_already_exists(new_path.as_path()) {
                 return None;
             }
@@ -588,6 +588,7 @@ impl ProjectPanel {
                     if selection.entry_id == edited_entry_id {
                         selection.worktree_id = worktree_id;
                         selection.entry_id = new_entry.id;
+                        this.expand_to_selection(cx);
                     }
                 }
                 this.update_visible_entries(None, cx);
@@ -965,6 +966,24 @@ impl ProjectPanel {
         Some((worktree, entry))
     }
 
+    fn expand_to_selection(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
+        let (worktree, entry) = self.selected_entry(cx)?;
+        let expanded_dir_ids = self.expanded_dir_ids.entry(worktree.id()).or_default();
+
+        for path in entry.path.ancestors() {
+            let Some(entry) = worktree.entry_for_path(path) else {
+                continue;
+            };
+            if entry.is_dir() {
+                if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) {
+                    expanded_dir_ids.insert(idx, entry.id);
+                }
+            }
+        }
+
+        Some(())
+    }
+
     fn update_visible_entries(
         &mut self,
         new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
@@ -1592,6 +1611,7 @@ impl ClipboardEntry {
 mod tests {
     use super::*;
     use gpui::{TestAppContext, ViewHandle};
+    use pretty_assertions::assert_eq;
     use project::FakeFs;
     use serde_json::json;
     use settings::SettingsStore;
@@ -2002,6 +2022,133 @@ mod tests {
         );
     }
 
+    #[gpui::test(iterations = 30)]
+    async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/root1",
+            json!({
+                ".dockerignore": "",
+                ".git": {
+                    "HEAD": "",
+                },
+                "a": {
+                    "0": { "q": "", "r": "", "s": "" },
+                    "1": { "t": "", "u": "" },
+                    "2": { "v": "", "w": "", "x": "", "y": "" },
+                },
+                "b": {
+                    "3": { "Q": "" },
+                    "4": { "R": "", "S": "", "T": "", "U": "" },
+                },
+                "C": {
+                    "5": {},
+                    "6": { "V": "", "W": "" },
+                    "7": { "X": "" },
+                    "8": { "Y": {}, "Z": "" }
+                }
+            }),
+        )
+        .await;
+        fs.insert_tree(
+            "/root2",
+            json!({
+                "d": {
+                    "9": ""
+                },
+                "e": {}
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
+
+        select_path(&panel, "root1", cx);
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1  <== selected",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    > C",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+
+        // Add a file with the root folder selected. The filename editor is placed
+        // before the first file in the root folder.
+        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
+        cx.read_window(window_id, |cx| {
+            let panel = panel.read(cx);
+            assert!(panel.filename_editor.is_focused(cx));
+        });
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    > C",
+                "      [EDITOR: '']  <== selected",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+
+        let confirm = panel.update(cx, |panel, cx| {
+            panel.filename_editor.update(cx, |editor, cx| {
+                editor.set_text("/bdir1/dir2/the-new-filename", cx)
+            });
+            panel.confirm(&Confirm, cx).unwrap()
+        });
+
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    > C",
+                "      [PROCESSING: '/bdir1/dir2/the-new-filename']  <== selected",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+
+        confirm.await.unwrap();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..13, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    v bdir1",
+                "        v dir2",
+                "              the-new-filename  <== selected",
+                "    > C",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+    }
+
     #[gpui::test]
     async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
         init_test(cx);

crates/recent_projects/Cargo.toml 🔗

@@ -21,6 +21,7 @@ util = { path = "../util"}
 theme = { path = "../theme" }
 workspace = { path = "../workspace" }
 
+futures.workspace = true
 ordered-float.workspace = true
 postage.workspace = true
 smol.workspace = true

crates/recent_projects/src/recent_projects.rs 🔗

@@ -48,7 +48,7 @@ fn toggle(
                     let workspace = cx.weak_handle();
                     cx.add_view(|cx| {
                         RecentProjects::new(
-                            RecentProjectsDelegate::new(workspace, workspace_locations),
+                            RecentProjectsDelegate::new(workspace, workspace_locations, true),
                             cx,
                         )
                         .with_max_size(800., 1200.)
@@ -64,25 +64,40 @@ fn toggle(
     }))
 }
 
-type RecentProjects = Picker<RecentProjectsDelegate>;
+pub fn build_recent_projects(
+    workspace: WeakViewHandle<Workspace>,
+    workspaces: Vec<WorkspaceLocation>,
+    cx: &mut ViewContext<RecentProjects>,
+) -> RecentProjects {
+    Picker::new(
+        RecentProjectsDelegate::new(workspace, workspaces, false),
+        cx,
+    )
+    .with_theme(|theme| theme.picker.clone())
+}
+
+pub type RecentProjects = Picker<RecentProjectsDelegate>;
 
-struct RecentProjectsDelegate {
+pub struct RecentProjectsDelegate {
     workspace: WeakViewHandle<Workspace>,
     workspace_locations: Vec<WorkspaceLocation>,
     selected_match_index: usize,
     matches: Vec<StringMatch>,
+    render_paths: bool,
 }
 
 impl RecentProjectsDelegate {
     fn new(
         workspace: WeakViewHandle<Workspace>,
         workspace_locations: Vec<WorkspaceLocation>,
+        render_paths: bool,
     ) -> Self {
         Self {
             workspace,
             workspace_locations,
             selected_match_index: 0,
             matches: Default::default(),
+            render_paths,
         }
     }
 }
@@ -188,6 +203,7 @@ impl PickerDelegate for RecentProjectsDelegate {
                 highlighted_location
                     .paths
                     .into_iter()
+                    .filter(|_| self.render_paths)
                     .map(|highlighted_path| highlighted_path.render(style.label.clone())),
             )
             .flex(1., false)

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/settings/Cargo.toml 🔗

@@ -38,5 +38,5 @@ tree-sitter-json = "*"
 gpui = { path = "../gpui", features = ["test-support"] }
 fs = { path = "../fs", features = ["test-support"] }
 indoc.workspace = true
-pretty_assertions = "1.3.0"
+pretty_assertions.workspace = true
 unindent.workspace = true

crates/settings/src/keymap_file.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{settings_store::parse_json_with_comments, SettingsAssets};
 use anyhow::{anyhow, Context, Result};
 use collections::BTreeMap;
-use gpui::{keymap_matcher::Binding, AppContext};
+use gpui::{keymap_matcher::Binding, AppContext, NoAction};
 use schemars::{
     gen::{SchemaGenerator, SchemaSettings},
     schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation},
@@ -11,18 +11,18 @@ use serde::Deserialize;
 use serde_json::Value;
 use util::{asset_str, ResultExt};
 
-#[derive(Deserialize, Default, Clone, JsonSchema)]
+#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
 #[serde(transparent)]
 pub struct KeymapFile(Vec<KeymapBlock>);
 
-#[derive(Deserialize, Default, Clone, JsonSchema)]
+#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
 pub struct KeymapBlock {
     #[serde(default)]
     context: Option<String>,
     bindings: BTreeMap<String, KeymapAction>,
 }
 
-#[derive(Deserialize, Default, Clone)]
+#[derive(Debug, Deserialize, Default, Clone)]
 #[serde(transparent)]
 pub struct KeymapAction(Value);
 
@@ -61,21 +61,22 @@ impl KeymapFile {
                     // We want to deserialize the action data as a `RawValue` so that we can
                     // deserialize the action itself dynamically directly from the JSON
                     // string. But `RawValue` currently does not work inside of an untagged enum.
-                    if let Value::Array(items) = action {
-                        let Ok([name, data]): Result<[serde_json::Value; 2], _> = items.try_into() else {
-                            return Some(Err(anyhow!("Expected array of length 2")));
-                        };
-                        let serde_json::Value::String(name) = name else {
-                            return Some(Err(anyhow!("Expected first item in array to be a string.")))
-                        };
-                        cx.deserialize_action(
-                            &name,
-                            Some(data),
-                        )
-                    } else if let Value::String(name) = action {
-                        cx.deserialize_action(&name, None)
-                    } else {
-                        return Some(Err(anyhow!("Expected two-element array, got {:?}", action)));
+                    match action {
+                        Value::Array(items) => {
+                            let Ok([name, data]): Result<[serde_json::Value; 2], _> = items.try_into() else {
+                                return Some(Err(anyhow!("Expected array of length 2")));
+                            };
+                            let serde_json::Value::String(name) = name else {
+                                return Some(Err(anyhow!("Expected first item in array to be a string.")))
+                            };
+                            cx.deserialize_action(
+                                &name,
+                                Some(data),
+                            )
+                        },
+                        Value::String(name) => cx.deserialize_action(&name, None),
+                        Value::Null => Ok(no_action()),
+                        _ => return Some(Err(anyhow!("Expected two-element array, got {action:?}"))),
                     }
                     .with_context(|| {
                         format!(
@@ -115,6 +116,10 @@ impl KeymapFile {
                         instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
                         ..Default::default()
                     }),
+                    Schema::Object(SchemaObject {
+                        instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Null))),
+                        ..Default::default()
+                    }),
                 ]),
                 ..Default::default()
             })),
@@ -129,6 +134,10 @@ impl KeymapFile {
     }
 }
 
+fn no_action() -> Box<dyn gpui::Action> {
+    Box::new(NoAction {})
+}
+
 #[cfg(test)]
 mod tests {
     use crate::KeymapFile;

crates/terminal_view/src/terminal_element.rs 🔗

@@ -395,16 +395,17 @@ impl TerminalElement {
         // Terminal Emulator controlled behavior:
         region = region
             // Start selections
-            .on_down(
-                MouseButton::Left,
-                TerminalElement::generic_button_handler(
-                    connection,
-                    origin,
-                    move |terminal, origin, e, _cx| {
-                        terminal.mouse_down(&e, origin);
-                    },
-                ),
-            )
+            .on_down(MouseButton::Left, move |event, v: &mut TerminalView, cx| {
+                cx.focus_parent();
+                v.context_menu.update(cx, |menu, _cx| menu.delay_cancel());
+                if let Some(conn_handle) = connection.upgrade(cx) {
+                    conn_handle.update(cx, |terminal, cx| {
+                        terminal.mouse_down(&event, origin);
+
+                        cx.notify();
+                    })
+                }
+            })
             // Update drag selections
             .on_drag(MouseButton::Left, move |event, _: &mut TerminalView, cx| {
                 if cx.is_self_focused() {

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -87,6 +87,7 @@ impl TerminalPanel {
                                 }
                             })
                         },
+                        |_, _| {},
                         None,
                     ))
                     .with_child(Pane::render_tab_bar_button(
@@ -100,6 +101,7 @@ impl TerminalPanel {
                         Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))),
                         cx,
                         move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
+                        |_, _| {},
                         None,
                     ))
                     .into_any()

crates/text/src/text.rs 🔗

@@ -2489,7 +2489,12 @@ impl ToOffset for Point {
 
 impl ToOffset for usize {
     fn to_offset(&self, snapshot: &BufferSnapshot) -> usize {
-        assert!(*self <= snapshot.len(), "offset {self} is out of range");
+        assert!(
+            *self <= snapshot.len(),
+            "offset {} is out of range, max allowed is {}",
+            self,
+            snapshot.len()
+        );
         *self
     }
 }

crates/theme/src/theme.rs 🔗

@@ -65,7 +65,6 @@ pub struct Theme {
     pub assistant: AssistantStyle,
     pub feedback: FeedbackStyle,
     pub welcome: WelcomeStyle,
-    pub color_scheme: ColorScheme,
     pub titlebar: Titlebar,
 }
 
@@ -118,8 +117,9 @@ pub struct Titlebar {
     #[serde(flatten)]
     pub container: ContainerStyle,
     pub height: f32,
-    pub title: TextStyle,
-    pub highlight_color: Color,
+    pub project_menu_button: Toggleable<Interactive<ContainedText>>,
+    pub project_name_divider: ContainedText,
+    pub git_menu_button: Toggleable<Interactive<ContainedText>>,
     pub item_spacing: f32,
     pub face_pile_spacing: f32,
     pub avatar_ribbon: AvatarRibbon,
@@ -585,6 +585,8 @@ pub struct Picker {
     pub empty_input_editor: FieldEditor,
     pub no_matches: ContainedLabel,
     pub item: Toggleable<Interactive<ContainedLabel>>,
+    pub header: ContainedLabel,
+    pub footer: ContainedLabel,
 }
 
 #[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
@@ -720,6 +722,7 @@ pub struct Scrollbar {
     pub width: f32,
     pub min_height_factor: f32,
     pub git: GitDiffColors,
+    pub selections: Color,
 }
 
 #[derive(Clone, Deserialize, Default, JsonSchema)]

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/pane.rs 🔗

@@ -273,6 +273,11 @@ impl Pane {
                         Some(("New...".into(), None)),
                         cx,
                         |pane, cx| pane.deploy_new_menu(cx),
+                        |pane, cx| {
+                            pane.tab_bar_context_menu
+                                .handle
+                                .update(cx, |menu, _| menu.delay_cancel())
+                        },
                         pane.tab_bar_context_menu
                             .handle_if_kind(TabBarContextMenuKind::New),
                     ))
@@ -283,6 +288,11 @@ impl Pane {
                         Some(("Split Pane".into(), None)),
                         cx,
                         |pane, cx| pane.deploy_split_menu(cx),
+                        |pane, cx| {
+                            pane.tab_bar_context_menu
+                                .handle
+                                .update(cx, |menu, _| menu.delay_cancel())
+                        },
                         pane.tab_bar_context_menu
                             .handle_if_kind(TabBarContextMenuKind::Split),
                     ))
@@ -304,6 +314,7 @@ impl Pane {
                             Some((tooltip_label, Some(Box::new(ToggleZoom)))),
                             cx,
                             move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
+                            move |_, _| {},
                             None,
                         )
                     })
@@ -988,7 +999,7 @@ impl Pane {
 
     fn deploy_split_menu(&mut self, cx: &mut ViewContext<Self>) {
         self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
-            menu.show(
+            menu.toggle(
                 Default::default(),
                 AnchorCorner::TopRight,
                 vec![
@@ -1006,7 +1017,7 @@ impl Pane {
 
     fn deploy_new_menu(&mut self, cx: &mut ViewContext<Self>) {
         self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
-            menu.show(
+            menu.toggle(
                 Default::default(),
                 AnchorCorner::TopRight,
                 vec![
@@ -1416,13 +1427,17 @@ impl Pane {
             .into_any()
     }
 
-    pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
+    pub fn render_tab_bar_button<
+        F1: 'static + Fn(&mut Pane, &mut EventContext<Pane>),
+        F2: 'static + Fn(&mut Pane, &mut EventContext<Pane>),
+    >(
         index: usize,
         icon: &'static str,
         is_active: bool,
         tooltip: Option<(String, Option<Box<dyn Action>>)>,
         cx: &mut ViewContext<Pane>,
-        on_click: F,
+        on_click: F1,
+        on_down: F2,
         context_menu: Option<ViewHandle<ContextMenu>>,
     ) -> AnyElement<Pane> {
         enum TabBarButton {}
@@ -1440,6 +1455,7 @@ impl Pane {
                 .with_height(style.button_width)
         })
         .with_cursor_style(CursorStyle::PointingHand)
+        .on_down(MouseButton::Left, move |_, pane, cx| on_down(pane, cx))
         .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx))
         .into_any();
         if let Some((tooltip, action)) = tooltip {

crates/workspace/src/workspace.rs 🔗

@@ -97,9 +97,25 @@ lazy_static! {
 }
 
 pub trait Modal: View {
+    fn has_focus(&self) -> bool;
     fn dismiss_on_event(event: &Self::Event) -> bool;
 }
 
+trait ModalHandle {
+    fn as_any(&self) -> &AnyViewHandle;
+    fn has_focus(&self, cx: &WindowContext) -> bool;
+}
+
+impl<T: Modal> ModalHandle for ViewHandle<T> {
+    fn as_any(&self) -> &AnyViewHandle {
+        self
+    }
+
+    fn has_focus(&self, cx: &WindowContext) -> bool {
+        self.read(cx).has_focus()
+    }
+}
+
 #[derive(Clone, PartialEq)]
 pub struct RemoveWorktreeFromProject(pub WorktreeId);
 
@@ -466,7 +482,7 @@ pub enum Event {
 pub struct Workspace {
     weak_self: WeakViewHandle<Self>,
     remote_entity_subscription: Option<client::Subscription>,
-    modal: Option<AnyViewHandle>,
+    modal: Option<ActiveModal>,
     zoomed: Option<AnyWeakViewHandle>,
     zoomed_position: Option<DockPosition>,
     center: PaneGroup,
@@ -495,6 +511,11 @@ pub struct Workspace {
     pane_history_timestamp: Arc<AtomicUsize>,
 }
 
+struct ActiveModal {
+    view: Box<dyn ModalHandle>,
+    previously_focused_view_id: Option<usize>,
+}
+
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
 pub struct ViewId {
     pub creator: PeerId,
@@ -1482,8 +1503,10 @@ impl Workspace {
         cx.notify();
         // Whatever modal was visible is getting clobbered. If its the same type as V, then return
         // it. Otherwise, create a new modal and set it as active.
-        let already_open_modal = self.modal.take().and_then(|modal| modal.downcast::<V>());
-        if let Some(already_open_modal) = already_open_modal {
+        if let Some(already_open_modal) = self
+            .dismiss_modal(cx)
+            .and_then(|modal| modal.downcast::<V>())
+        {
             cx.focus_self();
             Some(already_open_modal)
         } else {
@@ -1494,8 +1517,12 @@ impl Workspace {
                 }
             })
             .detach();
+            let previously_focused_view_id = cx.focused_view_id();
             cx.focus(&modal);
-            self.modal = Some(modal.into_any());
+            self.modal = Some(ActiveModal {
+                view: Box::new(modal),
+                previously_focused_view_id,
+            });
             None
         }
     }
@@ -1503,13 +1530,20 @@ impl Workspace {
     pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
         self.modal
             .as_ref()
-            .and_then(|modal| modal.clone().downcast::<V>())
+            .and_then(|modal| modal.view.as_any().clone().downcast::<V>())
     }
 
-    pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
-        if self.modal.take().is_some() {
-            cx.focus(&self.active_pane);
+    pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) -> Option<AnyViewHandle> {
+        if let Some(modal) = self.modal.take() {
+            if let Some(previously_focused_view_id) = modal.previously_focused_view_id {
+                if modal.view.has_focus(cx) {
+                    cx.window_context().focus(Some(previously_focused_view_id));
+                }
+            }
             cx.notify();
+            Some(modal.view.as_any().clone())
+        } else {
+            None
         }
     }
 
@@ -3496,7 +3530,7 @@ impl View for Workspace {
                                         )
                                     }))
                                     .with_children(self.modal.as_ref().map(|modal| {
-                                        ChildView::new(modal, cx)
+                                        ChildView::new(modal.view.as_any(), cx)
                                             .contained()
                                             .with_style(theme.workspace.modal)
                                             .aligned()
@@ -4775,6 +4809,7 @@ mod tests {
             theme::init((), cx);
             language::init(cx);
             crate::init_settings(cx);
+            Project::init_settings(cx);
         });
     }
 }

crates/zed/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
 description = "The fast, collaborative code editor."
 edition = "2021"
 name = "zed"
-version = "0.94.0"
+version = "0.95.0"
 publish = false
 
 [lib]
@@ -16,6 +16,7 @@ name = "Zed"
 path = "src/main.rs"
 
 [dependencies]
+audio = { path = "../audio" }
 activity_indicator = { path = "../activity_indicator" }
 auto_update = { path = "../auto_update" }
 breadcrumbs = { path = "../breadcrumbs" }

crates/zed/src/assets.rs 🔗

@@ -7,6 +7,7 @@ use rust_embed::RustEmbed;
 #[include = "fonts/**/*"]
 #[include = "icons/**/*"]
 #[include = "themes/**/*"]
+#[include = "sounds/**/*"]
 #[include = "*.md"]
 #[exclude = "*.DS_Store"]
 pub struct Assets;

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();
 
@@ -180,6 +185,8 @@ fn main() {
             background_actions,
         });
         cx.set_global(Arc::downgrade(&app_state));
+
+        audio::init(Assets, cx);
         auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx);
 
         workspace::init(app_state.clone(), cx);

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;
 
@@ -2074,6 +2075,167 @@ mod tests {
             line!(),
         );
 
+        #[track_caller]
+        fn assert_key_bindings_for<'a>(
+            window_id: usize,
+            cx: &TestAppContext,
+            actions: Vec<(&'static str, &'a dyn Action)>,
+            line: u32,
+        ) {
+            for (key, action) in actions {
+                // assert that...
+                assert!(
+                    cx.available_actions(window_id, 0)
+                        .into_iter()
+                        .any(|(_, bound_action, b)| {
+                            // action names match...
+                            bound_action.name() == action.name()
+                        && bound_action.namespace() == action.namespace()
+                        // and key strokes contain the given key
+                        && b.iter()
+                            .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
+                        }),
+                    "On {} Failed to find {} with key binding {}",
+                    line,
+                    action.name(),
+                    key
+                );
+            }
+        }
+    }
+
+    #[gpui::test]
+    async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
+        struct TestView;
+
+        impl Entity for TestView {
+            type Event = ();
+        }
+
+        impl View for TestView {
+            fn ui_name() -> &'static str {
+                "TestView"
+            }
+
+            fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
+                Empty::new().into_any()
+            }
+        }
+
+        let executor = cx.background();
+        let fs = FakeFs::new(executor.clone());
+
+        actions!(test, [A, B]);
+        // From the Atom keymap
+        actions!(workspace, [ActivatePreviousPane]);
+        // From the JetBrains keymap
+        actions!(pane, [ActivatePrevItem]);
+
+        fs.save(
+            "/settings.json".as_ref(),
+            &r#"
+            {
+                "base_keymap": "Atom"
+            }
+            "#
+            .into(),
+            Default::default(),
+        )
+        .await
+        .unwrap();
+
+        fs.save(
+            "/keymap.json".as_ref(),
+            &r#"
+            [
+                {
+                    "bindings": {
+                        "backspace": "test::A"
+                    }
+                }
+            ]
+            "#
+            .into(),
+            Default::default(),
+        )
+        .await
+        .unwrap();
+
+        cx.update(|cx| {
+            cx.set_global(SettingsStore::test(cx));
+            theme::init(Assets, cx);
+            welcome::init(cx);
+
+            cx.add_global_action(|_: &A, _cx| {});
+            cx.add_global_action(|_: &B, _cx| {});
+            cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
+            cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
+
+            let settings_rx = watch_config_file(
+                executor.clone(),
+                fs.clone(),
+                PathBuf::from("/settings.json"),
+            );
+            let keymap_rx =
+                watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
+
+            handle_keymap_file_changes(keymap_rx, cx);
+            handle_settings_file_changes(settings_rx, cx);
+        });
+
+        cx.foreground().run_until_parked();
+
+        let (window_id, _view) = cx.add_window(|_| TestView);
+
+        // Test loading the keymap base at all
+        assert_key_bindings_for(
+            window_id,
+            cx,
+            vec![("backspace", &A), ("k", &ActivatePreviousPane)],
+            line!(),
+        );
+
+        // Test disabling the key binding for the base keymap
+        fs.save(
+            "/keymap.json".as_ref(),
+            &r#"
+            [
+                {
+                    "bindings": {
+                        "backspace": null
+                    }
+                }
+            ]
+            "#
+            .into(),
+            Default::default(),
+        )
+        .await
+        .unwrap();
+
+        cx.foreground().run_until_parked();
+
+        assert_key_bindings_for(window_id, cx, vec![("k", &ActivatePreviousPane)], line!());
+
+        // Test modifying the base, while retaining the users keymap
+        fs.save(
+            "/settings.json".as_ref(),
+            &r#"
+            {
+                "base_keymap": "JetBrains"
+            }
+            "#
+            .into(),
+            Default::default(),
+        )
+        .await
+        .unwrap();
+
+        cx.foreground().run_until_parked();
+
+        assert_key_bindings_for(window_id, cx, vec![("[", &ActivatePrevItem)], line!());
+
+        #[track_caller]
         fn assert_key_bindings_for<'a>(
             window_id: usize,
             cx: &TestAppContext,
@@ -2160,6 +2322,7 @@ mod tests {
             state.initialize_workspace = initialize_workspace;
             state.build_window_options = build_window_options;
             theme::init((), cx);
+            audio::init((), cx);
             call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
             workspace::init(app_state.clone(), cx);
             Project::init_settings(cx);

docs/zed/syntax-highlighting.md 🔗

@@ -35,7 +35,7 @@ Match a property identifier and highlight it using the identifier `@property`. I
 ```
 
 ```ts
-function buildDefaultSyntax(colorScheme: ColorScheme): Partial<Syntax> {
+function buildDefaultSyntax(colorScheme: Theme): Partial<Syntax> {
     // ...
 }
 ```

styles/package-lock.json 🔗

@@ -27,7 +27,8 @@
                 "ts-node": "^10.9.1",
                 "typescript": "^5.1.5",
                 "utility-types": "^3.10.0",
-                "vitest": "^0.32.0"
+                "vitest": "^0.32.0",
+                "zustand": "^4.3.8"
             }
         },
         "node_modules/@aashutoshrathi/word-wrap": {
@@ -2595,6 +2596,12 @@
                 "node": ">= 0.8"
             }
         },
+        "node_modules/js-tokens": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+            "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+            "peer": true
+        },
         "node_modules/js-yaml": {
             "version": "4.1.0",
             "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@@ -2706,6 +2713,18 @@
             "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
             "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
         },
+        "node_modules/loose-envify": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+            "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+            "peer": true,
+            "dependencies": {
+                "js-tokens": "^3.0.0 || ^4.0.0"
+            },
+            "bin": {
+                "loose-envify": "cli.js"
+            }
+        },
         "node_modules/loupe": {
             "version": "2.3.6",
             "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz",
@@ -3292,6 +3311,18 @@
                 }
             ]
         },
+        "node_modules/react": {
+            "version": "18.2.0",
+            "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
+            "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
+            "peer": true,
+            "dependencies": {
+                "loose-envify": "^1.1.0"
+            },
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
         "node_modules/react-is": {
             "version": "17.0.2",
             "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@@ -4025,6 +4056,14 @@
                 "punycode": "^2.1.0"
             }
         },
+        "node_modules/use-sync-external-store": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+            "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+            "peerDependencies": {
+                "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+            }
+        },
         "node_modules/utility-types": {
             "version": "3.10.0",
             "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz",
@@ -4305,6 +4344,29 @@
             "funding": {
                 "url": "https://github.com/sponsors/sindresorhus"
             }
+        },
+        "node_modules/zustand": {
+            "version": "4.3.8",
+            "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.8.tgz",
+            "integrity": "sha512-4h28KCkHg5ii/wcFFJ5Fp+k1J3gJoasaIbppdgZFO4BPJnsNxL0mQXBSFgOgAdCdBj35aDTPvdAJReTMntFPGg==",
+            "dependencies": {
+                "use-sync-external-store": "1.2.0"
+            },
+            "engines": {
+                "node": ">=12.7.0"
+            },
+            "peerDependencies": {
+                "immer": ">=9.0",
+                "react": ">=16.8"
+            },
+            "peerDependenciesMeta": {
+                "immer": {
+                    "optional": true
+                },
+                "react": {
+                    "optional": true
+                }
+            }
         }
     }
 }

styles/package.json 🔗

@@ -16,21 +16,22 @@
         "@tokens-studio/types": "^0.2.3",
         "@types/chroma-js": "^2.4.0",
         "@types/node": "^18.14.1",
+        "@typescript-eslint/eslint-plugin": "^5.60.1",
+        "@typescript-eslint/parser": "^5.60.1",
+        "@vitest/coverage-v8": "^0.32.0",
         "ayu": "^8.0.1",
         "chroma-js": "^2.4.2",
         "deepmerge": "^4.3.0",
+        "eslint": "^8.43.0",
+        "eslint-import-resolver-typescript": "^3.5.5",
+        "eslint-plugin-import": "^2.27.5",
         "json-schema-to-typescript": "^13.0.2",
         "toml": "^3.0.0",
         "ts-deepmerge": "^6.0.3",
         "ts-node": "^10.9.1",
+        "typescript": "^5.1.5",
         "utility-types": "^3.10.0",
         "vitest": "^0.32.0",
-        "@typescript-eslint/eslint-plugin": "^5.60.1",
-        "@typescript-eslint/parser": "^5.60.1",
-        "@vitest/coverage-v8": "^0.32.0",
-        "eslint": "^8.43.0",
-        "eslint-import-resolver-typescript": "^3.5.5",
-        "eslint-plugin-import": "^2.27.5",
-        "typescript": "^5.1.5"
+        "zustand": "^4.3.8"
     }
 }

styles/src/build_themes.ts 🔗

@@ -2,8 +2,9 @@ import * as fs from "fs"
 import { tmpdir } from "os"
 import * as path from "path"
 import app from "./style_tree/app"
-import { ColorScheme, create_color_scheme } from "./theme/color_scheme"
+import { Theme, create_theme } from "./theme/create_theme"
 import { themes } from "./themes"
+import { useThemeStore } from "./theme"
 
 const assets_directory = `${__dirname}/../../assets`
 const temp_directory = fs.mkdtempSync(path.join(tmpdir(), "build-themes"))
@@ -20,15 +21,22 @@ function clear_themes(theme_directory: string) {
     }
 }
 
-function write_themes(themes: ColorScheme[], output_directory: string) {
+const all_themes: Theme[] = themes.map((theme) =>
+    create_theme(theme)
+)
+
+function write_themes(themes: Theme[], output_directory: string) {
     clear_themes(output_directory)
-    for (const color_scheme of themes) {
-        const style_tree = app(color_scheme)
+    for (const theme of themes) {
+        const { setTheme } = useThemeStore.getState()
+        setTheme(theme)
+
+        const style_tree = app()
         const style_tree_json = JSON.stringify(style_tree, null, 2)
-        const temp_path = path.join(temp_directory, `${color_scheme.name}.json`)
+        const temp_path = path.join(temp_directory, `${theme.name}.json`)
         const out_path = path.join(
             output_directory,
-            `${color_scheme.name}.json`
+            `${theme.name}.json`
         )
         fs.writeFileSync(temp_path, style_tree_json)
         fs.renameSync(temp_path, out_path)
@@ -36,8 +44,4 @@ function write_themes(themes: ColorScheme[], output_directory: string) {
     }
 }
 
-const all_themes: ColorScheme[] = themes.map((theme) =>
-    create_color_scheme(theme)
-)
-
 write_themes(all_themes, `${assets_directory}/themes`)

styles/src/build_tokens.ts 🔗

@@ -1,9 +1,9 @@
 import * as fs from "fs"
 import * as path from "path"
-import { ColorScheme, create_color_scheme } from "./common"
+import { Theme, create_theme, useThemeStore } from "./common"
 import { themes } from "./themes"
 import { slugify } from "./utils/slugify"
-import { theme_tokens } from "./theme/tokens/color_scheme"
+import { theme_tokens } from "./theme/tokens/theme"
 
 const TOKENS_DIRECTORY = path.join(__dirname, "..", "target", "tokens")
 const TOKENS_FILE = path.join(TOKENS_DIRECTORY, "$themes.json")
@@ -27,7 +27,7 @@ type TokenSet = {
     selected_token_sets: { [key: string]: "enabled" }
 }
 
-function build_token_set_order(theme: ColorScheme[]): {
+function build_token_set_order(theme: Theme[]): {
     token_set_order: string[]
 } {
     const token_set_order: string[] = theme.map((scheme) =>
@@ -36,7 +36,7 @@ function build_token_set_order(theme: ColorScheme[]): {
     return { token_set_order }
 }
 
-function build_themes_index(theme: ColorScheme[]): TokenSet[] {
+function build_themes_index(theme: Theme[]): TokenSet[] {
     const themes_index: TokenSet[] = theme.map((scheme, index) => {
         const id = `${scheme.is_light ? "light" : "dark"}_${scheme.name
             .toLowerCase()
@@ -55,12 +55,15 @@ function build_themes_index(theme: ColorScheme[]): TokenSet[] {
     return themes_index
 }
 
-function write_tokens(themes: ColorScheme[], tokens_directory: string) {
+function write_tokens(themes: Theme[], tokens_directory: string) {
     clear_tokens(tokens_directory)
 
     for (const theme of themes) {
+        const { setTheme } = useThemeStore.getState()
+        setTheme(theme)
+
         const file_name = slugify(theme.name) + ".json"
-        const tokens = theme_tokens(theme)
+        const tokens = theme_tokens()
         const tokens_json = JSON.stringify(tokens, null, 2)
         const out_path = path.join(tokens_directory, file_name)
         fs.writeFileSync(out_path, tokens_json, { mode: 0o644 })
@@ -80,8 +83,8 @@ function write_tokens(themes: ColorScheme[], tokens_directory: string) {
     console.log(`- ${METADATA_FILE} created`)
 }
 
-const all_themes: ColorScheme[] = themes.map((theme) =>
-    create_color_scheme(theme)
+const all_themes: Theme[] = themes.map((theme) =>
+    create_theme(theme)
 )
 
 write_tokens(all_themes, TOKENS_DIRECTORY)

styles/src/component/icon_button.ts 🔗

@@ -1,6 +1,6 @@
 import { interactive, toggleable } from "../element"
 import { background, foreground } from "../style_tree/components"
-import { ColorScheme } from "../theme/color_scheme"
+import { useTheme, Theme } from "../theme"
 
 export type Margin = {
     top: number
@@ -11,21 +11,20 @@ export type Margin = {
 
 interface IconButtonOptions {
     layer?:
-        | ColorScheme["lowest"]
-        | ColorScheme["middle"]
-        | ColorScheme["highest"]
-    color?: keyof ColorScheme["lowest"]
+    | Theme["lowest"]
+    | Theme["middle"]
+    | Theme["highest"]
+    color?: keyof Theme["lowest"]
     margin?: Partial<Margin>
 }
 
 type ToggleableIconButtonOptions = IconButtonOptions & {
-    active_color?: keyof ColorScheme["lowest"]
+    active_color?: keyof Theme["lowest"]
 }
 
-export function icon_button(
-    theme: ColorScheme,
-    { color, margin, layer }: IconButtonOptions
-) {
+export function icon_button({ color, margin, layer }: IconButtonOptions) {
+    const theme = useTheme()
+
     if (!color) color = "base"
 
     const m = {
@@ -68,15 +67,15 @@ export function icon_button(
 }
 
 export function toggleable_icon_button(
-    theme: ColorScheme,
+    theme: Theme,
     { color, active_color, margin }: ToggleableIconButtonOptions
 ) {
     if (!color) color = "base"
 
     return toggleable({
         state: {
-            inactive: icon_button(theme, { color, margin }),
-            active: icon_button(theme, {
+            inactive: icon_button({ color, margin }),
+            active: icon_button({
                 color: active_color ? active_color : color,
                 margin,
                 layer: theme.middle,

styles/src/component/text_button.ts 🔗

@@ -5,27 +5,30 @@ import {
     foreground,
     text,
 } from "../style_tree/components"
-import { ColorScheme } from "../theme/color_scheme"
+import { useTheme, Theme } from "../theme"
 import { Margin } from "./icon_button"
 
 interface TextButtonOptions {
     layer?:
-        | ColorScheme["lowest"]
-        | ColorScheme["middle"]
-        | ColorScheme["highest"]
-    color?: keyof ColorScheme["lowest"]
+    | Theme["lowest"]
+    | Theme["middle"]
+    | Theme["highest"]
+    color?: keyof Theme["lowest"]
     margin?: Partial<Margin>
     text_properties?: TextProperties
 }
 
 type ToggleableTextButtonOptions = TextButtonOptions & {
-    active_color?: keyof ColorScheme["lowest"]
+    active_color?: keyof Theme["lowest"]
 }
 
-export function text_button(
-    theme: ColorScheme,
-    { color, layer, margin, text_properties }: TextButtonOptions
-) {
+export function text_button({
+    color,
+    layer,
+    margin,
+    text_properties,
+}: TextButtonOptions) {
+    const theme = useTheme()
     if (!color) color = "base"
 
     const text_options: TextProperties = {
@@ -72,15 +75,15 @@ export function text_button(
 }
 
 export function toggleable_text_button(
-    theme: ColorScheme,
+    theme: Theme,
     { color, active_color, margin }: ToggleableTextButtonOptions
 ) {
     if (!color) color = "base"
 
     return toggleable({
         state: {
-            inactive: text_button(theme, { color, margin }),
-            active: text_button(theme, {
+            inactive: text_button({ color, margin }),
+            active: text_button({
                 color: active_color ? active_color : color,
                 margin,
                 layer: theme.middle,

styles/src/style_tree/app.ts 🔗

@@ -17,59 +17,46 @@ import terminal from "./terminal"
 import contact_list from "./contact_list"
 import toolbar_dropdown_menu from "./toolbar_dropdown_menu"
 import incoming_call_notification from "./incoming_call_notification"
-import { ColorScheme } from "../theme/color_scheme"
 import welcome from "./welcome"
 import copilot from "./copilot"
 import assistant from "./assistant"
 import { titlebar } from "./titlebar"
 import editor from "./editor"
 import feedback from "./feedback"
+import { useTheme } from "../common"
+
+export default function app(): any {
+    const theme = useTheme()
 
-export default function app(theme: ColorScheme): any {
     return {
         meta: {
             name: theme.name,
             is_light: theme.is_light,
         },
-        command_palette: command_palette(theme),
-        contact_notification: contact_notification(theme),
-        project_shared_notification: project_shared_notification(theme),
-        incoming_call_notification: incoming_call_notification(theme),
-        picker: picker(theme),
-        workspace: workspace(theme),
-        titlebar: titlebar(theme),
-        copilot: copilot(theme),
-        welcome: welcome(theme),
-        context_menu: context_menu(theme),
-        editor: editor(theme),
-        project_diagnostics: project_diagnostics(theme),
-        project_panel: project_panel(theme),
-        contacts_popover: contacts_popover(theme),
-        contact_finder: contact_finder(theme),
-        contact_list: contact_list(theme),
-        toolbar_dropdown_menu: toolbar_dropdown_menu(theme),
-        search: search(theme),
-        shared_screen: shared_screen(theme),
-        update_notification: update_notification(theme),
-        simple_message_notification: simple_message_notification(theme),
-        tooltip: tooltip(theme),
-        terminal: terminal(theme),
-        assistant: assistant(theme),
-        feedback: feedback(theme),
-        color_scheme: {
-            ...theme,
-            players: Object.values(theme.players),
-            ramps: {
-                neutral: theme.ramps.neutral.colors(100, "hex"),
-                red: theme.ramps.red.colors(100, "hex"),
-                orange: theme.ramps.orange.colors(100, "hex"),
-                yellow: theme.ramps.yellow.colors(100, "hex"),
-                green: theme.ramps.green.colors(100, "hex"),
-                cyan: theme.ramps.cyan.colors(100, "hex"),
-                blue: theme.ramps.blue.colors(100, "hex"),
-                violet: theme.ramps.violet.colors(100, "hex"),
-                magenta: theme.ramps.magenta.colors(100, "hex"),
-            },
-        },
+        command_palette: command_palette(),
+        contact_notification: contact_notification(),
+        project_shared_notification: project_shared_notification(),
+        incoming_call_notification: incoming_call_notification(),
+        picker: picker(),
+        workspace: workspace(),
+        titlebar: titlebar(),
+        copilot: copilot(),
+        welcome: welcome(),
+        context_menu: context_menu(),
+        editor: editor(),
+        project_diagnostics: project_diagnostics(),
+        project_panel: project_panel(),
+        contacts_popover: contacts_popover(),
+        contact_finder: contact_finder(),
+        contact_list: contact_list(),
+        toolbar_dropdown_menu: toolbar_dropdown_menu(),
+        search: search(),
+        shared_screen: shared_screen(),
+        update_notification: update_notification(),
+        simple_message_notification: simple_message_notification(),
+        tooltip: tooltip(),
+        terminal: terminal(),
+        assistant: assistant(),
+        feedback: feedback()
     }
 }

styles/src/style_tree/assistant.ts 🔗

@@ -1,11 +1,7 @@
-import { ColorScheme, StyleSets } from "../theme/color_scheme"
 import { text, border, background, foreground, TextStyle } from "./components"
 import { Interactive, interactive } from "../element"
 import { tab_bar_button } from "../component/tab_bar_button"
-
-interface ToolbarButtonOptions {
-    icon: string
-}
+import { StyleSets, useTheme } from "../theme"
 
 type RoleCycleButton = TextStyle & {
     background?: string
@@ -23,7 +19,8 @@ type RemainingTokens = TextStyle & {
     corner_radius: number,
 }
 
-export default function assistant(theme: ColorScheme): any {
+export default function assistant(): any {
+    const theme = useTheme()
 
     const interactive_role = (color: StyleSets): Interactive<RoleCycleButton> => {
         return (

styles/src/style_tree/command_palette.ts 🔗

@@ -1,9 +1,11 @@
-import { ColorScheme } from "../theme/color_scheme"
 import { with_opacity } from "../theme/color"
 import { text, background } from "./components"
 import { toggleable } from "../element"
+import { useTheme } from "../theme"
+
+export default function command_palette(): any {
+    const theme = useTheme()
 
-export default function command_palette(theme: ColorScheme): any {
     const key = toggleable({
         base: {
             text: text(theme.highest, "mono", "variant", "default", {

styles/src/style_tree/components.ts 🔗

@@ -1,5 +1,5 @@
 import { font_families, font_sizes, FontWeight } from "../common"
-import { Layer, Styles, StyleSets, Style } from "../theme/color_scheme"
+import { Layer, Styles, StyleSets, Style } from "../theme/create_theme"
 
 function is_style_set(key: any): key is StyleSets {
     return [

styles/src/style_tree/contact_finder.ts 🔗

@@ -1,8 +1,10 @@
 import picker from "./picker"
-import { ColorScheme } from "../theme/color_scheme"
 import { background, border, foreground, text } from "./components"
+import { useTheme } from "../theme"
+
+export default function contact_finder(): any {
+    const theme = useTheme()
 
-export default function contact_finder(theme: ColorScheme): any {
     const side_margin = 6
     const contact_button = {
         background: background(theme.middle, "variant"),
@@ -12,7 +14,7 @@ export default function contact_finder(theme: ColorScheme): any {
         corner_radius: 8,
     }
 
-    const picker_style = picker(theme)
+    const picker_style = picker()
     const picker_input = {
         background: background(theme.middle, "on"),
         corner_radius: 6,
@@ -44,6 +46,8 @@ export default function contact_finder(theme: ColorScheme): any {
             no_matches: picker_style.no_matches,
             input_editor: picker_input,
             empty_input_editor: picker_input,
+            header: picker_style.header,
+            footer: picker_style.footer,
         },
         row_height: 28,
         contact_avatar: {

styles/src/style_tree/contact_list.ts 🔗

@@ -1,4 +1,3 @@
-import { ColorScheme } from "../theme/color_scheme"
 import {
     background,
     border,
@@ -7,7 +6,10 @@ import {
     text,
 } from "./components"
 import { interactive, toggleable } from "../element"
-export default function contacts_panel(theme: ColorScheme): any {
+import { useTheme } from "../theme"
+export default function contacts_panel(): any {
+    const theme = useTheme()
+
     const name_margin = 8
     const side_padding = 12
 

styles/src/style_tree/contact_notification.ts 🔗

@@ -1,8 +1,10 @@
-import { ColorScheme } from "../theme/color_scheme"
 import { background, foreground, text } from "./components"
 import { interactive } from "../element"
+import { useTheme } from "../theme"
+
+export default function contact_notification(): any {
+    const theme = useTheme()
 
-export default function contact_notification(theme: ColorScheme): any {
     const avatar_size = 12
     const header_padding = 8
 

styles/src/style_tree/contacts_popover.ts 🔗

@@ -1,7 +1,9 @@
-import { ColorScheme } from "../theme/color_scheme"
+import { useTheme } from "../theme"
 import { background, border } from "./components"
 
-export default function contacts_popover(theme: ColorScheme): any {
+export default function contacts_popover(): any {
+    const theme = useTheme()
+
     return {
         background: background(theme.middle),
         corner_radius: 6,

styles/src/style_tree/context_menu.ts 🔗

@@ -1,8 +1,10 @@
-import { ColorScheme } from "../theme/color_scheme"
 import { background, border, border_color, text } from "./components"
 import { interactive, toggleable } from "../element"
+import { useTheme } from "../theme"
+
+export default function context_menu(): any {
+    const theme = useTheme()
 
-export default function context_menu(theme: ColorScheme): any {
     return {
         background: background(theme.middle),
         corner_radius: 10,

styles/src/style_tree/copilot.ts 🔗

@@ -1,7 +1,9 @@
-import { ColorScheme } from "../theme/color_scheme"
 import { background, border, foreground, svg, text } from "./components"
 import { interactive } from "../element"
-export default function copilot(theme: ColorScheme): any {
+import { useTheme } from "../theme"
+export default function copilot(): any {
+    const theme = useTheme()
+
     const content_width = 264
 
     const cta_button =

styles/src/style_tree/editor.ts 🔗

@@ -1,5 +1,5 @@
 import { with_opacity } from "../theme/color"
-import { ColorScheme, Layer, StyleSets } from "../theme/color_scheme"
+import { Layer, StyleSets } from "../theme/create_theme"
 import {
     background,
     border,
@@ -11,8 +11,11 @@ import hover_popover from "./hover_popover"
 
 import { build_syntax } from "../theme/syntax"
 import { interactive, toggleable } from "../element"
+import { useTheme } from "../theme"
+
+export default function editor(): any {
+    const theme = useTheme()
 
-export default function editor(theme: ColorScheme): any {
     const { is_light } = theme
 
     const layer = theme.highest
@@ -45,7 +48,7 @@ export default function editor(theme: ColorScheme): any {
         }
     }
 
-    const syntax = build_syntax(theme)
+    const syntax = build_syntax()
 
     return {
         text_color: syntax.primary.color,
@@ -248,7 +251,7 @@ export default function editor(theme: ColorScheme): any {
         invalid_hint_diagnostic: diagnostic(theme.middle, "base"),
         invalid_information_diagnostic: diagnostic(theme.middle, "base"),
         invalid_warning_diagnostic: diagnostic(theme.middle, "base"),
-        hover_popover: hover_popover(theme),
+        hover_popover: hover_popover(),
         link_definition: {
             color: syntax.link_uri.color,
             underline: syntax.link_uri.underline,
@@ -301,6 +304,7 @@ export default function editor(theme: ColorScheme): any {
                     ? with_opacity(theme.ramps.green(0.5).hex(), 0.8)
                     : with_opacity(theme.ramps.green(0.4).hex(), 0.8),
             },
+            selections: foreground(layer, "accent")
         },
         composition_mark: {
             underline: {

styles/src/style_tree/feedback.ts 🔗

@@ -1,8 +1,10 @@
-import { ColorScheme } from "../theme/color_scheme"
 import { background, border, text } from "./components"
 import { interactive } from "../element"
+import { useTheme } from "../theme"
+
+export default function feedback(): any {
+    const theme = useTheme()
 
-export default function feedback(theme: ColorScheme): any {
     return {
         submit_button: interactive({
             base: {

styles/src/style_tree/hover_popover.ts 🔗

@@ -1,7 +1,9 @@
-import { ColorScheme } from "../theme/color_scheme"
+import { useTheme } from "../theme"
 import { background, border, foreground, text } from "./components"
 
-export default function hover_popover(theme: ColorScheme): any {
+export default function hover_popover(): any {
+    const theme = useTheme()
+
     const base_container = {
         background: background(theme.middle),
         corner_radius: 8,

styles/src/style_tree/incoming_call_notification.ts 🔗

@@ -1,9 +1,9 @@
-import { ColorScheme } from "../theme/color_scheme"
+import { useTheme } from "../theme"
 import { background, border, text } from "./components"
 
-export default function incoming_call_notification(
-    theme: ColorScheme
-): unknown {
+export default function incoming_call_notification(): unknown {
+    const theme = useTheme()
+
     const avatar_size = 48
     return {
         window_height: 74,

styles/src/style_tree/picker.ts 🔗

@@ -1,9 +1,11 @@
-import { ColorScheme } from "../theme/color_scheme"
 import { with_opacity } from "../theme/color"
 import { background, border, text } from "./components"
 import { interactive, toggleable } from "../element"
+import { useTheme } from "../theme"
+
+export default function picker(): any {
+    const theme = useTheme()
 
-export default function picker(theme: ColorScheme): any {
     const container = {
         background: background(theme.lowest),
         border: border(theme.lowest),
@@ -108,5 +110,23 @@ export default function picker(theme: ColorScheme): any {
                 top: 8,
             },
         },
+        header: {
+            text: text(theme.lowest, "sans", "variant", { size: "xs" }),
+
+            margin: {
+                top: 1,
+                left: 8,
+                right: 8,
+            },
+        },
+        footer: {
+            text: text(theme.lowest, "sans", "variant", { size: "xs" }),
+            margin: {
+                top: 1,
+                left: 8,
+                right: 8,
+            },
+
+        }
     }
 }

styles/src/style_tree/project_diagnostics.ts 🔗

@@ -1,7 +1,9 @@
-import { ColorScheme } from "../theme/color_scheme"
+import { useTheme } from "../theme"
 import { background, text } from "./components"
 
-export default function project_diagnostics(theme: ColorScheme): any {
+export default function project_diagnostics(): any {
+    const theme = useTheme()
+
     return {
         background: background(theme.highest),
         tab_icon_spacing: 4,

styles/src/style_tree/project_panel.ts 🔗

@@ -1,4 +1,3 @@
-import { ColorScheme } from "../theme/color_scheme"
 import { with_opacity } from "../theme/color"
 import {
     Border,
@@ -10,7 +9,10 @@ import {
 } from "./components"
 import { interactive, toggleable } from "../element"
 import merge from "ts-deepmerge"
-export default function project_panel(theme: ColorScheme): any {
+import { useTheme } from "../theme"
+export default function project_panel(): any {
+    const theme = useTheme()
+
     const { is_light } = theme
 
     type EntryStateProps = {
@@ -65,13 +67,12 @@ export default function project_panel(theme: ColorScheme): any {
         const unselected_hovered_style = merge(
             base_properties,
             { background: background(theme.middle, "hovered") },
-            unselected?.hovered ?? {},
+            unselected?.hovered ?? {}
         )
         const unselected_clicked_style = merge(
             base_properties,
-            { background: background(theme.middle, "pressed"), }
-            ,
-            unselected?.clicked ?? {},
+            { background: background(theme.middle, "pressed") },
+            unselected?.clicked ?? {}
         )
         const selected_default_style = merge(
             base_properties,
@@ -79,18 +80,15 @@ export default function project_panel(theme: ColorScheme): any {
                 background: background(theme.lowest),
                 text: text(theme.lowest, "sans", { size: "sm" }),
             },
-            selected_style?.default ?? {},
-
+            selected_style?.default ?? {}
         )
         const selected_hovered_style = merge(
             base_properties,
             {
                 background: background(theme.lowest, "hovered"),
                 text: text(theme.lowest, "sans", { size: "sm" }),
-
             },
-            selected_style?.hovered ?? {},
-
+            selected_style?.hovered ?? {}
         )
         const selected_clicked_style = merge(
             base_properties,
@@ -98,8 +96,7 @@ export default function project_panel(theme: ColorScheme): any {
                 background: background(theme.lowest, "pressed"),
                 text: text(theme.lowest, "sans", { size: "sm" }),
             },
-            selected_style?.clicked ?? {},
-
+            selected_style?.clicked ?? {}
         )
 
         return toggleable({

styles/src/style_tree/project_shared_notification.ts 🔗

@@ -1,9 +1,9 @@
-import { ColorScheme } from "../theme/color_scheme"
+import { useTheme } from "../theme"
 import { background, border, text } from "./components"
 
-export default function project_shared_notification(
-    theme: ColorScheme
-): unknown {
+export default function project_shared_notification(): unknown {
+    const theme = useTheme()
+
     const avatar_size = 48
     return {
         window_height: 74,

styles/src/style_tree/search.ts 🔗

@@ -1,9 +1,11 @@
-import { ColorScheme } from "../theme/color_scheme"
 import { with_opacity } from "../theme/color"
 import { background, border, foreground, text } from "./components"
 import { interactive, toggleable } from "../element"
+import { useTheme } from "../theme"
+
+export default function search(): any {
+    const theme = useTheme()
 
-export default function search(theme: ColorScheme): any {
     // Search input
     const editor = {
         background: background(theme.highest),

styles/src/style_tree/shared_screen.ts 🔗

@@ -1,7 +1,9 @@
-import { ColorScheme } from "../theme/color_scheme"
+import { useTheme } from "../theme"
 import { background } from "./components"
 
-export default function sharedScreen(theme: ColorScheme) {
+export default function sharedScreen() {
+    const theme = useTheme()
+
     return {
         background: background(theme.highest),
     }

styles/src/style_tree/simple_message_notification.ts 🔗

@@ -1,8 +1,10 @@
-import { ColorScheme } from "../theme/color_scheme"
 import { background, border, foreground, text } from "./components"
 import { interactive } from "../element"
+import { useTheme } from "../theme"
+
+export default function simple_message_notification(): any {
+    const theme = useTheme()
 
-export default function simple_message_notification(theme: ColorScheme): any {
     const header_padding = 8
 
     return {

styles/src/style_tree/status_bar.ts 🔗

@@ -1,7 +1,9 @@
-import { ColorScheme } from "../theme/color_scheme"
 import { background, border, foreground, text } from "./components"
 import { interactive, toggleable } from "../element"
-export default function status_bar(theme: ColorScheme): any {
+import { useTheme } from "../common"
+export default function status_bar(): any {
+    const theme = useTheme()
+
     const layer = theme.lowest
 
     const status_container = {

styles/src/style_tree/tab_bar.ts 🔗

@@ -1,9 +1,11 @@
-import { ColorScheme } from "../theme/color_scheme"
 import { with_opacity } from "../theme/color"
 import { text, border, background, foreground } from "./components"
 import { interactive, toggleable } from "../element"
+import { useTheme } from "../common"
+
+export default function tab_bar(): any {
+    const theme = useTheme()
 
-export default function tab_bar(theme: ColorScheme): any {
     const height = 32
 
     const active_layer = theme.highest

styles/src/style_tree/terminal.ts 🔗

@@ -1,6 +1,8 @@
-import { ColorScheme } from "../theme/color_scheme"
+import { useTheme } from "../theme"
+
+export default function terminal() {
+    const theme = useTheme()
 
-export default function terminal(theme: ColorScheme) {
     /**
      * Colors are controlled per-cell in the terminal grid.
      * Cells can be set to any of these more 'theme-capable' colors

styles/src/style_tree/titlebar.ts 🔗

@@ -1,7 +1,7 @@
-import { ColorScheme } from "../common"
 import { icon_button, toggleable_icon_button } from "../component/icon_button"
 import { toggleable_text_button } from "../component/text_button"
 import { interactive, toggleable } from "../element"
+import { useTheme } from "../theme"
 import { with_opacity } from "../theme/color"
 import { background, border, foreground, text } from "./components"
 
@@ -22,7 +22,9 @@ function build_spacing(
     }
 }
 
-function call_controls(theme: ColorScheme) {
+function call_controls() {
+    const theme = useTheme()
+
     const button_height = 18
 
     const space = build_spacing(TITLEBAR_HEIGHT, button_height, ITEM_SPACING)
@@ -69,7 +71,9 @@ function call_controls(theme: ColorScheme) {
  * When logged in shows the user's avatar and a chevron,
  * When logged out only shows a chevron.
  */
-function user_menu(theme: ColorScheme) {
+function user_menu() {
+    const theme = useTheme()
+
     const button_height = 18
 
     const space = build_spacing(TITLEBAR_HEIGHT, button_height, ITEM_SPACING)
@@ -155,7 +159,9 @@ function user_menu(theme: ColorScheme) {
     }
 }
 
-export function titlebar(theme: ColorScheme): any {
+export function titlebar(): any {
+    const theme = useTheme()
+
     const avatar_width = 15
     const avatar_outer_width = avatar_width + 4
     const follower_avatar_width = 14
@@ -173,8 +179,14 @@ export function titlebar(theme: ColorScheme): any {
         },
 
         // Project
-        title: text(theme.lowest, "sans", "variant"),
-        highlight_color: text(theme.lowest, "sans", "active").color,
+        project_name_divider: text(theme.lowest, "sans", "variant"),
+
+        project_menu_button: toggleable_text_button(theme, {
+            color: 'base',
+        }),
+        git_menu_button: toggleable_text_button(theme, {
+            color: 'variant',
+        }),
 
         // Collaborators
         leader_avatar: {
@@ -237,14 +249,14 @@ export function titlebar(theme: ColorScheme): any {
             corner_radius: 6,
         },
 
-        leave_call_button: icon_button(theme, {
+        leave_call_button: icon_button({
             margin: {
                 left: ITEM_SPACING / 2,
                 right: ITEM_SPACING,
             },
         }),
 
-        ...call_controls(theme),
+        ...call_controls(),
 
         toggle_contacts_button: toggleable_icon_button(theme, {
             margin: {
@@ -261,6 +273,6 @@ export function titlebar(theme: ColorScheme): any {
             background: foreground(theme.lowest, "accent"),
         },
         share_button: toggleable_text_button(theme, {}),
-        user_menu: user_menu(theme),
+        user_menu: user_menu(),
     }
 }

styles/src/style_tree/toolbar_dropdown_menu.ts 🔗

@@ -1,7 +1,9 @@
-import { ColorScheme } from "../theme/color_scheme"
 import { background, border, text } from "./components"
 import { interactive, toggleable } from "../element"
-export default function dropdown_menu(theme: ColorScheme): any {
+import { useTheme } from "../theme"
+export default function dropdown_menu(): any {
+    const theme = useTheme()
+
     return {
         row_height: 30,
         background: background(theme.middle),

styles/src/style_tree/tooltip.ts 🔗

@@ -1,7 +1,9 @@
-import { ColorScheme } from "../theme/color_scheme"
+import { useTheme } from "../theme"
 import { background, border, text } from "./components"
 
-export default function tooltip(theme: ColorScheme): any {
+export default function tooltip(): any {
+    const theme = useTheme()
+
     return {
         background: background(theme.middle),
         border: border(theme.middle),

styles/src/style_tree/update_notification.ts 🔗

@@ -1,8 +1,10 @@
-import { ColorScheme } from "../theme/color_scheme"
 import { foreground, text } from "./components"
 import { interactive } from "../element"
+import { useTheme } from "../theme"
+
+export default function update_notification(): any {
+    const theme = useTheme()
 
-export default function update_notification(theme: ColorScheme): any {
     const header_padding = 8
 
     return {

styles/src/style_tree/welcome.ts 🔗

@@ -1,4 +1,3 @@
-import { ColorScheme } from "../theme/color_scheme"
 import { with_opacity } from "../theme/color"
 import {
     border,
@@ -9,8 +8,11 @@ import {
     svg,
 } from "./components"
 import { interactive } from "../element"
+import { useTheme } from "../theme"
+
+export default function welcome(): any {
+    const theme = useTheme()
 
-export default function welcome(theme: ColorScheme): any {
     const checkbox_base = {
         corner_radius: 4,
         padding: {

styles/src/style_tree/workspace.ts 🔗

@@ -1,4 +1,3 @@
-import { ColorScheme } from "../theme/color_scheme"
 import { with_opacity } from "../theme/color"
 import {
     background,
@@ -11,9 +10,12 @@ import {
 import statusBar from "./status_bar"
 import tabBar from "./tab_bar"
 import { interactive } from "../element"
-
 import { titlebar } from "./titlebar"
-export default function workspace(theme: ColorScheme): any {
+import { useTheme } from "../theme"
+
+export default function workspace(): any {
+    const theme = useTheme()
+
     const { is_light } = theme
 
     return {
@@ -85,7 +87,7 @@ export default function workspace(theme: ColorScheme): any {
         },
         leader_border_opacity: 0.7,
         leader_border_width: 2.0,
-        tab_bar: tabBar(theme),
+        tab_bar: tabBar(),
         modal: {
             margin: {
                 bottom: 52,
@@ -123,8 +125,8 @@ export default function workspace(theme: ColorScheme): any {
             color: border_color(theme.lowest),
             width: 1,
         },
-        status_bar: statusBar(theme),
-        titlebar: titlebar(theme),
+        status_bar: statusBar(),
+        titlebar: titlebar(),
         toolbar: {
             height: 34,
             background: background(theme.highest),

styles/src/theme/color_scheme.ts → styles/src/theme/create_theme.ts 🔗

@@ -8,7 +8,7 @@ import {
 } from "./theme_config"
 import { get_ramps } from "./ramps"
 
-export interface ColorScheme {
+export interface Theme {
     name: string
     is_light: boolean
 
@@ -114,7 +114,7 @@ export interface Style {
     foreground: string
 }
 
-export function create_color_scheme(theme: ThemeConfig): ColorScheme {
+export function create_theme(theme: ThemeConfig): Theme {
     const {
         name,
         appearance,

styles/src/theme/index.ts 🔗

@@ -1,4 +1,25 @@
-export * from "./color_scheme"
+import { create } from "zustand"
+import { Theme } from "./create_theme"
+
+type ThemeState = {
+    theme: Theme | undefined
+    setTheme: (theme: Theme) => void
+}
+
+export const useThemeStore = create<ThemeState>((set) => ({
+    theme: undefined,
+    setTheme: (theme) => set(() => ({ theme })),
+}))
+
+export const useTheme = (): Theme => {
+    const { theme } = useThemeStore.getState()
+
+    if (!theme) throw new Error("Tried to use theme before it was loaded")
+
+    return theme
+}
+
+export * from "./create_theme"
 export * from "./ramps"
 export * from "./syntax"
 export * from "./theme_config"

styles/src/theme/ramps.ts 🔗

@@ -1,5 +1,5 @@
 import chroma, { Color, Scale } from "chroma-js"
-import { RampSet } from "./color_scheme"
+import { RampSet } from "./create_theme"
 import {
     ThemeConfigInputColors,
     ThemeConfigInputColorsKeys,

styles/src/theme/syntax.ts 🔗

@@ -1,6 +1,5 @@
 import deepmerge from "deepmerge"
-import { FontWeight, font_weights } from "../common"
-import { ColorScheme } from "./color_scheme"
+import { FontWeight, font_weights, useTheme } from "../common"
 import chroma from "chroma-js"
 
 export interface SyntaxHighlightStyle {
@@ -123,7 +122,9 @@ const default_syntax_highlight_style: Omit<SyntaxHighlightStyle, "color"> = {
     italic: false,
 }
 
-function build_default_syntax(color_scheme: ColorScheme): Syntax {
+function build_default_syntax(): Syntax {
+    const theme = useTheme()
+
     // Make a temporary object that is allowed to be missing
     // the "color" property for each style
     const syntax: {
@@ -141,8 +142,8 @@ function build_default_syntax(color_scheme: ColorScheme): Syntax {
     // predictive color distinct from any other color in the theme
     const predictive = chroma
         .mix(
-            color_scheme.ramps.neutral(0.4).hex(),
-            color_scheme.ramps.blue(0.4).hex(),
+            theme.ramps.neutral(0.4).hex(),
+            theme.ramps.blue(0.4).hex(),
             0.45,
             "lch"
         )
@@ -151,32 +152,32 @@ function build_default_syntax(color_scheme: ColorScheme): Syntax {
     // hint color distinct from any other color in the theme
     const hint = chroma
         .mix(
-            color_scheme.ramps.neutral(0.6).hex(),
-            color_scheme.ramps.blue(0.4).hex(),
+            theme.ramps.neutral(0.6).hex(),
+            theme.ramps.blue(0.4).hex(),
             0.45,
             "lch"
         )
         .hex()
 
     const color = {
-        primary: color_scheme.ramps.neutral(1).hex(),
-        comment: color_scheme.ramps.neutral(0.71).hex(),
-        punctuation: color_scheme.ramps.neutral(0.86).hex(),
+        primary: theme.ramps.neutral(1).hex(),
+        comment: theme.ramps.neutral(0.71).hex(),
+        punctuation: theme.ramps.neutral(0.86).hex(),
         predictive: predictive,
         hint: hint,
-        emphasis: color_scheme.ramps.blue(0.5).hex(),
-        string: color_scheme.ramps.orange(0.5).hex(),
-        function: color_scheme.ramps.yellow(0.5).hex(),
-        type: color_scheme.ramps.cyan(0.5).hex(),
-        constructor: color_scheme.ramps.blue(0.5).hex(),
-        variant: color_scheme.ramps.blue(0.5).hex(),
-        property: color_scheme.ramps.blue(0.5).hex(),
-        enum: color_scheme.ramps.orange(0.5).hex(),
-        operator: color_scheme.ramps.orange(0.5).hex(),
-        number: color_scheme.ramps.green(0.5).hex(),
-        boolean: color_scheme.ramps.green(0.5).hex(),
-        constant: color_scheme.ramps.green(0.5).hex(),
-        keyword: color_scheme.ramps.blue(0.5).hex(),
+        emphasis: theme.ramps.blue(0.5).hex(),
+        string: theme.ramps.orange(0.5).hex(),
+        function: theme.ramps.yellow(0.5).hex(),
+        type: theme.ramps.cyan(0.5).hex(),
+        constructor: theme.ramps.blue(0.5).hex(),
+        variant: theme.ramps.blue(0.5).hex(),
+        property: theme.ramps.blue(0.5).hex(),
+        enum: theme.ramps.orange(0.5).hex(),
+        operator: theme.ramps.orange(0.5).hex(),
+        number: theme.ramps.green(0.5).hex(),
+        boolean: theme.ramps.green(0.5).hex(),
+        constant: theme.ramps.green(0.5).hex(),
+        keyword: theme.ramps.blue(0.5).hex(),
     }
 
     // Then assign colors and use Syntax to enforce each style getting it's own color
@@ -211,11 +212,11 @@ function build_default_syntax(color_scheme: ColorScheme): Syntax {
             weight: font_weights.bold,
         },
         link_uri: {
-            color: color_scheme.ramps.green(0.5).hex(),
+            color: theme.ramps.green(0.5).hex(),
             underline: true,
         },
         link_text: {
-            color: color_scheme.ramps.orange(0.5).hex(),
+            color: theme.ramps.orange(0.5).hex(),
             italic: true,
         },
         "text.literal": {
@@ -231,7 +232,7 @@ function build_default_syntax(color_scheme: ColorScheme): Syntax {
             color: color.punctuation,
         },
         "punctuation.special": {
-            color: color_scheme.ramps.neutral(0.86).hex(),
+            color: theme.ramps.neutral(0.86).hex(),
         },
         "punctuation.list_marker": {
             color: color.punctuation,
@@ -252,10 +253,10 @@ function build_default_syntax(color_scheme: ColorScheme): Syntax {
             color: color.string,
         },
         constructor: {
-            color: color_scheme.ramps.blue(0.5).hex(),
+            color: theme.ramps.blue(0.5).hex(),
         },
         variant: {
-            color: color_scheme.ramps.blue(0.5).hex(),
+            color: theme.ramps.blue(0.5).hex(),
         },
         type: {
             color: color.type,
@@ -264,16 +265,16 @@ function build_default_syntax(color_scheme: ColorScheme): Syntax {
             color: color.primary,
         },
         label: {
-            color: color_scheme.ramps.blue(0.5).hex(),
+            color: theme.ramps.blue(0.5).hex(),
         },
         tag: {
-            color: color_scheme.ramps.blue(0.5).hex(),
+            color: theme.ramps.blue(0.5).hex(),
         },
         attribute: {
-            color: color_scheme.ramps.blue(0.5).hex(),
+            color: theme.ramps.blue(0.5).hex(),
         },
         property: {
-            color: color_scheme.ramps.blue(0.5).hex(),
+            color: theme.ramps.blue(0.5).hex(),
         },
         constant: {
             color: color.constant,
@@ -307,17 +308,18 @@ function build_default_syntax(color_scheme: ColorScheme): Syntax {
     return default_syntax
 }
 
-function merge_syntax(
-    default_syntax: Syntax,
-    color_scheme: ColorScheme
-): Syntax {
-    if (!color_scheme.syntax) {
+export function build_syntax(): Syntax {
+    const theme = useTheme()
+
+    const default_syntax: Syntax = build_default_syntax()
+
+    if (!theme.syntax) {
         return default_syntax
     }
 
-    return deepmerge<Syntax, Partial<ThemeSyntax>>(
+    const syntax = deepmerge<Syntax, Partial<ThemeSyntax>>(
         default_syntax,
-        color_scheme.syntax,
+        theme.syntax,
         {
             arrayMerge: (destinationArray, sourceArray) => [
                 ...destinationArray,
@@ -325,12 +327,6 @@ function merge_syntax(
             ],
         }
     )
-}
-
-export function build_syntax(color_scheme: ColorScheme): Syntax {
-    const default_syntax: Syntax = build_default_syntax(color_scheme)
-
-    const syntax = merge_syntax(default_syntax, color_scheme)
 
     return syntax
 }

styles/src/theme/theme_config.ts 🔗

@@ -66,35 +66,10 @@ type ThemeConfigProperties = ThemeMeta & {
     override: ThemeConfigOverrides
 }
 
-// This should be the format a theme is defined as
 export type ThemeConfig = {
     [K in keyof ThemeConfigProperties]: ThemeConfigProperties[K]
 }
 
-interface ThemeColors {
-    neutral: string[]
-    red: string[]
-    orange: string[]
-    yellow: string[]
-    green: string[]
-    cyan: string[]
-    blue: string[]
-    violet: string[]
-    magenta: string[]
-}
-
-type ThemeSyntax = Required<Syntax>
-
-export type ThemeProperties = ThemeMeta & {
-    color: ThemeColors
-    syntax: ThemeSyntax
-}
-
-// This should be a theme after all its properties have been resolved
-export type Theme = {
-    [K in keyof ThemeProperties]: ThemeProperties[K]
-}
-
 export enum ThemeAppearance {
     Light = "light",
     Dark = "dark",
@@ -104,45 +79,3 @@ export enum ThemeLicenseType {
     MIT = "MIT",
     Apache2 = "Apache License 2.0",
 }
-
-export type ThemeFamilyItem =
-    | ThemeConfig
-    | { light: ThemeConfig; dark: ThemeConfig }
-
-type ThemeFamilyProperties = Partial<Omit<ThemeMeta, "name" | "appearance">> & {
-    name: string
-    default: ThemeFamilyItem
-    variants: {
-        [key: string]: ThemeFamilyItem
-    }
-}
-
-// Idea: A theme family is a collection of themes that share the same name
-// For example, a theme family could be `One Dark` and have a `light` and `dark` variant
-// The Ayu family could have `light`, `mirage`, and `dark` variants
-
-type ThemeFamily = {
-    [K in keyof ThemeFamilyProperties]: ThemeFamilyProperties[K]
-}
-
-/** The collection of all themes
- *
- * Example:
- * ```ts
- * {
- *   one_dark,
- *   one_light,
- *     ayu: {
- *     name: 'Ayu',
- *     default: 'ayu_mirage',
- *     variants: {
- *       light: 'ayu_light',
- *       mirage: 'ayu_mirage',
- *       dark: 'ayu_dark',
- *     },
- *   },
- *  ...
- * }
- * ```
- */
-export type ThemeIndex = Record<string, ThemeFamily | ThemeConfig>

styles/src/theme/tokens/layer.ts 🔗

@@ -1,5 +1,5 @@
 import { SingleColorToken } from "@tokens-studio/types"
-import { Layer, Style, StyleSet } from "../color_scheme"
+import { Layer, Style, StyleSet } from "../create_theme"
 import { color_token } from "./token"
 
 interface StyleToken {

styles/src/theme/tokens/players.ts 🔗

@@ -1,12 +1,14 @@
 import { SingleColorToken } from "@tokens-studio/types"
 import { color_token } from "./token"
-import { ColorScheme, Players } from "../color_scheme"
+import { Players } from "../create_theme"
+import { useTheme } from "../../../src/common"
 
 export type PlayerToken = Record<"selection" | "cursor", SingleColorToken>
 
 export type PlayersToken = Record<keyof Players, PlayerToken>
 
-function build_player_token(theme: ColorScheme, index: number): PlayerToken {
+function build_player_token(index: number): PlayerToken {
+    const theme = useTheme()
     const player_number = index.toString() as keyof Players
 
     return {
@@ -21,13 +23,15 @@ function build_player_token(theme: ColorScheme, index: number): PlayerToken {
     }
 }
 
-export const players_token = (theme: ColorScheme): PlayersToken => ({
-    "0": build_player_token(theme, 0),
-    "1": build_player_token(theme, 1),
-    "2": build_player_token(theme, 2),
-    "3": build_player_token(theme, 3),
-    "4": build_player_token(theme, 4),
-    "5": build_player_token(theme, 5),
-    "6": build_player_token(theme, 6),
-    "7": build_player_token(theme, 7),
-})
+export const players_token = (): PlayersToken => {
+    return {
+        "0": build_player_token(0),
+        "1": build_player_token(1),
+        "2": build_player_token(2),
+        "3": build_player_token(3),
+        "4": build_player_token(4),
+        "5": build_player_token(5),
+        "6": build_player_token(6),
+        "7": build_player_token(7),
+    }
+}

styles/src/theme/tokens/color_scheme.ts → styles/src/theme/tokens/theme.ts 🔗

@@ -5,18 +5,18 @@ import {
     TokenTypes,
 } from "@tokens-studio/types"
 import {
-    ColorScheme,
     Shadow,
     SyntaxHighlightStyle,
     ThemeSyntax,
-} from "../color_scheme"
+} from "../create_theme"
 import { LayerToken, layer_token } from "./layer"
 import { PlayersToken, players_token } from "./players"
 import { color_token } from "./token"
 import { Syntax } from "../syntax"
 import editor from "../../style_tree/editor"
+import { useTheme } from "../../../src/common"
 
-interface ColorSchemeTokens {
+interface ThemeTokens {
     name: SingleOtherToken
     appearance: SingleOtherToken
     lowest: LayerToken
@@ -39,12 +39,14 @@ const create_shadow_token = (
     }
 }
 
-const popover_shadow_token = (theme: ColorScheme): SingleBoxShadowToken => {
+const popover_shadow_token = (): SingleBoxShadowToken => {
+    const theme = useTheme()
     const shadow = theme.popover_shadow
     return create_shadow_token(shadow, "popover_shadow")
 }
 
-const modal_shadow_token = (theme: ColorScheme): SingleBoxShadowToken => {
+const modal_shadow_token = (): SingleBoxShadowToken => {
+    const theme = useTheme()
     const shadow = theme.modal_shadow
     return create_shadow_token(shadow, "modal_shadow")
 }
@@ -68,13 +70,15 @@ function syntax_highlight_style_color_tokens(
     }, {} as ThemeSyntaxColorTokens)
 }
 
-const syntax_tokens = (theme: ColorScheme): ColorSchemeTokens["syntax"] => {
-    const syntax = editor(theme).syntax
+const syntax_tokens = (): ThemeTokens["syntax"] => {
+    const syntax = editor().syntax
 
     return syntax_highlight_style_color_tokens(syntax)
 }
 
-export function theme_tokens(theme: ColorScheme): ColorSchemeTokens {
+export function theme_tokens(): ThemeTokens {
+    const theme = useTheme()
+
     return {
         name: {
             name: "themeName",
@@ -89,9 +93,9 @@ export function theme_tokens(theme: ColorScheme): ColorSchemeTokens {
         lowest: layer_token(theme.lowest, "lowest"),
         middle: layer_token(theme.middle, "middle"),
         highest: layer_token(theme.highest, "highest"),
-        popover_shadow: popover_shadow_token(theme),
-        modal_shadow: modal_shadow_token(theme),
-        players: players_token(theme),
-        syntax: syntax_tokens(theme),
+        popover_shadow: popover_shadow_token(),
+        modal_shadow: modal_shadow_token(),
+        players: players_token(),
+        syntax: syntax_tokens(),
     }
 }

styles/tsconfig.json 🔗

@@ -22,17 +22,9 @@
         "strictPropertyInitialization": false,
         "skipLibCheck": true,
         "useUnknownInCatchVariables": false,
-        "baseUrl": ".",
-        "paths": {
-            "@/*": ["./*"],
-            "@element/*": ["./src/element/*"],
-            "@component/*": ["./src/component/*"],
-            "@styleTree/*": ["./src/styleTree/*"],
-            "@theme/*": ["./src/theme/*"],
-            "@types/*": ["./src/util/*"],
-            "@themes/*": ["./src/themes/*"],
-            "@util/*": ["./src/util/*"]
-        }
+        "baseUrl": "."
     },
-    "exclude": ["node_modules"]
+    "exclude": [
+        "node_modules"
+    ]
 }