Merge branch 'main' into allow-following-outside-of-projects

Max Brunsfeld created

Change summary

Cargo.lock                                                | 284 ++-
Cargo.toml                                                |   2 
README.md                                                 |   4 
assets/keymaps/default.json                               |  39 
assets/keymaps/vim.json                                   |  64 
assets/settings/default.json                              |  21 
crates/ai/Cargo.toml                                      |  37 
crates/ai/src/ai.rs                                       | 296 ----
crates/ai/src/completion.rs                               | 212 +++
crates/ai/src/embedding.rs                                |  67 
crates/assistant/Cargo.toml                               |  48 
crates/assistant/README.zmd                               |   0 
crates/assistant/features.zmd                             |   0 
crates/assistant/src/assistant.rs                         | 112 +
crates/assistant/src/assistant_panel.rs                   |   9 
crates/assistant/src/assistant_settings.rs                |   0 
crates/assistant/src/codegen.rs                           |  59 
crates/assistant/src/streaming_diff.rs                    |   0 
crates/call/src/call.rs                                   |  80 
crates/collab_ui/src/channel_view.rs                      |  11 
crates/collab_ui/src/collab_panel.rs                      |  23 
crates/collab_ui/src/collab_ui.rs                         |  32 
crates/command_palette/src/command_palette.rs             |  47 
crates/editor/src/editor.rs                               |  72 +
crates/editor/src/editor_tests.rs                         |  30 
crates/editor/src/inlay_hint_cache.rs                     |   2 
crates/editor/src/items.rs                                |   4 
crates/editor/src/scroll/scroll_amount.rs                 |  10 
crates/editor/src/test/editor_test_context.rs             |  15 
crates/file_finder/Cargo.toml                             |   1 
crates/file_finder/src/file_finder.rs                     | 573 +++++++-
crates/fs/src/fs.rs                                       |   2 
crates/fuzzy/src/fuzzy.rs                                 |   4 
crates/fuzzy/src/paths.rs                                 |  38 
crates/gpui/src/app/window.rs                             |  14 
crates/gpui/src/font_cache.rs                             |   7 
crates/gpui2/src/style.rs                                 | 147 -
crates/gpui2_macros/src/styleable_helpers.rs              |   4 
crates/language_tools/src/lsp_log.rs                      |  57 
crates/lsp/src/lsp.rs                                     |   8 
crates/picker/src/picker.rs                               |  37 
crates/project/src/project.rs                             |  59 
crates/project_symbols/src/project_symbols.rs             |   2 
crates/quick_action_bar/Cargo.toml                        |   2 
crates/quick_action_bar/src/quick_action_bar.rs           |  54 
crates/search/src/buffer_search.rs                        |  37 
crates/search/src/project_search.rs                       |  61 
crates/semantic_index/Cargo.toml                          |   6 
crates/semantic_index/examples/eval.rs                    |   4 
crates/semantic_index/src/db.rs                           | 132 +
crates/semantic_index/src/embedding_queue.rs              |   3 
crates/semantic_index/src/parsing.rs                      |   2 
crates/semantic_index/src/semantic_index.rs               |  37 
crates/semantic_index/src/semantic_index_tests.rs         |   5 
crates/storybook/Cargo.toml                               |   5 
crates/storybook/src/collab_panel.rs                      |  16 
crates/storybook/src/stories.rs                           |   2 
crates/storybook/src/stories/components.rs                |   4 
crates/storybook/src/stories/components/breadcrumb.rs     |  16 
crates/storybook/src/stories/components/facepile.rs       |  50 
crates/storybook/src/stories/components/toolbar.rs        |  16 
crates/storybook/src/stories/components/traffic_lights.rs |  18 
crates/storybook/src/stories/elements.rs                  |   1 
crates/storybook/src/stories/elements/avatar.rs           |  26 
crates/storybook/src/story.rs                             |  38 
crates/storybook/src/story_selector.rs                    |  76 +
crates/storybook/src/storybook.rs                         |  69 
crates/storybook/src/ui.rs                                |  23 
crates/storybook/src/ui/component.rs                      |   4 
crates/storybook/src/ui/element.rs                        |   9 
crates/storybook/src/ui/element/icon.rs                   |  73 -
crates/storybook/src/ui/module.rs                         |   5 
crates/storybook/src/ui/module/project_panel.rs           |  97 -
crates/storybook/src/workspace.rs                         |  14 
crates/terminal_view/src/terminal_view.rs                 |   7 
crates/ui/Cargo.toml                                      |  12 
crates/ui/doc/elevation.md                                |  57 
crates/ui/src/components.rs                               |  55 
crates/ui/src/components/breadcrumb.rs                    |  36 
crates/ui/src/components/chat_panel.rs                    |   9 
crates/ui/src/components/collab_panel.rs                  | 177 ++
crates/ui/src/components/command_palette.rs               |  31 
crates/ui/src/components/facepile.rs                      |  13 
crates/ui/src/components/follow_group.rs                  |  10 
crates/ui/src/components/icon_button.rs                   |  50 
crates/ui/src/components/list.rs                          |  64 +
crates/ui/src/components/list_item.rs                     |  66 
crates/ui/src/components/list_section_header.rs           |  88 +
crates/ui/src/components/palette.rs                       | 124 +
crates/ui/src/components/palette_item.rs                  |  63 
crates/ui/src/components/project_panel.rs                 |  62 
crates/ui/src/components/status_bar.rs                    |  25 
crates/ui/src/components/tab.rs                           |   7 
crates/ui/src/components/tab_bar.rs                       |  16 
crates/ui/src/components/title_bar.rs                     |  56 
crates/ui/src/components/toolbar.rs                       |  35 
crates/ui/src/components/traffic_lights.rs                |  30 
crates/ui/src/components/workspace.rs                     |  80 +
crates/ui/src/element_ext.rs                              |   6 
crates/ui/src/elements.rs                                 |  17 
crates/ui/src/elements/avatar.rs                          |   8 
crates/ui/src/elements/details.rs                         |   6 
crates/ui/src/elements/icon.rs                            | 155 ++
crates/ui/src/elements/indicator.rs                       |   7 
crates/ui/src/elements/input.rs                           |   9 
crates/ui/src/elements/label.rs                           |  28 
crates/ui/src/elements/text_button.rs                     |   9 
crates/ui/src/elements/tool_divider.rs                    |   7 
crates/ui/src/lib.rs                                      |  17 
crates/ui/src/prelude.rs                                  |  15 
crates/ui/src/static_data.rs                              | 166 ++
crates/ui/src/theme.rs                                    |  18 
crates/ui/src/tokens.rs                                   |  18 
crates/ui/tracker.md                                      |   0 
crates/util/src/util.rs                                   |  14 
crates/vim/Cargo.toml                                     |   2 
crates/vim/src/command.rs                                 | 438 ++++++
crates/vim/src/normal.rs                                  |   9 
crates/vim/src/normal/scroll.rs                           |  88 +
crates/vim/src/normal/search.rs                           | 198 +++
crates/vim/src/test.rs                                    |   3 
crates/vim/src/test/neovim_backed_binding_test_context.rs |  17 
crates/vim/src/test/neovim_backed_test_context.rs         |  37 
crates/vim/src/test/neovim_connection.rs                  |   8 
crates/vim/src/vim.rs                                     |   9 
crates/vim/src/visual.rs                                  |  50 
crates/vim/test_data/test_command_basics.json             |   6 
crates/vim/test_data/test_command_goto.json               |   5 
crates/vim/test_data/test_command_replace.json            |  29 
crates/vim/test_data/test_command_search.json             |  11 
crates/vim/test_data/test_ctrl_d_u.json                   |  22 
crates/workspace/src/item.rs                              |   6 
crates/workspace/src/pane.rs                              | 355 +++--
crates/workspace/src/pane_group.rs                        |  22 
crates/workspace/src/workspace.rs                         | 252 ++-
crates/zed/Cargo.toml                                     |   6 
crates/zed/src/languages.rs                               |  39 
crates/zed/src/languages/css/config.toml                  |   1 
crates/zed/src/languages/elixir_next.rs                   | 266 ++++
crates/zed/src/main.rs                                    |   4 
crates/zed/src/menus.rs                                   |   8 
crates/zed/src/zed.rs                                     |  54 
docs/tools.md                                             |   4 
docs/ui/states.md                                         |  43 
script/evaluate_semantic_index                            |   2 
script/reset_db                                           |   2 
styles/src/style_tree/update_notification.ts              |   8 
147 files changed, 5,386 insertions(+), 1,729 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -79,9 +79,9 @@ dependencies = [
 
 [[package]]
 name = "aho-corasick"
-version = "1.1.0"
+version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f2135563fb5c609d2b2b87c1e8ce7bc41b0b45430fa9661f457981503dd5bf0"
+checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab"
 dependencies = [
  "memchr",
 ]
@@ -91,36 +91,25 @@ name = "ai"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "chrono",
- "client",
- "collections",
- "ctor",
- "editor",
- "env_logger 0.9.3",
- "fs",
+ "async-trait",
+ "bincode",
  "futures 0.3.28",
  "gpui",
- "indoc",
  "isahc",
- "language",
+ "lazy_static",
  "log",
- "menu",
+ "matrixmultiply",
  "ordered-float",
  "parking_lot 0.11.2",
- "project",
+ "parse_duration",
+ "postage",
  "rand 0.8.5",
  "regex",
- "schemars",
- "search",
+ "rusqlite",
  "serde",
  "serde_json",
- "settings",
- "smol",
- "theme",
- "tiktoken-rs 0.4.5",
+ "tiktoken-rs 0.5.4",
  "util",
- "uuid 1.4.1",
- "workspace",
 ]
 
 [[package]]
@@ -305,6 +294,44 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
 
+[[package]]
+name = "assistant"
+version = "0.1.0"
+dependencies = [
+ "ai",
+ "anyhow",
+ "chrono",
+ "client",
+ "collections",
+ "ctor",
+ "editor",
+ "env_logger 0.9.3",
+ "fs",
+ "futures 0.3.28",
+ "gpui",
+ "indoc",
+ "isahc",
+ "language",
+ "log",
+ "menu",
+ "ordered-float",
+ "parking_lot 0.11.2",
+ "project",
+ "rand 0.8.5",
+ "regex",
+ "schemars",
+ "search",
+ "serde",
+ "serde_json",
+ "settings",
+ "smol",
+ "theme",
+ "tiktoken-rs 0.4.5",
+ "util",
+ "uuid 1.4.1",
+ "workspace",
+]
+
 [[package]]
 name = "async-broadcast"
 version = "0.4.1"
@@ -2144,7 +2171,7 @@ dependencies = [
  "convert_case 0.4.0",
  "proc-macro2",
  "quote",
- "rustc_version 0.4.0",
+ "rustc_version",
  "syn 1.0.109",
 ]
 
@@ -2586,6 +2613,7 @@ dependencies = [
 name = "file_finder"
 version = "0.1.0"
 dependencies = [
+ "collections",
  "ctor",
  "editor",
  "env_logger 0.9.3",
@@ -3237,7 +3265,7 @@ dependencies = [
  "indexmap 1.9.3",
  "slab",
  "tokio",
- "tokio-util 0.7.8",
+ "tokio-util 0.7.9",
  "tracing",
 ]
 
@@ -3358,9 +3386,9 @@ dependencies = [
 
 [[package]]
 name = "hermit-abi"
-version = "0.3.2"
+version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
+checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
 
 [[package]]
 name = "hex"
@@ -3654,7 +3682,7 @@ version = "1.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
 dependencies = [
- "hermit-abi 0.3.2",
+ "hermit-abi 0.3.3",
  "libc",
  "windows-sys",
 ]
@@ -3711,8 +3739,8 @@ version = "0.4.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
 dependencies = [
- "hermit-abi 0.3.2",
- "rustix 0.38.13",
+ "hermit-abi 0.3.3",
+ "rustix 0.38.14",
  "windows-sys",
 ]
 
@@ -4280,9 +4308,9 @@ checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb"
 
 [[package]]
 name = "matrixmultiply"
-version = "0.3.7"
+version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "090126dc04f95dc0d1c1c91f61bdd474b3930ca064c1edc8a849da2c6cbe1e77"
+checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2"
 dependencies = [
  "autocfg",
  "rawpointer",
@@ -4556,6 +4584,19 @@ dependencies = [
  "tempfile",
 ]
 
+[[package]]
+name = "ndarray"
+version = "0.15.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32"
+dependencies = [
+ "matrixmultiply",
+ "num-complex 0.4.4",
+ "num-integer",
+ "num-traits",
+ "rawpointer",
+]
+
 [[package]]
 name = "ndk"
 version = "0.7.0"
@@ -4682,7 +4723,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36"
 dependencies = [
  "num-bigint 0.2.6",
- "num-complex",
+ "num-complex 0.2.4",
  "num-integer",
  "num-iter",
  "num-rational 0.2.4",
@@ -4738,6 +4779,15 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "num-complex"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214"
+dependencies = [
+ "num-traits",
+]
+
 [[package]]
 name = "num-derive"
 version = "0.3.3"
@@ -4809,7 +4859,7 @@ version = "1.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
 dependencies = [
- "hermit-abi 0.3.2",
+ "hermit-abi 0.3.3",
  "libc",
 ]
 
@@ -4846,7 +4896,7 @@ dependencies = [
  "rmp",
  "rmpv",
  "tokio",
- "tokio-util 0.7.8",
+ "tokio-util 0.7.9",
 ]
 
 [[package]]
@@ -5147,11 +5197,11 @@ dependencies = [
 
 [[package]]
 name = "pathfinder_simd"
-version = "0.5.1"
+version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39fe46acc5503595e5949c17b818714d26fdf9b4920eacf3b2947f0199f4a6ff"
+checksum = "0444332826c70dc47be74a7c6a5fc44e23a7905ad6858d4162b658320455ef93"
 dependencies = [
- "rustc_version 0.3.3",
+ "rustc_version",
 ]
 
 [[package]]
@@ -5186,17 +5236,6 @@ version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
 
-[[package]]
-name = "pest"
-version = "2.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7a4d085fd991ac8d5b05a147b437791b4260b76326baf0fc60cf7c9c27ecd33"
-dependencies = [
- "memchr",
- "thiserror",
- "ucd-trie",
-]
-
 [[package]]
 name = "petgraph"
 version = "0.6.4"
@@ -5732,7 +5771,7 @@ dependencies = [
 name = "quick_action_bar"
 version = "0.1.0"
 dependencies = [
- "ai",
+ "assistant",
  "editor",
  "gpui",
  "search",
@@ -5868,9 +5907,9 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
 
 [[package]]
 name = "rayon"
-version = "1.7.0"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b"
+checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1"
 dependencies = [
  "either",
  "rayon-core",
@@ -5878,14 +5917,12 @@ dependencies = [
 
 [[package]]
 name = "rayon-core"
-version = "1.11.0"
+version = "1.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
+checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed"
 dependencies = [
- "crossbeam-channel",
  "crossbeam-deque",
  "crossbeam-utils",
- "num_cpus",
 ]
 
 [[package]]
@@ -6335,22 +6372,13 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
 
-[[package]]
-name = "rustc_version"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
-dependencies = [
- "semver 0.11.0",
-]
-
 [[package]]
 name = "rustc_version"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
 dependencies = [
- "semver 1.0.18",
+ "semver",
 ]
 
 [[package]]
@@ -6385,9 +6413,9 @@ dependencies = [
 
 [[package]]
 name = "rustix"
-version = "0.38.13"
+version = "0.38.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662"
+checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f"
 dependencies = [
  "bitflags 2.4.0",
  "errno 0.3.3",
@@ -6736,9 +6764,9 @@ dependencies = [
 name = "semantic_index"
 version = "0.1.0"
 dependencies = [
+ "ai",
  "anyhow",
  "async-trait",
- "bincode",
  "client",
  "collections",
  "ctor",
@@ -6747,15 +6775,13 @@ dependencies = [
  "futures 0.3.28",
  "globset",
  "gpui",
- "isahc",
  "language",
  "lazy_static",
  "log",
- "matrixmultiply",
+ "ndarray",
  "node_runtime",
  "ordered-float",
  "parking_lot 0.11.2",
- "parse_duration",
  "picker",
  "postage",
  "pretty_assertions",
@@ -6789,30 +6815,12 @@ dependencies = [
  "zed",
 ]
 
-[[package]]
-name = "semver"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
-dependencies = [
- "semver-parser",
-]
-
 [[package]]
 name = "semver"
 version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
 
-[[package]]
-name = "semver-parser"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
-dependencies = [
- "pest",
-]
-
 [[package]]
 name = "seq-macro"
 version = "0.2.2"
@@ -6982,9 +6990,9 @@ dependencies = [
 
 [[package]]
 name = "sha1"
-version = "0.10.5"
+version = "0.10.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
 dependencies = [
  "cfg-if 1.0.0",
  "cpufeatures",
@@ -7157,9 +7165,9 @@ dependencies = [
 
 [[package]]
 name = "smallvec"
-version = "1.11.0"
+version = "1.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
+checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a"
 
 [[package]]
 name = "smol"
@@ -7394,13 +7402,16 @@ name = "storybook"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "clap 4.4.4",
  "gpui2",
  "log",
  "rust-embed",
  "serde",
  "settings",
  "simplelog",
+ "strum",
  "theme",
+ "ui",
  "util",
 ]
 
@@ -7421,6 +7432,28 @@ version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
 
+[[package]]
+name = "strum"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
+dependencies = [
+ "strum_macros",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn 2.0.37",
+]
+
 [[package]]
 name = "subtle"
 version = "2.4.1"
@@ -7440,15 +7473,15 @@ dependencies = [
 
 [[package]]
 name = "sval"
-version = "2.6.1"
+version = "2.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b031320a434d3e9477ccf9b5756d57d4272937b8d22cb88af80b7633a1b78b1"
+checksum = "05d11eec9fbe2bc8bc71e7349f0e7534db9a96d961fb9f302574275b7880ad06"
 
 [[package]]
 name = "sval_buffer"
-version = "2.6.1"
+version = "2.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6bf7e9412af26b342f3f2cc5cc4122b0105e9d16eb76046cd14ed10106cf6028"
+checksum = "6b7451f69a93c5baf2653d5aa8bb4178934337f16c22830a50b06b386f72d761"
 dependencies = [
  "sval",
  "sval_ref",
@@ -7456,18 +7489,18 @@ dependencies = [
 
 [[package]]
 name = "sval_dynamic"
-version = "2.6.1"
+version = "2.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0ef628e8a77a46ed3338db8d1b08af77495123cc229453084e47cd716d403cf"
+checksum = "c34f5a2cc12b4da2adfb59d5eedfd9b174a23cc3fae84cec71dcbcd9302068f5"
 dependencies = [
  "sval",
 ]
 
 [[package]]
 name = "sval_fmt"
-version = "2.6.1"
+version = "2.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7dc09e9364c2045ab5fa38f7b04d077b3359d30c4c2b3ec4bae67a358bd64326"
+checksum = "2f578b2301341e246d00b35957f2952c4ec554ad9c7cfaee10bc86bc92896578"
 dependencies = [
  "itoa",
  "ryu",
@@ -7476,9 +7509,9 @@ dependencies = [
 
 [[package]]
 name = "sval_json"
-version = "2.6.1"
+version = "2.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ada6f627e38cbb8860283649509d87bc4a5771141daa41c78fd31f2b9485888d"
+checksum = "8346c00f5dc6efe18bea8d13c1f7ca4f112b20803434bf3657ac17c0f74cbc4b"
 dependencies = [
  "itoa",
  "ryu",
@@ -7487,18 +7520,18 @@ dependencies = [
 
 [[package]]
 name = "sval_ref"
-version = "2.6.1"
+version = "2.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "703ca1942a984bd0d9b5a4c0a65ab8b4b794038d080af4eb303c71bc6bf22d7c"
+checksum = "6617cc89952f792aebc0f4a1a76bc51e80c70b18c491bd52215c7989c4c3dd06"
 dependencies = [
  "sval",
 ]
 
 [[package]]
 name = "sval_serde"
-version = "2.6.1"
+version = "2.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "830926cd0581f7c3e5d51efae4d35c6b6fc4db583842652891ba2f1bed8db046"
+checksum = "fe3d1e59f023341d9af75d86f3bc148a6704f3f831eef0dd90bbe9cb445fa024"
 dependencies = [
  "serde",
  "sval",
@@ -7649,7 +7682,7 @@ dependencies = [
  "cfg-if 1.0.0",
  "fastrand 2.0.0",
  "redox_syscall 0.3.5",
- "rustix 0.38.13",
+ "rustix 0.38.14",
  "windows-sys",
 ]
 
@@ -8051,9 +8084,9 @@ dependencies = [
 
 [[package]]
 name = "tokio-util"
-version = "0.7.8"
+version = "0.7.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d"
+checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d"
 dependencies = [
  "bytes 1.5.0",
  "futures-core",
@@ -8152,7 +8185,7 @@ dependencies = [
  "rand 0.8.5",
  "slab",
  "tokio",
- "tokio-util 0.7.8",
+ "tokio-util 0.7.9",
  "tower-layer",
  "tower-service",
  "tracing",
@@ -8598,10 +8631,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
 
 [[package]]
-name = "ucd-trie"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
+name = "ui"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "gpui2",
+ "serde",
+ "settings",
+ "theme",
+]
 
 [[package]]
 name = "unicase"
@@ -8671,9 +8709,9 @@ checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
 
 [[package]]
 name = "unicode-width"
-version = "0.1.10"
+version = "0.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
 
 [[package]]
 name = "unicode_categories"
@@ -8864,6 +8902,7 @@ dependencies = [
  "async-trait",
  "collections",
  "command_palette",
+ "diagnostics",
  "editor",
  "futures 0.3.28",
  "gpui",
@@ -8885,6 +8924,7 @@ dependencies = [
  "tokio",
  "util",
  "workspace",
+ "zed-actions",
 ]
 
 [[package]]
@@ -9386,7 +9426,7 @@ dependencies = [
  "either",
  "home",
  "once_cell",
- "rustix 0.38.13",
+ "rustix 0.38.14",
 ]
 
 [[package]]
@@ -9471,9 +9511,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
 
 [[package]]
 name = "winapi-util"
-version = "0.1.5"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
 dependencies = [
  "winapi 0.3.9",
 ]
@@ -9796,11 +9836,11 @@ dependencies = [
 
 [[package]]
 name = "zed"
-version = "0.106.0"
+version = "0.107.0"
 dependencies = [
  "activity_indicator",
- "ai",
  "anyhow",
+ "assistant",
  "async-compression",
  "async-recursion 0.3.2",
  "async-tar",
@@ -9865,12 +9905,14 @@ dependencies = [
  "rpc",
  "rsa",
  "rust-embed",
+ "schemars",
  "search",
  "semantic_index",
  "serde",
  "serde_derive",
  "serde_json",
  "settings",
+ "shellexpand",
  "simplelog",
  "smallvec",
  "smol",

Cargo.toml πŸ”—

@@ -2,6 +2,7 @@
 members = [
     "crates/activity_indicator",
     "crates/ai",
+    "crates/assistant",
     "crates/audio",
     "crates/auto_update",
     "crates/breadcrumbs",
@@ -69,6 +70,7 @@ members = [
     "crates/text",
     "crates/theme",
     "crates/theme_selector",
+    "crates/ui",
     "crates/util",
     "crates/semantic_index",
     "crates/vim",

README.md πŸ”—

@@ -13,7 +13,7 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
   sudo xcodebuild -license
   ```
 
-* Install homebrew, node and rustup-init (rutup, rust, cargo, etc.)
+* Install homebrew, node and rustup-init (rustup, rust, cargo, etc.)
   ```
   /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
   brew install node rustup-init
@@ -36,7 +36,7 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
     brew install foreman
     ```
 
-* Ensure the Zed.dev website is checked out in a sibling directory and install it's dependencies:
+* Ensure the Zed.dev website is checked out in a sibling directory and install its dependencies:
 
     ```
     cd ..

assets/keymaps/default.json πŸ”—

@@ -30,6 +30,7 @@
       "cmd-s": "workspace::Save",
       "cmd-shift-s": "workspace::SaveAs",
       "cmd-=": "zed::IncreaseBufferFontSize",
+      "cmd-+": "zed::IncreaseBufferFontSize",
       "cmd--": "zed::DecreaseBufferFontSize",
       "cmd-0": "zed::ResetBufferFontSize",
       "cmd-,": "zed::OpenSettings",
@@ -249,6 +250,7 @@
     "bindings": {
       "escape": "project_search::ToggleFocus",
       "alt-tab": "search::CycleMode",
+      "cmd-shift-h": "search::ToggleReplace",
       "alt-cmd-g": "search::ActivateRegexMode",
       "alt-cmd-s": "search::ActivateSemanticMode",
       "alt-cmd-x": "search::ActivateTextMode"
@@ -261,11 +263,19 @@
       "down": "search::NextHistoryQuery"
     }
   },
+  {
+    "context": "ProjectSearchBar && in_replace",
+    "bindings": {
+      "enter": "search::ReplaceNext",
+      "cmd-enter": "search::ReplaceAll"
+    }
+  },
   {
     "context": "ProjectSearchView",
     "bindings": {
       "escape": "project_search::ToggleFocus",
       "alt-tab": "search::CycleMode",
+      "cmd-shift-h": "search::ToggleReplace",
       "alt-cmd-g": "search::ActivateRegexMode",
       "alt-cmd-s": "search::ActivateSemanticMode",
       "alt-cmd-x": "search::ActivateTextMode"
@@ -277,6 +287,7 @@
       "cmd-f": "project_search::ToggleFocus",
       "cmd-g": "search::SelectNextMatch",
       "cmd-shift-g": "search::SelectPrevMatch",
+      "cmd-shift-h": "search::ToggleReplace",
       "alt-enter": "search::SelectAllMatches",
       "alt-cmd-c": "search::ToggleCaseSensitive",
       "alt-cmd-w": "search::ToggleWholeWord",
@@ -498,6 +509,22 @@
       "cmd-k cmd-down": [
         "workspace::ActivatePaneInDirection",
         "Down"
+      ],
+      "cmd-k shift-left": [
+        "workspace::SwapPaneInDirection",
+        "Left"
+      ],
+      "cmd-k shift-right": [
+        "workspace::SwapPaneInDirection",
+        "Right"
+      ],
+      "cmd-k shift-up": [
+        "workspace::SwapPaneInDirection",
+        "Up"
+      ],
+      "cmd-k shift-down": [
+        "workspace::SwapPaneInDirection",
+        "Down"
       ]
     }
   },
@@ -562,7 +589,7 @@
     }
   },
   {
-    "context": "ProjectSearchBar",
+    "context": "ProjectSearchBar && !in_replace",
     "bindings": {
       "cmd-enter": "project_search::SearchInNew"
     }
@@ -588,14 +615,20 @@
     }
   },
   {
-    "context": "CollabPanel",
+    "context": "CollabPanel && not_editing",
     "bindings": {
       "ctrl-backspace": "collab_panel::Remove",
       "space": "menu::Confirm"
     }
   },
   {
-    "context": "CollabPanel > Editor",
+    "context": "(CollabPanel && editing) > Editor",
+    "bindings": {
+      "space": "collab_panel::InsertSpace"
+    }
+  },
+  {
+    "context": "(CollabPanel && not_editing) > Editor",
     "bindings": {
       "cmd-c": "collab_panel::StartLinkChannel",
       "cmd-x": "collab_panel::StartMoveChannel",

assets/keymaps/vim.json πŸ”—

@@ -18,6 +18,7 @@
           }
         }
       ],
+      ":": "command_palette::Toggle",
       "h": "vim::Left",
       "left": "vim::Left",
       "backspace": "vim::Backspace",
@@ -125,6 +126,21 @@
       "g shift-t": "pane::ActivatePrevItem",
       "g d": "editor::GoToDefinition",
       "g shift-d": "editor::GoToTypeDefinition",
+      "g n": "vim::SelectNext",
+      "g shift-n": "vim::SelectPrevious",
+      "g >": [
+        "editor::SelectNext",
+        {
+          "replace_newest": true
+        }
+      ],
+      "g <": [
+        "editor::SelectPrevious",
+        {
+          "replace_newest": true
+        }
+      ],
+      "g a": "editor::SelectAllMatches",
       "g s": "outline::Toggle",
       "g shift-s": "project_symbols::Toggle",
       "g .": "editor::ToggleCodeActions", // zed specific
@@ -205,13 +221,13 @@
       "shift-z shift-q": [
         "pane::CloseActiveItem",
         {
-          "saveBehavior": "dontSave"
+          "saveIntent": "skip"
         }
       ],
       "shift-z shift-z": [
         "pane::CloseActiveItem",
         {
-          "saveBehavior": "promptOnConflict"
+          "saveIntent": "saveAll"
         }
       ],
       // Count support
@@ -300,6 +316,38 @@
         "workspace::ActivatePaneInDirection",
         "Down"
       ],
+      "ctrl-w shift-left": [
+        "workspace::SwapPaneInDirection",
+        "Left"
+      ],
+      "ctrl-w shift-right": [
+        "workspace::SwapPaneInDirection",
+        "Right"
+      ],
+      "ctrl-w shift-up": [
+        "workspace::SwapPaneInDirection",
+        "Up"
+      ],
+      "ctrl-w shift-down": [
+        "workspace::SwapPaneInDirection",
+        "Down"
+      ],
+      "ctrl-w shift-h": [
+        "workspace::SwapPaneInDirection",
+        "Left"
+      ],
+      "ctrl-w shift-l": [
+        "workspace::SwapPaneInDirection",
+        "Right"
+      ],
+      "ctrl-w shift-k": [
+        "workspace::SwapPaneInDirection",
+        "Up"
+      ],
+      "ctrl-w shift-j": [
+        "workspace::SwapPaneInDirection",
+        "Down"
+      ],
       "ctrl-w g t": "pane::ActivateNextItem",
       "ctrl-w ctrl-g t": "pane::ActivateNextItem",
       "ctrl-w g shift-t": "pane::ActivatePrevItem",
@@ -318,7 +366,17 @@
       "ctrl-w c": "pane::CloseAllItems",
       "ctrl-w ctrl-c": "pane::CloseAllItems",
       "ctrl-w q": "pane::CloseAllItems",
-      "ctrl-w ctrl-q": "pane::CloseAllItems"
+      "ctrl-w ctrl-q": "pane::CloseAllItems",
+      "ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
+      "ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
+      "ctrl-w n": [
+        "workspace::NewFileInDirection",
+        "Up"
+      ],
+      "ctrl-w ctrl-n": [
+        "workspace::NewFileInDirection",
+        "Up"
+      ]
     }
   },
   {

assets/settings/default.json πŸ”—

@@ -372,6 +372,27 @@
   "semantic_index": {
     "enabled": false
   },
+  // Settings specific to our elixir integration
+  "elixir": {
+    // Set Zed to use the experimental Next LS LSP server.
+    // Note that changing this setting requires a restart of Zed
+    // to take effect.
+    //
+    // May take 3 values:
+    //  1. Use the standard elixir-ls LSP server
+    //         "next": "off"
+    //  2. Use a bundled version of the next Next LS LSP server
+    //         "next": "on",
+    //  3. Use a local build of the next Next LS LSP server:
+    //         "next": {
+    //           "local": {
+    //             "path": "~/next-ls/bin/start",
+    //             "arguments": ["--stdio"]
+    //            }
+    //          },
+    //
+    "next": "off"
+  },
   // Different settings for specific languages.
   "languages": {
     "Plain Text": {

crates/ai/Cargo.toml πŸ”—

@@ -9,39 +9,26 @@ path = "src/ai.rs"
 doctest = false
 
 [dependencies]
-client = { path = "../client" }
-collections = { path = "../collections"}
-editor = { path = "../editor" }
-fs = { path = "../fs" }
 gpui = { path = "../gpui" }
-language = { path = "../language" }
-menu = { path = "../menu" }
-search = { path = "../search" }
-settings = { path = "../settings" }
-theme = { path = "../theme" }
 util = { path = "../util" }
-uuid = { version = "1.1.2", features = ["v4"] }
-workspace = { path = "../workspace" }
-
+async-trait.workspace = true
 anyhow.workspace = true
-chrono = { version = "0.4", features = ["serde"] }
 futures.workspace = true
-indoc.workspace = true
-isahc.workspace = true
+lazy_static.workspace = true
 ordered-float.workspace = true
 parking_lot.workspace = true
+isahc.workspace = true
 regex.workspace = true
-schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
-smol.workspace = true
-tiktoken-rs = "0.4"
+postage.workspace = true
+rand.workspace = true
+log.workspace = true
+parse_duration = "2.1.1"
+tiktoken-rs = "0.5.0"
+matrixmultiply = "0.3.7"
+rusqlite = { version = "0.27.0", features = ["blob", "array", "modern_sqlite"] }
+bincode = "1.3.3"
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
-project = { path = "../project", features = ["test-support"] }
-
-ctor.workspace = true
-env_logger.workspace = true
-log.workspace = true
-rand.workspace = true
+gpui = { path = "../gpui", features = ["test-support"] }

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

@@ -1,294 +1,2 @@
-pub mod assistant;
-mod assistant_settings;
-mod codegen;
-mod streaming_diff;
-
-use anyhow::{anyhow, Result};
-pub use assistant::AssistantPanel;
-use assistant_settings::OpenAIModel;
-use chrono::{DateTime, Local};
-use collections::HashMap;
-use fs::Fs;
-use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
-use gpui::{executor::Background, AppContext};
-use isahc::{http::StatusCode, Request, RequestExt};
-use regex::Regex;
-use serde::{Deserialize, Serialize};
-use std::{
-    cmp::Reverse,
-    ffi::OsStr,
-    fmt::{self, Display},
-    io,
-    path::PathBuf,
-    sync::Arc,
-};
-use util::paths::CONVERSATIONS_DIR;
-
-const OPENAI_API_URL: &'static str = "https://api.openai.com/v1";
-
-// Data types for chat completion requests
-#[derive(Debug, Default, Serialize)]
-pub struct OpenAIRequest {
-    model: String,
-    messages: Vec<RequestMessage>,
-    stream: bool,
-}
-
-#[derive(
-    Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize,
-)]
-struct MessageId(usize);
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-struct MessageMetadata {
-    role: Role,
-    sent_at: DateTime<Local>,
-    status: MessageStatus,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-enum MessageStatus {
-    Pending,
-    Done,
-    Error(Arc<str>),
-}
-
-#[derive(Serialize, Deserialize)]
-struct SavedMessage {
-    id: MessageId,
-    start: usize,
-}
-
-#[derive(Serialize, Deserialize)]
-struct SavedConversation {
-    id: Option<String>,
-    zed: String,
-    version: String,
-    text: String,
-    messages: Vec<SavedMessage>,
-    message_metadata: HashMap<MessageId, MessageMetadata>,
-    summary: String,
-    model: OpenAIModel,
-}
-
-impl SavedConversation {
-    const VERSION: &'static str = "0.1.0";
-}
-
-struct SavedConversationMetadata {
-    title: String,
-    path: PathBuf,
-    mtime: chrono::DateTime<chrono::Local>,
-}
-
-impl SavedConversationMetadata {
-    pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
-        fs.create_dir(&CONVERSATIONS_DIR).await?;
-
-        let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
-        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();
-
-            let metadata = fs.metadata(&path).await?;
-            if let Some((file_name, metadata)) = path
-                .file_name()
-                .and_then(|name| name.to_str())
-                .zip(metadata)
-            {
-                let title = re.replace(file_name, "");
-                conversations.push(Self {
-                    title: title.into_owned(),
-                    path,
-                    mtime: metadata.mtime.into(),
-                });
-            }
-        }
-        conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
-
-        Ok(conversations)
-    }
-}
-
-#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
-struct RequestMessage {
-    role: Role,
-    content: String,
-}
-
-#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
-pub struct ResponseMessage {
-    role: Option<Role>,
-    content: Option<String>,
-}
-
-#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
-#[serde(rename_all = "lowercase")]
-enum Role {
-    User,
-    Assistant,
-    System,
-}
-
-impl Role {
-    pub fn cycle(&mut self) {
-        *self = match self {
-            Role::User => Role::Assistant,
-            Role::Assistant => Role::System,
-            Role::System => Role::User,
-        }
-    }
-}
-
-impl Display for Role {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            Role::User => write!(f, "User"),
-            Role::Assistant => write!(f, "Assistant"),
-            Role::System => write!(f, "System"),
-        }
-    }
-}
-
-#[derive(Deserialize, Debug)]
-pub struct OpenAIResponseStreamEvent {
-    pub id: Option<String>,
-    pub object: String,
-    pub created: u32,
-    pub model: String,
-    pub choices: Vec<ChatChoiceDelta>,
-    pub usage: Option<Usage>,
-}
-
-#[derive(Deserialize, Debug)]
-pub struct Usage {
-    pub prompt_tokens: u32,
-    pub completion_tokens: u32,
-    pub total_tokens: u32,
-}
-
-#[derive(Deserialize, Debug)]
-pub struct ChatChoiceDelta {
-    pub index: u32,
-    pub delta: ResponseMessage,
-    pub finish_reason: Option<String>,
-}
-
-#[derive(Deserialize, Debug)]
-struct OpenAIUsage {
-    prompt_tokens: u64,
-    completion_tokens: u64,
-    total_tokens: u64,
-}
-
-#[derive(Deserialize, Debug)]
-struct OpenAIChoice {
-    text: String,
-    index: u32,
-    logprobs: Option<serde_json::Value>,
-    finish_reason: Option<String>,
-}
-
-pub fn init(cx: &mut AppContext) {
-    assistant::init(cx);
-}
-
-pub async fn stream_completion(
-    api_key: String,
-    executor: Arc<Background>,
-    mut request: OpenAIRequest,
-) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
-    request.stream = true;
-
-    let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
-
-    let json_data = serde_json::to_string(&request)?;
-    let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions"))
-        .header("Content-Type", "application/json")
-        .header("Authorization", format!("Bearer {}", api_key))
-        .body(json_data)?
-        .send_async()
-        .await?;
-
-    let status = response.status();
-    if status == StatusCode::OK {
-        executor
-            .spawn(async move {
-                let mut lines = BufReader::new(response.body_mut()).lines();
-
-                fn parse_line(
-                    line: Result<String, io::Error>,
-                ) -> Result<Option<OpenAIResponseStreamEvent>> {
-                    if let Some(data) = line?.strip_prefix("data: ") {
-                        let event = serde_json::from_str(&data)?;
-                        Ok(Some(event))
-                    } else {
-                        Ok(None)
-                    }
-                }
-
-                while let Some(line) = lines.next().await {
-                    if let Some(event) = parse_line(line).transpose() {
-                        let done = event.as_ref().map_or(false, |event| {
-                            event
-                                .choices
-                                .last()
-                                .map_or(false, |choice| choice.finish_reason.is_some())
-                        });
-                        if tx.unbounded_send(event).is_err() {
-                            break;
-                        }
-
-                        if done {
-                            break;
-                        }
-                    }
-                }
-
-                anyhow::Ok(())
-            })
-            .detach();
-
-        Ok(rx)
-    } else {
-        let mut body = String::new();
-        response.body_mut().read_to_string(&mut body).await?;
-
-        #[derive(Deserialize)]
-        struct OpenAIResponse {
-            error: OpenAIError,
-        }
-
-        #[derive(Deserialize)]
-        struct OpenAIError {
-            message: String,
-        }
-
-        match serde_json::from_str::<OpenAIResponse>(&body) {
-            Ok(response) if !response.error.message.is_empty() => Err(anyhow!(
-                "Failed to connect to OpenAI API: {}",
-                response.error.message,
-            )),
-
-            _ => Err(anyhow!(
-                "Failed to connect to OpenAI API: {} {}",
-                response.status(),
-                body,
-            )),
-        }
-    }
-}
-
-#[cfg(test)]
-#[ctor::ctor]
-fn init_logger() {
-    if std::env::var("RUST_LOG").is_ok() {
-        env_logger::init();
-    }
-}
+pub mod completion;
+pub mod embedding;

crates/ai/src/completion.rs πŸ”—

@@ -0,0 +1,212 @@
+use anyhow::{anyhow, Result};
+use futures::{
+    future::BoxFuture, io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, FutureExt,
+    Stream, StreamExt,
+};
+use gpui::executor::Background;
+use isahc::{http::StatusCode, Request, RequestExt};
+use serde::{Deserialize, Serialize};
+use std::{
+    fmt::{self, Display},
+    io,
+    sync::Arc,
+};
+
+pub const OPENAI_API_URL: &'static str = "https://api.openai.com/v1";
+
+#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
+#[serde(rename_all = "lowercase")]
+pub enum Role {
+    User,
+    Assistant,
+    System,
+}
+
+impl Role {
+    pub fn cycle(&mut self) {
+        *self = match self {
+            Role::User => Role::Assistant,
+            Role::Assistant => Role::System,
+            Role::System => Role::User,
+        }
+    }
+}
+
+impl Display for Role {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Role::User => write!(f, "User"),
+            Role::Assistant => write!(f, "Assistant"),
+            Role::System => write!(f, "System"),
+        }
+    }
+}
+
+#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
+pub struct RequestMessage {
+    pub role: Role,
+    pub content: String,
+}
+
+#[derive(Debug, Default, Serialize)]
+pub struct OpenAIRequest {
+    pub model: String,
+    pub messages: Vec<RequestMessage>,
+    pub stream: bool,
+}
+
+#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
+pub struct ResponseMessage {
+    pub role: Option<Role>,
+    pub content: Option<String>,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct OpenAIUsage {
+    pub prompt_tokens: u32,
+    pub completion_tokens: u32,
+    pub total_tokens: u32,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct ChatChoiceDelta {
+    pub index: u32,
+    pub delta: ResponseMessage,
+    pub finish_reason: Option<String>,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct OpenAIResponseStreamEvent {
+    pub id: Option<String>,
+    pub object: String,
+    pub created: u32,
+    pub model: String,
+    pub choices: Vec<ChatChoiceDelta>,
+    pub usage: Option<OpenAIUsage>,
+}
+
+pub async fn stream_completion(
+    api_key: String,
+    executor: Arc<Background>,
+    mut request: OpenAIRequest,
+) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
+    request.stream = true;
+
+    let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
+
+    let json_data = serde_json::to_string(&request)?;
+    let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions"))
+        .header("Content-Type", "application/json")
+        .header("Authorization", format!("Bearer {}", api_key))
+        .body(json_data)?
+        .send_async()
+        .await?;
+
+    let status = response.status();
+    if status == StatusCode::OK {
+        executor
+            .spawn(async move {
+                let mut lines = BufReader::new(response.body_mut()).lines();
+
+                fn parse_line(
+                    line: Result<String, io::Error>,
+                ) -> Result<Option<OpenAIResponseStreamEvent>> {
+                    if let Some(data) = line?.strip_prefix("data: ") {
+                        let event = serde_json::from_str(&data)?;
+                        Ok(Some(event))
+                    } else {
+                        Ok(None)
+                    }
+                }
+
+                while let Some(line) = lines.next().await {
+                    if let Some(event) = parse_line(line).transpose() {
+                        let done = event.as_ref().map_or(false, |event| {
+                            event
+                                .choices
+                                .last()
+                                .map_or(false, |choice| choice.finish_reason.is_some())
+                        });
+                        if tx.unbounded_send(event).is_err() {
+                            break;
+                        }
+
+                        if done {
+                            break;
+                        }
+                    }
+                }
+
+                anyhow::Ok(())
+            })
+            .detach();
+
+        Ok(rx)
+    } else {
+        let mut body = String::new();
+        response.body_mut().read_to_string(&mut body).await?;
+
+        #[derive(Deserialize)]
+        struct OpenAIResponse {
+            error: OpenAIError,
+        }
+
+        #[derive(Deserialize)]
+        struct OpenAIError {
+            message: String,
+        }
+
+        match serde_json::from_str::<OpenAIResponse>(&body) {
+            Ok(response) if !response.error.message.is_empty() => Err(anyhow!(
+                "Failed to connect to OpenAI API: {}",
+                response.error.message,
+            )),
+
+            _ => Err(anyhow!(
+                "Failed to connect to OpenAI API: {} {}",
+                response.status(),
+                body,
+            )),
+        }
+    }
+}
+
+pub trait CompletionProvider {
+    fn complete(
+        &self,
+        prompt: OpenAIRequest,
+    ) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>>;
+}
+
+pub struct OpenAICompletionProvider {
+    api_key: String,
+    executor: Arc<Background>,
+}
+
+impl OpenAICompletionProvider {
+    pub fn new(api_key: String, executor: Arc<Background>) -> Self {
+        Self { api_key, executor }
+    }
+}
+
+impl CompletionProvider for OpenAICompletionProvider {
+    fn complete(
+        &self,
+        prompt: OpenAIRequest,
+    ) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
+        let request = stream_completion(self.api_key.clone(), self.executor.clone(), prompt);
+        async move {
+            let response = request.await?;
+            let stream = response
+                .filter_map(|response| async move {
+                    match response {
+                        Ok(mut response) => Some(Ok(response.choices.pop()?.delta.content?)),
+                        Err(error) => Some(Err(error)),
+                    }
+                })
+                .boxed();
+            Ok(stream)
+        }
+        .boxed()
+    }
+}

crates/semantic_index/src/embedding.rs β†’ crates/ai/src/embedding.rs πŸ”—

@@ -27,8 +27,30 @@ lazy_static! {
 }
 
 #[derive(Debug, PartialEq, Clone)]
-pub struct Embedding(Vec<f32>);
+pub struct Embedding(pub Vec<f32>);
 
+// This is needed for semantic index functionality
+// Unfortunately it has to live wherever the "Embedding" struct is created.
+// Keeping this in here though, introduces a 'rusqlite' dependency into AI
+// which is less than ideal
+impl FromSql for Embedding {
+    fn column_result(value: ValueRef) -> FromSqlResult<Self> {
+        let bytes = value.as_blob()?;
+        let embedding: Result<Vec<f32>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
+        if embedding.is_err() {
+            return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err()));
+        }
+        Ok(Embedding(embedding.unwrap()))
+    }
+}
+
+impl ToSql for Embedding {
+    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
+        let bytes = bincode::serialize(&self.0)
+            .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
+        Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes)))
+    }
+}
 impl From<Vec<f32>> for Embedding {
     fn from(value: Vec<f32>) -> Self {
         Embedding(value)
@@ -63,24 +85,24 @@ impl Embedding {
     }
 }
 
-impl FromSql for Embedding {
-    fn column_result(value: ValueRef) -> FromSqlResult<Self> {
-        let bytes = value.as_blob()?;
-        let embedding: Result<Vec<f32>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
-        if embedding.is_err() {
-            return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err()));
-        }
-        Ok(Embedding(embedding.unwrap()))
-    }
-}
-
-impl ToSql for Embedding {
-    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
-        let bytes = bincode::serialize(&self.0)
-            .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
-        Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes)))
-    }
-}
+// impl FromSql for Embedding {
+//     fn column_result(value: ValueRef) -> FromSqlResult<Self> {
+//         let bytes = value.as_blob()?;
+//         let embedding: Result<Vec<f32>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
+//         if embedding.is_err() {
+//             return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err()));
+//         }
+//         Ok(Embedding(embedding.unwrap()))
+//     }
+// }
+
+// impl ToSql for Embedding {
+//     fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
+//         let bytes = bincode::serialize(&self.0)
+//             .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
+//         Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes)))
+//     }
+// }
 
 #[derive(Clone)]
 pub struct OpenAIEmbeddings {
@@ -117,6 +139,7 @@ struct OpenAIEmbeddingUsage {
 
 #[async_trait]
 pub trait EmbeddingProvider: Sync + Send {
+    fn is_authenticated(&self) -> bool;
     async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>>;
     fn max_tokens_per_batch(&self) -> usize;
     fn truncate(&self, span: &str) -> (String, usize);
@@ -127,6 +150,9 @@ pub struct DummyEmbeddings {}
 
 #[async_trait]
 impl EmbeddingProvider for DummyEmbeddings {
+    fn is_authenticated(&self) -> bool {
+        true
+    }
     fn rate_limit_expiration(&self) -> Option<Instant> {
         None
     }
@@ -229,6 +255,9 @@ impl OpenAIEmbeddings {
 
 #[async_trait]
 impl EmbeddingProvider for OpenAIEmbeddings {
+    fn is_authenticated(&self) -> bool {
+        OPENAI_API_KEY.as_ref().is_some()
+    }
     fn max_tokens_per_batch(&self) -> usize {
         50000
     }

crates/assistant/Cargo.toml πŸ”—

@@ -0,0 +1,48 @@
+[package]
+name = "assistant"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/assistant.rs"
+doctest = false
+
+[dependencies]
+ai = { path = "../ai" }
+client = { path = "../client" }
+collections = { path = "../collections"}
+editor = { path = "../editor" }
+fs = { path = "../fs" }
+gpui = { path = "../gpui" }
+language = { path = "../language" }
+menu = { path = "../menu" }
+search = { path = "../search" }
+settings = { path = "../settings" }
+theme = { path = "../theme" }
+util = { path = "../util" }
+uuid = { version = "1.1.2", features = ["v4"] }
+workspace = { path = "../workspace" }
+
+anyhow.workspace = true
+chrono = { version = "0.4", features = ["serde"] }
+futures.workspace = true
+indoc.workspace = true
+isahc.workspace = true
+ordered-float.workspace = true
+parking_lot.workspace = true
+regex.workspace = true
+schemars.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+smol.workspace = true
+tiktoken-rs = "0.4"
+
+[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
+project = { path = "../project", features = ["test-support"] }
+
+ctor.workspace = true
+env_logger.workspace = true
+log.workspace = true
+rand.workspace = true

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

@@ -0,0 +1,112 @@
+pub mod assistant_panel;
+mod assistant_settings;
+mod codegen;
+mod streaming_diff;
+
+use ai::completion::Role;
+use anyhow::Result;
+pub use assistant_panel::AssistantPanel;
+use assistant_settings::OpenAIModel;
+use chrono::{DateTime, Local};
+use collections::HashMap;
+use fs::Fs;
+use futures::StreamExt;
+use gpui::AppContext;
+use regex::Regex;
+use serde::{Deserialize, Serialize};
+use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc};
+use util::paths::CONVERSATIONS_DIR;
+
+#[derive(
+    Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize,
+)]
+struct MessageId(usize);
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+struct MessageMetadata {
+    role: Role,
+    sent_at: DateTime<Local>,
+    status: MessageStatus,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+enum MessageStatus {
+    Pending,
+    Done,
+    Error(Arc<str>),
+}
+
+#[derive(Serialize, Deserialize)]
+struct SavedMessage {
+    id: MessageId,
+    start: usize,
+}
+
+#[derive(Serialize, Deserialize)]
+struct SavedConversation {
+    id: Option<String>,
+    zed: String,
+    version: String,
+    text: String,
+    messages: Vec<SavedMessage>,
+    message_metadata: HashMap<MessageId, MessageMetadata>,
+    summary: String,
+    model: OpenAIModel,
+}
+
+impl SavedConversation {
+    const VERSION: &'static str = "0.1.0";
+}
+
+struct SavedConversationMetadata {
+    title: String,
+    path: PathBuf,
+    mtime: chrono::DateTime<chrono::Local>,
+}
+
+impl SavedConversationMetadata {
+    pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
+        fs.create_dir(&CONVERSATIONS_DIR).await?;
+
+        let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
+        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();
+
+            let metadata = fs.metadata(&path).await?;
+            if let Some((file_name, metadata)) = path
+                .file_name()
+                .and_then(|name| name.to_str())
+                .zip(metadata)
+            {
+                let title = re.replace(file_name, "");
+                conversations.push(Self {
+                    title: title.into_owned(),
+                    path,
+                    mtime: metadata.mtime.into(),
+                });
+            }
+        }
+        conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
+
+        Ok(conversations)
+    }
+}
+
+pub fn init(cx: &mut AppContext) {
+    assistant_panel::init(cx);
+}
+
+#[cfg(test)]
+#[ctor::ctor]
+fn init_logger() {
+    if std::env::var("RUST_LOG").is_ok() {
+        env_logger::init();
+    }
+}

crates/ai/src/assistant.rs β†’ crates/assistant/src/assistant_panel.rs πŸ”—

@@ -1,8 +1,11 @@
 use crate::{
     assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel},
-    codegen::{self, Codegen, CodegenKind, OpenAICompletionProvider},
-    stream_completion, MessageId, MessageMetadata, MessageStatus, OpenAIRequest, RequestMessage,
-    Role, SavedConversation, SavedConversationMetadata, SavedMessage, OPENAI_API_URL,
+    codegen::{self, Codegen, CodegenKind},
+    MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata,
+    SavedMessage,
+};
+use ai::completion::{
+    stream_completion, OpenAICompletionProvider, OpenAIRequest, RequestMessage, OPENAI_API_URL,
 };
 use anyhow::{anyhow, Result};
 use chrono::{DateTime, Local};

crates/ai/src/codegen.rs β†’ crates/assistant/src/codegen.rs πŸ”—

@@ -1,59 +1,14 @@
-use crate::{
-    stream_completion,
-    streaming_diff::{Hunk, StreamingDiff},
-    OpenAIRequest,
-};
+use crate::streaming_diff::{Hunk, StreamingDiff};
+use ai::completion::{CompletionProvider, OpenAIRequest};
 use anyhow::Result;
 use editor::{
     multi_buffer, Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
 };
-use futures::{
-    channel::mpsc, future::BoxFuture, stream::BoxStream, FutureExt, SinkExt, Stream, StreamExt,
-};
-use gpui::{executor::Background, Entity, ModelContext, ModelHandle, Task};
+use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
+use gpui::{Entity, ModelContext, ModelHandle, Task};
 use language::{Rope, TransactionId};
 use std::{cmp, future, ops::Range, sync::Arc};
 
-pub trait CompletionProvider {
-    fn complete(
-        &self,
-        prompt: OpenAIRequest,
-    ) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>>;
-}
-
-pub struct OpenAICompletionProvider {
-    api_key: String,
-    executor: Arc<Background>,
-}
-
-impl OpenAICompletionProvider {
-    pub fn new(api_key: String, executor: Arc<Background>) -> Self {
-        Self { api_key, executor }
-    }
-}
-
-impl CompletionProvider for OpenAICompletionProvider {
-    fn complete(
-        &self,
-        prompt: OpenAIRequest,
-    ) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
-        let request = stream_completion(self.api_key.clone(), self.executor.clone(), prompt);
-        async move {
-            let response = request.await?;
-            let stream = response
-                .filter_map(|response| async move {
-                    match response {
-                        Ok(mut response) => Some(Ok(response.choices.pop()?.delta.content?)),
-                        Err(error) => Some(Err(error)),
-                    }
-                })
-                .boxed();
-            Ok(stream)
-        }
-        .boxed()
-    }
-}
-
 pub enum Event {
     Finished,
     Undone,
@@ -397,13 +352,17 @@ fn strip_markdown_codeblock(
 #[cfg(test)]
 mod tests {
     use super::*;
-    use futures::stream;
+    use futures::{
+        future::BoxFuture,
+        stream::{self, BoxStream},
+    };
     use gpui::{executor::Deterministic, TestAppContext};
     use indoc::indoc;
     use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point};
     use parking_lot::Mutex;
     use rand::prelude::*;
     use settings::SettingsStore;
+    use smol::future::FutureExt;
 
     #[gpui::test(iterations = 10)]
     async fn test_transform_autoindent(

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

@@ -394,9 +394,14 @@ impl ActiveCall {
 
         cx.spawn(|this, mut cx| async move {
             let result = invite.await;
+            if result.is_ok() {
+                this.update(&mut cx, |this, cx| this.report_call_event("invite", cx));
+            } else {
+                // TODO: Resport collaboration error
+            }
+
             this.update(&mut cx, |this, cx| {
                 this.pending_invites.remove(&called_user_id);
-                this.report_call_event("invite", cx);
                 cx.notify();
             });
             result
@@ -461,13 +466,7 @@ impl ActiveCall {
             .borrow_mut()
             .take()
             .ok_or_else(|| anyhow!("no incoming call"))?;
-        Self::report_call_event_for_room(
-            "decline incoming",
-            Some(call.room_id),
-            None,
-            &self.client,
-            cx,
-        );
+        report_call_event_for_room("decline incoming", call.room_id, None, &self.client, cx);
         self.client.send(proto::DeclineCall {
             room_id: call.room_id,
         })?;
@@ -597,31 +596,46 @@ impl ActiveCall {
         &self.pending_invites
     }
 
-    fn report_call_event(&self, operation: &'static str, cx: &AppContext) {
-        let (room_id, channel_id) = match self.room() {
-            Some(room) => {
-                let room = room.read(cx);
-                (Some(room.id()), room.channel_id())
-            }
-            None => (None, None),
-        };
-        Self::report_call_event_for_room(operation, room_id, channel_id, &self.client, cx)
+    pub fn report_call_event(&self, operation: &'static str, cx: &AppContext) {
+        if let Some(room) = self.room() {
+            let room = room.read(cx);
+            report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client, cx);
+        }
     }
+}
 
-    pub fn report_call_event_for_room(
-        operation: &'static str,
-        room_id: Option<u64>,
-        channel_id: Option<u64>,
-        client: &Arc<Client>,
-        cx: &AppContext,
-    ) {
-        let telemetry = client.telemetry();
-        let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
-        let event = ClickhouseEvent::Call {
-            operation,
-            room_id,
-            channel_id,
-        };
-        telemetry.report_clickhouse_event(event, telemetry_settings);
-    }
+pub fn report_call_event_for_room(
+    operation: &'static str,
+    room_id: u64,
+    channel_id: Option<u64>,
+    client: &Arc<Client>,
+    cx: &AppContext,
+) {
+    let telemetry = client.telemetry();
+    let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
+    let event = ClickhouseEvent::Call {
+        operation,
+        room_id: Some(room_id),
+        channel_id,
+    };
+    telemetry.report_clickhouse_event(event, telemetry_settings);
+}
+
+pub fn report_call_event_for_channel(
+    operation: &'static str,
+    channel_id: u64,
+    client: &Arc<Client>,
+    cx: &AppContext,
+) {
+    let room = ActiveCall::global(cx).read(cx).room();
+
+    let telemetry = client.telemetry();
+    let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
+
+    let event = ClickhouseEvent::Call {
+        operation,
+        room_id: room.map(|r| r.read(cx).id()),
+        channel_id: Some(channel_id),
+    };
+    telemetry.report_clickhouse_event(event, telemetry_settings);
 }

crates/collab_ui/src/channel_view.rs πŸ”—

@@ -1,5 +1,5 @@
 use anyhow::{anyhow, Result};
-use call::ActiveCall;
+use call::report_call_event_for_channel;
 use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId};
 use client::{
     proto::{self, PeerId},
@@ -52,14 +52,9 @@ impl ChannelView {
         cx.spawn(|mut cx| async move {
             let channel_view = channel_view.await?;
             pane.update(&mut cx, |pane, cx| {
-                let room_id = ActiveCall::global(cx)
-                    .read(cx)
-                    .room()
-                    .map(|room| room.read(cx).id());
-                ActiveCall::report_call_event_for_room(
+                report_call_event_for_channel(
                     "open channel notes",
-                    room_id,
-                    Some(channel_id),
+                    channel_id,
                     &workspace.read(cx).app_state().client,
                     cx,
                 );

crates/collab_ui/src/collab_panel.rs πŸ”—

@@ -136,6 +136,7 @@ actions!(
         StartMoveChannel,
         StartLinkChannel,
         MoveOrLinkToSelected,
+        InsertSpace,
     ]
 );
 
@@ -184,6 +185,7 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(CollabPanel::select_next);
     cx.add_action(CollabPanel::select_prev);
     cx.add_action(CollabPanel::confirm);
+    cx.add_action(CollabPanel::insert_space);
     cx.add_action(CollabPanel::remove);
     cx.add_action(CollabPanel::remove_selected_channel);
     cx.add_action(CollabPanel::show_inline_context_menu);
@@ -2518,6 +2520,14 @@ impl CollabPanel {
         }
     }
 
+    fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) {
+        if self.channel_editing_state.is_some() {
+            self.channel_name_editor.update(cx, |editor, cx| {
+                editor.insert(" ", cx);
+            });
+        }
+    }
+
     fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
         if let Some(editing_state) = &mut self.channel_editing_state {
             match editing_state {
@@ -3054,6 +3064,19 @@ impl View for CollabPanel {
         .on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
         .into_any_named("collab panel")
     }
+
+    fn update_keymap_context(
+        &self,
+        keymap: &mut gpui::keymap_matcher::KeymapContext,
+        _: &AppContext,
+    ) {
+        Self::reset_to_default_keymap_context(keymap);
+        if self.channel_editing_state.is_some() {
+            keymap.add_identifier("editing");
+        } else {
+            keymap.add_identifier("not_editing");
+        }
+    }
 }
 
 impl Panel for CollabPanel {

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

@@ -10,7 +10,7 @@ mod panel_settings;
 mod project_shared_notification;
 mod sharing_status_indicator;
 
-use call::{ActiveCall, Room};
+use call::{report_call_event_for_room, ActiveCall, Room};
 use gpui::{
     actions,
     geometry::{
@@ -55,18 +55,18 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
         let client = call.client();
         let toggle_screen_sharing = room.update(cx, |room, cx| {
             if room.is_screen_sharing() {
-                ActiveCall::report_call_event_for_room(
+                report_call_event_for_room(
                     "disable screen share",
-                    Some(room.id()),
+                    room.id(),
                     room.channel_id(),
                     &client,
                     cx,
                 );
                 Task::ready(room.unshare_screen(cx))
             } else {
-                ActiveCall::report_call_event_for_room(
+                report_call_event_for_room(
                     "enable screen share",
-                    Some(room.id()),
+                    room.id(),
                     room.channel_id(),
                     &client,
                     cx,
@@ -83,23 +83,13 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
     if let Some(room) = call.room().cloned() {
         let client = call.client();
         room.update(cx, |room, cx| {
-            if room.is_muted(cx) {
-                ActiveCall::report_call_event_for_room(
-                    "enable microphone",
-                    Some(room.id()),
-                    room.channel_id(),
-                    &client,
-                    cx,
-                );
+            let operation = if room.is_muted(cx) {
+                "enable microphone"
             } else {
-                ActiveCall::report_call_event_for_room(
-                    "disable microphone",
-                    Some(room.id()),
-                    room.channel_id(),
-                    &client,
-                    cx,
-                );
-            }
+                "disable microphone"
+            };
+            report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
+
             room.toggle_mute(cx)
         })
         .map(|task| task.detach_and_log_err(cx))

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

@@ -18,6 +18,15 @@ actions!(command_palette, [Toggle]);
 
 pub type CommandPalette = Picker<CommandPaletteDelegate>;
 
+pub type CommandPaletteInterceptor =
+    Box<dyn Fn(&str, &AppContext) -> Option<CommandInterceptResult>>;
+
+pub struct CommandInterceptResult {
+    pub action: Box<dyn Action>,
+    pub string: String,
+    pub positions: Vec<usize>,
+}
+
 pub struct CommandPaletteDelegate {
     actions: Vec<Command>,
     matches: Vec<StringMatch>,
@@ -117,7 +126,7 @@ impl PickerDelegate for CommandPaletteDelegate {
                     }
                 })
                 .collect::<Vec<_>>();
-            let actions = cx.read(move |cx| {
+            let mut actions = cx.read(move |cx| {
                 let hit_counts = cx.optional_global::<HitCounts>();
                 actions.sort_by_key(|action| {
                     (
@@ -136,7 +145,7 @@ impl PickerDelegate for CommandPaletteDelegate {
                     char_bag: command.name.chars().collect(),
                 })
                 .collect::<Vec<_>>();
-            let matches = if query.is_empty() {
+            let mut matches = if query.is_empty() {
                 candidates
                     .into_iter()
                     .enumerate()
@@ -158,6 +167,40 @@ impl PickerDelegate for CommandPaletteDelegate {
                 )
                 .await
             };
+            let intercept_result = cx.read(|cx| {
+                if cx.has_global::<CommandPaletteInterceptor>() {
+                    cx.global::<CommandPaletteInterceptor>()(&query, cx)
+                } else {
+                    None
+                }
+            });
+            if let Some(CommandInterceptResult {
+                action,
+                string,
+                positions,
+            }) = intercept_result
+            {
+                if let Some(idx) = matches
+                    .iter()
+                    .position(|m| actions[m.candidate_id].action.id() == action.id())
+                {
+                    matches.remove(idx);
+                }
+                actions.push(Command {
+                    name: string.clone(),
+                    action,
+                    keystrokes: vec![],
+                });
+                matches.insert(
+                    0,
+                    StringMatch {
+                        candidate_id: actions.len() - 1,
+                        string,
+                        positions,
+                        score: 0.0,
+                    },
+                )
+            }
             picker
                 .update(&mut cx, |picker, _| {
                     let delegate = picker.delegate_mut();

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

@@ -104,7 +104,7 @@ use sum_tree::TreeMap;
 use text::Rope;
 use theme::{DiagnosticStyle, Theme, ThemeSettings};
 use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
-use workspace::{ItemNavHistory, ViewId, Workspace};
+use workspace::{ItemNavHistory, SplitDirection, ViewId, Workspace};
 
 use crate::git::diff_hunk_to_display;
 
@@ -364,6 +364,7 @@ pub fn init_settings(cx: &mut AppContext) {
 pub fn init(cx: &mut AppContext) {
     init_settings(cx);
     cx.add_action(Editor::new_file);
+    cx.add_action(Editor::new_file_in_direction);
     cx.add_action(Editor::cancel);
     cx.add_action(Editor::newline);
     cx.add_action(Editor::newline_above);
@@ -1141,12 +1142,14 @@ struct CodeActionsMenu {
 impl CodeActionsMenu {
     fn select_first(&mut self, cx: &mut ViewContext<Editor>) {
         self.selected_item = 0;
+        self.list.scroll_to(ScrollTarget::Show(self.selected_item));
         cx.notify()
     }
 
     fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
         if self.selected_item > 0 {
             self.selected_item -= 1;
+            self.list.scroll_to(ScrollTarget::Show(self.selected_item));
             cx.notify()
         }
     }
@@ -1154,12 +1157,14 @@ impl CodeActionsMenu {
     fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
         if self.selected_item + 1 < self.actions.len() {
             self.selected_item += 1;
+            self.list.scroll_to(ScrollTarget::Show(self.selected_item));
             cx.notify()
         }
     }
 
     fn select_last(&mut self, cx: &mut ViewContext<Editor>) {
         self.selected_item = self.actions.len() - 1;
+        self.list.scroll_to(ScrollTarget::Show(self.selected_item));
         cx.notify()
     }
 
@@ -1212,7 +1217,9 @@ impl CodeActionsMenu {
                                     workspace.update(cx, |workspace, cx| {
                                         if let Some(task) = Editor::confirm_code_action(
                                             workspace,
-                                            &Default::default(),
+                                            &ConfirmCodeAction {
+                                                item_ix: Some(item_ix),
+                                            },
                                             cx,
                                         ) {
                                             task.detach_and_log_err(cx);
@@ -1637,6 +1644,26 @@ impl Editor {
         }
     }
 
+    pub fn new_file_in_direction(
+        workspace: &mut Workspace,
+        action: &workspace::NewFileInDirection,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        let project = workspace.project().clone();
+        if project.read(cx).is_remote() {
+            cx.propagate_action();
+        } else if let Some(buffer) = project
+            .update(cx, |project, cx| project.create_buffer("", None, cx))
+            .log_err()
+        {
+            workspace.split_item(
+                action.0,
+                Box::new(cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))),
+                cx,
+            );
+        }
+    }
+
     pub fn replica_id(&self, cx: &AppContext) -> ReplicaId {
         self.buffer.read(cx).replica_id()
     }
@@ -4631,7 +4658,13 @@ impl Editor {
     }
 
     pub fn convert_to_title_case(&mut self, _: &ConvertToTitleCase, cx: &mut ViewContext<Self>) {
-        self.manipulate_text(cx, |text| text.to_case(Case::Title))
+        self.manipulate_text(cx, |text| {
+            // Hack to get around the fact that to_case crate doesn't support '\n' as a word boundary
+            // https://github.com/rutrum/convert-case/issues/16
+            text.split("\n")
+                .map(|line| line.to_case(Case::Title))
+                .join("\n")
+        })
     }
 
     pub fn convert_to_snake_case(&mut self, _: &ConvertToSnakeCase, cx: &mut ViewContext<Self>) {
@@ -4647,7 +4680,13 @@ impl Editor {
         _: &ConvertToUpperCamelCase,
         cx: &mut ViewContext<Self>,
     ) {
-        self.manipulate_text(cx, |text| text.to_case(Case::UpperCamel))
+        self.manipulate_text(cx, |text| {
+            // Hack to get around the fact that to_case crate doesn't support '\n' as a word boundary
+            // https://github.com/rutrum/convert-case/issues/16
+            text.split("\n")
+                .map(|line| line.to_case(Case::UpperCamel))
+                .join("\n")
+        })
     }
 
     pub fn convert_to_lower_camel_case(
@@ -7135,7 +7174,7 @@ impl Editor {
             );
         });
         if split {
-            workspace.split_item(Box::new(editor), cx);
+            workspace.split_item(SplitDirection::Right, Box::new(editor), cx);
         } else {
             workspace.add_item(Box::new(editor), cx);
         }
@@ -8566,6 +8605,29 @@ impl Editor {
 
         self.handle_input(text, cx);
     }
+
+    pub fn supports_inlay_hints(&self, cx: &AppContext) -> bool {
+        let Some(project) = self.project.as_ref() else {
+            return false;
+        };
+        let project = project.read(cx);
+
+        let mut supports = false;
+        self.buffer().read(cx).for_each_buffer(|buffer| {
+            if !supports {
+                supports = project
+                    .language_servers_for_buffer(buffer.read(cx), cx)
+                    .any(
+                        |(_, server)| match server.capabilities().inlay_hint_provider {
+                            Some(lsp::OneOf::Left(enabled)) => enabled,
+                            Some(lsp::OneOf::Right(_)) => true,
+                            None => false,
+                        },
+                    )
+            }
+        });
+        supports
+    }
 }
 
 pub trait CollaborationHub {

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

@@ -1429,7 +1429,7 @@ async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
         assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.));
 
         editor.scroll_screen(&ScrollAmount::Page(-0.5), cx);
-        assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.));
+        assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.));
         editor.scroll_screen(&ScrollAmount::Page(0.5), cx);
         assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.));
     });
@@ -2792,6 +2792,34 @@ async fn test_manipulate_text(cx: &mut TestAppContext) {
         «hello worldˇ»
     "});
 
+    // Test multiple line, single selection case
+    // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
+    cx.set_state(indoc! {"
+        Β«The quick brown
+        fox jumps over
+        the lazy dogˇ»
+    "});
+    cx.update_editor(|e, cx| e.convert_to_title_case(&ConvertToTitleCase, cx));
+    cx.assert_editor_state(indoc! {"
+        Β«The Quick Brown
+        Fox Jumps Over
+        The Lazy Dogˇ»
+    "});
+
+    // Test multiple line, single selection case
+    // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
+    cx.set_state(indoc! {"
+        Β«The quick brown
+        fox jumps over
+        the lazy dogˇ»
+    "});
+    cx.update_editor(|e, cx| e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, cx));
+    cx.assert_editor_state(indoc! {"
+        Β«TheQuickBrown
+        FoxJumpsOver
+        TheLazyDogˇ»
+    "});
+
     // From here on out, test more complex cases of manipulate_text()
 
     // Test no selection case - should affect words cursors are in

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

@@ -941,7 +941,7 @@ async fn fetch_and_update_hints(
         })
         .await;
     if let Some(new_update) = new_update {
-        log::info!(
+        log::debug!(
             "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}",
             new_update.remove_from_visible.len(),
             new_update.remove_from_cache.len(),

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

@@ -996,7 +996,9 @@ impl SearchableItem for Editor {
         };
 
         if let Some(replacement) = query.replacement_for(&text) {
-            self.edit([(identifier.clone(), Arc::from(&*replacement))], cx);
+            self.transact(cx, |this, cx| {
+                this.edit([(identifier.clone(), Arc::from(&*replacement))], cx);
+            });
         }
     }
     fn match_index_for_direction(

crates/editor/src/scroll/scroll_amount.rs πŸ”—

@@ -15,9 +15,13 @@ impl ScrollAmount {
             Self::Line(count) => *count,
             Self::Page(count) => editor
                 .visible_line_count()
-                // subtract one to leave an anchor line
-                // round towards zero (so page-up and page-down are symmetric)
-                .map(|l| (l * count).trunc() - count.signum())
+                .map(|mut l| {
+                    // for full pages subtract one to leave an anchor line
+                    if count.abs() == 1.0 {
+                        l -= 1.0
+                    }
+                    (l * count).trunc()
+                })
                 .unwrap_or(0.),
         }
     }

crates/editor/src/test/editor_test_context.rs πŸ”—

@@ -3,8 +3,8 @@ use crate::{
 };
 use futures::Future;
 use gpui::{
-    keymap_matcher::Keystroke, AnyWindowHandle, AppContext, ContextHandle, ModelContext,
-    ViewContext, ViewHandle,
+    executor::Foreground, keymap_matcher::Keystroke, AnyWindowHandle, AppContext, ContextHandle,
+    ModelContext, ViewContext, ViewHandle,
 };
 use indoc::indoc;
 use language::{Buffer, BufferSnapshot};
@@ -114,6 +114,7 @@ impl<'a> EditorTestContext<'a> {
         let keystroke = Keystroke::parse(keystroke_text).unwrap();
 
         self.cx.dispatch_keystroke(self.window, keystroke, false);
+
         keystroke_under_test_handle
     }
 
@@ -126,6 +127,16 @@ impl<'a> EditorTestContext<'a> {
         for keystroke_text in keystroke_texts.into_iter() {
             self.simulate_keystroke(keystroke_text);
         }
+        // it is common for keyboard shortcuts to kick off async actions, so this ensures that they are complete
+        // before returning.
+        // NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too
+        // quickly races with async actions.
+        if let Foreground::Deterministic { cx_id: _, executor } = self.cx.foreground().as_ref() {
+            executor.run_until_parked();
+        } else {
+            unreachable!();
+        }
+
         keystrokes_under_test_handle
     }
 

crates/file_finder/Cargo.toml πŸ”—

@@ -10,6 +10,7 @@ doctest = false
 
 [dependencies]
 editor = { path = "../editor" }
+collections = { path = "../collections" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
 menu = { path = "../menu" }

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

@@ -1,5 +1,6 @@
+use collections::HashMap;
 use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
-use fuzzy::PathMatch;
+use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
 use gpui::{
     actions, elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext, WeakViewHandle,
 };
@@ -32,38 +33,114 @@ pub struct FileFinderDelegate {
     history_items: Vec<FoundPath>,
 }
 
-#[derive(Debug)]
-enum Matches {
-    History(Vec<FoundPath>),
-    Search(Vec<PathMatch>),
+#[derive(Debug, Default)]
+struct Matches {
+    history: Vec<(FoundPath, Option<PathMatch>)>,
+    search: Vec<PathMatch>,
 }
 
 #[derive(Debug)]
 enum Match<'a> {
-    History(&'a FoundPath),
+    History(&'a FoundPath, Option<&'a PathMatch>),
     Search(&'a PathMatch),
 }
 
 impl Matches {
     fn len(&self) -> usize {
-        match self {
-            Self::History(items) => items.len(),
-            Self::Search(items) => items.len(),
-        }
+        self.history.len() + self.search.len()
     }
 
     fn get(&self, index: usize) -> Option<Match<'_>> {
-        match self {
-            Self::History(items) => items.get(index).map(Match::History),
-            Self::Search(items) => items.get(index).map(Match::Search),
+        if index < self.history.len() {
+            self.history
+                .get(index)
+                .map(|(path, path_match)| Match::History(path, path_match.as_ref()))
+        } else {
+            self.search
+                .get(index - self.history.len())
+                .map(Match::Search)
+        }
+    }
+
+    fn push_new_matches(
+        &mut self,
+        history_items: &Vec<FoundPath>,
+        query: &PathLikeWithPosition<FileSearchQuery>,
+        mut new_search_matches: Vec<PathMatch>,
+        extend_old_matches: bool,
+    ) {
+        let matching_history_paths = matching_history_item_paths(history_items, query);
+        new_search_matches
+            .retain(|path_match| !matching_history_paths.contains_key(&path_match.path));
+        let history_items_to_show = history_items
+            .iter()
+            .filter_map(|history_item| {
+                Some((
+                    history_item.clone(),
+                    Some(
+                        matching_history_paths
+                            .get(&history_item.project.path)?
+                            .clone(),
+                    ),
+                ))
+            })
+            .collect::<Vec<_>>();
+        self.history = history_items_to_show;
+        if extend_old_matches {
+            self.search
+                .retain(|path_match| !matching_history_paths.contains_key(&path_match.path));
+            util::extend_sorted(
+                &mut self.search,
+                new_search_matches.into_iter(),
+                100,
+                |a, b| b.cmp(a),
+            )
+        } else {
+            self.search = new_search_matches;
         }
     }
 }
 
-impl Default for Matches {
-    fn default() -> Self {
-        Self::History(Vec::new())
+fn matching_history_item_paths(
+    history_items: &Vec<FoundPath>,
+    query: &PathLikeWithPosition<FileSearchQuery>,
+) -> HashMap<Arc<Path>, PathMatch> {
+    let history_items_by_worktrees = history_items
+        .iter()
+        .map(|found_path| {
+            let path = &found_path.project.path;
+            let candidate = PathMatchCandidate {
+                path,
+                char_bag: CharBag::from_iter(path.to_string_lossy().to_lowercase().chars()),
+            };
+            (found_path.project.worktree_id, candidate)
+        })
+        .fold(
+            HashMap::default(),
+            |mut candidates, (worktree_id, new_candidate)| {
+                candidates
+                    .entry(worktree_id)
+                    .or_insert_with(Vec::new)
+                    .push(new_candidate);
+                candidates
+            },
+        );
+    let mut matching_history_paths = HashMap::default();
+    for (worktree, candidates) in history_items_by_worktrees {
+        let max_results = candidates.len() + 1;
+        matching_history_paths.extend(
+            fuzzy::match_fixed_path_set(
+                candidates,
+                worktree.to_usize(),
+                query.path_like.path_query(),
+                false,
+                max_results,
+            )
+            .into_iter()
+            .map(|path_match| (Arc::clone(&path_match.path), path_match)),
+        );
     }
+    matching_history_paths
 }
 
 #[derive(Debug, Clone, PartialEq, Eq)]
@@ -81,66 +158,82 @@ impl FoundPath {
 actions!(file_finder, [Toggle]);
 
 pub fn init(cx: &mut AppContext) {
-    cx.add_action(toggle_file_finder);
+    cx.add_action(toggle_or_cycle_file_finder);
     FileFinder::init(cx);
 }
 
 const MAX_RECENT_SELECTIONS: usize = 20;
 
-fn toggle_file_finder(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
-    workspace.toggle_modal(cx, |workspace, cx| {
-        let project = workspace.project().read(cx);
-
-        let currently_opened_path = workspace
-            .active_item(cx)
-            .and_then(|item| item.project_path(cx))
-            .map(|project_path| {
-                let abs_path = project
-                    .worktree_for_id(project_path.worktree_id, cx)
-                    .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path));
-                FoundPath::new(project_path, abs_path)
-            });
-
-        // if exists, bubble the currently opened path to the top
-        let history_items = currently_opened_path
-            .clone()
-            .into_iter()
-            .chain(
-                workspace
-                    .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
+fn toggle_or_cycle_file_finder(
+    workspace: &mut Workspace,
+    _: &Toggle,
+    cx: &mut ViewContext<Workspace>,
+) {
+    match workspace.modal::<FileFinder>() {
+        Some(file_finder) => file_finder.update(cx, |file_finder, cx| {
+            let current_index = file_finder.delegate().selected_index();
+            file_finder.select_next(&menu::SelectNext, cx);
+            let new_index = file_finder.delegate().selected_index();
+            if current_index == new_index {
+                file_finder.select_first(&menu::SelectFirst, cx);
+            }
+        }),
+        None => {
+            workspace.toggle_modal(cx, |workspace, cx| {
+                let project = workspace.project().read(cx);
+
+                let currently_opened_path = workspace
+                    .active_item(cx)
+                    .and_then(|item| item.project_path(cx))
+                    .map(|project_path| {
+                        let abs_path = project
+                            .worktree_for_id(project_path.worktree_id, cx)
+                            .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path));
+                        FoundPath::new(project_path, abs_path)
+                    });
+
+                // if exists, bubble the currently opened path to the top
+                let history_items = currently_opened_path
+                    .clone()
                     .into_iter()
-                    .filter(|(history_path, _)| {
-                        Some(history_path)
-                            != currently_opened_path
-                                .as_ref()
-                                .map(|found_path| &found_path.project)
-                    })
-                    .filter(|(_, history_abs_path)| {
-                        history_abs_path.as_ref()
-                            != currently_opened_path
-                                .as_ref()
-                                .and_then(|found_path| found_path.absolute.as_ref())
-                    })
-                    .map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
-            )
-            .collect();
-
-        let project = workspace.project().clone();
-        let workspace = cx.handle().downgrade();
-        let finder = cx.add_view(|cx| {
-            Picker::new(
-                FileFinderDelegate::new(
-                    workspace,
-                    project,
-                    currently_opened_path,
-                    history_items,
-                    cx,
-                ),
-                cx,
-            )
-        });
-        finder
-    });
+                    .chain(
+                        workspace
+                            .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
+                            .into_iter()
+                            .filter(|(history_path, _)| {
+                                Some(history_path)
+                                    != currently_opened_path
+                                        .as_ref()
+                                        .map(|found_path| &found_path.project)
+                            })
+                            .filter(|(_, history_abs_path)| {
+                                history_abs_path.as_ref()
+                                    != currently_opened_path
+                                        .as_ref()
+                                        .and_then(|found_path| found_path.absolute.as_ref())
+                            })
+                            .map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
+                    )
+                    .collect();
+
+                let project = workspace.project().clone();
+                let workspace = cx.handle().downgrade();
+                let finder = cx.add_view(|cx| {
+                    Picker::new(
+                        FileFinderDelegate::new(
+                            workspace,
+                            project,
+                            currently_opened_path,
+                            history_items,
+                            cx,
+                        ),
+                        cx,
+                    )
+                });
+                finder
+            });
+        }
+    }
 }
 
 pub enum Event {
@@ -255,24 +348,14 @@ impl FileFinderDelegate {
     ) {
         if search_id >= self.latest_search_id {
             self.latest_search_id = search_id;
-            if self.latest_search_did_cancel
+            let extend_old_matches = self.latest_search_did_cancel
                 && Some(query.path_like.path_query())
                     == self
                         .latest_search_query
                         .as_ref()
-                        .map(|query| query.path_like.path_query())
-            {
-                match &mut self.matches {
-                    Matches::History(_) => self.matches = Matches::Search(matches),
-                    Matches::Search(search_matches) => {
-                        util::extend_sorted(search_matches, matches.into_iter(), 100, |a, b| {
-                            b.cmp(a)
-                        })
-                    }
-                }
-            } else {
-                self.matches = Matches::Search(matches);
-            }
+                        .map(|query| query.path_like.path_query());
+            self.matches
+                .push_new_matches(&self.history_items, &query, matches, extend_old_matches);
             self.latest_search_query = Some(query);
             self.latest_search_did_cancel = did_cancel;
             cx.notify();
@@ -286,7 +369,7 @@ impl FileFinderDelegate {
         ix: usize,
     ) -> (String, Vec<usize>, String, Vec<usize>) {
         let (file_name, file_name_positions, full_path, full_path_positions) = match path_match {
-            Match::History(found_path) => {
+            Match::History(found_path, found_path_match) => {
                 let worktree_id = found_path.project.worktree_id;
                 let project_relative_path = &found_path.project.path;
                 let has_worktree = self
@@ -318,14 +401,22 @@ impl FileFinderDelegate {
                         path = Arc::from(absolute_path.as_path());
                     }
                 }
-                self.labels_for_path_match(&PathMatch {
+
+                let mut path_match = PathMatch {
                     score: ix as f64,
                     positions: Vec::new(),
                     worktree_id: worktree_id.to_usize(),
                     path,
                     path_prefix: "".into(),
                     distance_to_relative_ancestor: usize::MAX,
-                })
+                };
+                if let Some(found_path_match) = found_path_match {
+                    path_match
+                        .positions
+                        .extend(found_path_match.positions.iter())
+                }
+
+                self.labels_for_path_match(&path_match)
             }
             Match::Search(path_match) => self.labels_for_path_match(path_match),
         };
@@ -406,8 +497,9 @@ impl PickerDelegate for FileFinderDelegate {
         if raw_query.is_empty() {
             let project = self.project.read(cx);
             self.latest_search_id = post_inc(&mut self.search_count);
-            self.matches = Matches::History(
-                self.history_items
+            self.matches = Matches {
+                history: self
+                    .history_items
                     .iter()
                     .filter(|history_item| {
                         project
@@ -421,8 +513,10 @@ impl PickerDelegate for FileFinderDelegate {
                                     .is_some())
                     })
                     .cloned()
+                    .map(|p| (p, None))
                     .collect(),
-            );
+                search: Vec::new(),
+            };
             cx.notify();
             Task::ready(())
         } else {
@@ -454,7 +548,7 @@ impl PickerDelegate for FileFinderDelegate {
                         }
                     };
                     match m {
-                        Match::History(history_match) => {
+                        Match::History(history_match, _) => {
                             let worktree_id = history_match.project.worktree_id;
                             if workspace
                                 .project()
@@ -866,11 +960,11 @@ mod tests {
 
         finder.update(cx, |finder, cx| {
             let delegate = finder.delegate_mut();
-            let matches = match &delegate.matches {
-                Matches::Search(path_matches) => path_matches,
-                _ => panic!("Search matches expected"),
-            }
-            .clone();
+            assert!(
+                delegate.matches.history.is_empty(),
+                "Search matches expected"
+            );
+            let matches = delegate.matches.search.clone();
 
             // Simulate a search being cancelled after the time limit,
             // returning only a subset of the matches that would have been found.
@@ -893,12 +987,11 @@ mod tests {
                 cx,
             );
 
-            match &delegate.matches {
-                Matches::Search(new_matches) => {
-                    assert_eq!(new_matches.as_slice(), &matches[0..4])
-                }
-                _ => panic!("Search matches expected"),
-            };
+            assert!(
+                delegate.matches.history.is_empty(),
+                "Search matches expected"
+            );
+            assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]);
         });
     }
 
@@ -1006,10 +1099,11 @@ mod tests {
         cx.read(|cx| {
             let finder = finder.read(cx);
             let delegate = finder.delegate();
-            let matches = match &delegate.matches {
-                Matches::Search(path_matches) => path_matches,
-                _ => panic!("Search matches expected"),
-            };
+            assert!(
+                delegate.matches.history.is_empty(),
+                "Search matches expected"
+            );
+            let matches = delegate.matches.search.clone();
             assert_eq!(matches.len(), 1);
 
             let (file_name, file_name_positions, full_path, full_path_positions) =
@@ -1088,10 +1182,11 @@ mod tests {
 
         finder.read_with(cx, |f, _| {
             let delegate = f.delegate();
-            let matches = match &delegate.matches {
-                Matches::Search(path_matches) => path_matches,
-                _ => panic!("Search matches expected"),
-            };
+            assert!(
+                delegate.matches.history.is_empty(),
+                "Search matches expected"
+            );
+            let matches = delegate.matches.search.clone();
             assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt"));
             assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt"));
         });
@@ -1459,6 +1554,255 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_toggle_panel_new_selections(
+        deterministic: Arc<gpui::executor::Deterministic>,
+        cx: &mut gpui::TestAppContext,
+    ) {
+        let app_state = init_test(cx);
+
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(
+                "/src",
+                json!({
+                    "test": {
+                        "first.rs": "// First Rust file",
+                        "second.rs": "// Second Rust file",
+                        "third.rs": "// Third Rust file",
+                    }
+                }),
+            )
+            .await;
+
+        let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
+
+        // generate some history to select from
+        open_close_queried_buffer(
+            "fir",
+            1,
+            "first.rs",
+            window.into(),
+            &workspace,
+            &deterministic,
+            cx,
+        )
+        .await;
+        open_close_queried_buffer(
+            "sec",
+            1,
+            "second.rs",
+            window.into(),
+            &workspace,
+            &deterministic,
+            cx,
+        )
+        .await;
+        open_close_queried_buffer(
+            "thi",
+            1,
+            "third.rs",
+            window.into(),
+            &workspace,
+            &deterministic,
+            cx,
+        )
+        .await;
+        let current_history = open_close_queried_buffer(
+            "sec",
+            1,
+            "second.rs",
+            window.into(),
+            &workspace,
+            &deterministic,
+            cx,
+        )
+        .await;
+
+        for expected_selected_index in 0..current_history.len() {
+            cx.dispatch_action(window.into(), Toggle);
+            let selected_index = cx.read(|cx| {
+                workspace
+                    .read(cx)
+                    .modal::<FileFinder>()
+                    .unwrap()
+                    .read(cx)
+                    .delegate()
+                    .selected_index()
+            });
+            assert_eq!(
+                selected_index, expected_selected_index,
+                "Should select the next item in the history"
+            );
+        }
+
+        cx.dispatch_action(window.into(), Toggle);
+        let selected_index = cx.read(|cx| {
+            workspace
+                .read(cx)
+                .modal::<FileFinder>()
+                .unwrap()
+                .read(cx)
+                .delegate()
+                .selected_index()
+        });
+        assert_eq!(
+            selected_index, 0,
+            "Should wrap around the history and start all over"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_search_preserves_history_items(
+        deterministic: Arc<gpui::executor::Deterministic>,
+        cx: &mut gpui::TestAppContext,
+    ) {
+        let app_state = init_test(cx);
+
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(
+                "/src",
+                json!({
+                    "test": {
+                        "first.rs": "// First Rust file",
+                        "second.rs": "// Second Rust file",
+                        "third.rs": "// Third Rust file",
+                        "fourth.rs": "// Fourth Rust file",
+                    }
+                }),
+            )
+            .await;
+
+        let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
+        let worktree_id = cx.read(|cx| {
+            let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
+            assert_eq!(worktrees.len(), 1,);
+
+            WorktreeId::from_usize(worktrees[0].id())
+        });
+
+        // generate some history to select from
+        open_close_queried_buffer(
+            "fir",
+            1,
+            "first.rs",
+            window.into(),
+            &workspace,
+            &deterministic,
+            cx,
+        )
+        .await;
+        open_close_queried_buffer(
+            "sec",
+            1,
+            "second.rs",
+            window.into(),
+            &workspace,
+            &deterministic,
+            cx,
+        )
+        .await;
+        open_close_queried_buffer(
+            "thi",
+            1,
+            "third.rs",
+            window.into(),
+            &workspace,
+            &deterministic,
+            cx,
+        )
+        .await;
+        open_close_queried_buffer(
+            "sec",
+            1,
+            "second.rs",
+            window.into(),
+            &workspace,
+            &deterministic,
+            cx,
+        )
+        .await;
+
+        cx.dispatch_action(window.into(), Toggle);
+        let first_query = "f";
+        let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
+        finder
+            .update(cx, |finder, cx| {
+                finder
+                    .delegate_mut()
+                    .update_matches(first_query.to_string(), cx)
+            })
+            .await;
+        finder.read_with(cx, |finder, _| {
+            let delegate = finder.delegate();
+            assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
+            let history_match = delegate.matches.history.first().unwrap();
+            assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
+            assert_eq!(history_match.0, FoundPath::new(
+                ProjectPath {
+                    worktree_id,
+                    path: Arc::from(Path::new("test/first.rs")),
+                },
+                Some(PathBuf::from("/src/test/first.rs"))
+            ));
+            assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
+            assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs"));
+        });
+
+        let second_query = "fsdasdsa";
+        let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
+        finder
+            .update(cx, |finder, cx| {
+                finder
+                    .delegate_mut()
+                    .update_matches(second_query.to_string(), cx)
+            })
+            .await;
+        finder.read_with(cx, |finder, _| {
+            let delegate = finder.delegate();
+            assert!(
+                delegate.matches.history.is_empty(),
+                "No history entries should match {second_query}"
+            );
+            assert!(
+                delegate.matches.search.is_empty(),
+                "No search entries should match {second_query}"
+            );
+        });
+
+        let first_query_again = first_query;
+        let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
+        finder
+            .update(cx, |finder, cx| {
+                finder
+                    .delegate_mut()
+                    .update_matches(first_query_again.to_string(), cx)
+            })
+            .await;
+        finder.read_with(cx, |finder, _| {
+            let delegate = finder.delegate();
+            assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query");
+            let history_match = delegate.matches.history.first().unwrap();
+            assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
+            assert_eq!(history_match.0, FoundPath::new(
+                ProjectPath {
+                    worktree_id,
+                    path: Arc::from(Path::new("test/first.rs")),
+                },
+                Some(PathBuf::from("/src/test/first.rs"))
+            ));
+            assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
+            assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs"));
+        });
+    }
+
     async fn open_close_queried_buffer(
         input: &str,
         expected_matches: usize,
@@ -1528,13 +1872,8 @@ mod tests {
         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
         active_pane
             .update(cx, |pane, cx| {
-                pane.close_active_item(
-                    &workspace::CloseActiveItem {
-                        save_behavior: None,
-                    },
-                    cx,
-                )
-                .unwrap()
+                pane.close_active_item(&workspace::CloseActiveItem { save_intent: None }, cx)
+                    .unwrap()
             })
             .await
             .unwrap();

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

@@ -507,7 +507,7 @@ impl FakeFs {
         state.emit_event(&[path]);
     }
 
-    fn write_file_internal(&self, path: impl AsRef<Path>, content: String) -> Result<()> {
+    pub fn write_file_internal(&self, path: impl AsRef<Path>, content: String) -> Result<()> {
         let mut state = self.state.lock();
         let path = path.as_ref();
         let inode = state.next_inode;

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

@@ -4,5 +4,7 @@ mod paths;
 mod strings;
 
 pub use char_bag::CharBag;
-pub use paths::{match_path_sets, PathMatch, PathMatchCandidate, PathMatchCandidateSet};
+pub use paths::{
+    match_fixed_path_set, match_path_sets, PathMatch, PathMatchCandidate, PathMatchCandidateSet,
+};
 pub use strings::{match_strings, StringMatch, StringMatchCandidate};

crates/fuzzy/src/paths.rs πŸ”—

@@ -90,6 +90,44 @@ impl Ord for PathMatch {
     }
 }
 
+pub fn match_fixed_path_set(
+    candidates: Vec<PathMatchCandidate>,
+    worktree_id: usize,
+    query: &str,
+    smart_case: bool,
+    max_results: usize,
+) -> Vec<PathMatch> {
+    let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
+    let query = query.chars().collect::<Vec<_>>();
+    let query_char_bag = CharBag::from(&lowercase_query[..]);
+
+    let mut matcher = Matcher::new(
+        &query,
+        &lowercase_query,
+        query_char_bag,
+        smart_case,
+        max_results,
+    );
+
+    let mut results = Vec::new();
+    matcher.match_candidates(
+        &[],
+        &[],
+        candidates.into_iter(),
+        &mut results,
+        &AtomicBool::new(false),
+        |candidate, score| PathMatch {
+            score,
+            worktree_id,
+            positions: Vec::new(),
+            path: candidate.path.clone(),
+            path_prefix: Arc::from(""),
+            distance_to_relative_ancestor: usize::MAX,
+        },
+    );
+    results
+}
+
 pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
     candidate_sets: &'a [Set],
     query: &str,

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

@@ -33,6 +33,7 @@ use std::{
     any::{type_name, Any, TypeId},
     mem,
     ops::{Deref, DerefMut, Range, Sub},
+    sync::Arc,
 };
 use taffy::{
     tree::{Measurable, MeasureFunc},
@@ -56,7 +57,7 @@ pub struct Window {
     pub(crate) rendered_views: HashMap<usize, Box<dyn AnyRootElement>>,
     scene: SceneBuilder,
     pub(crate) text_style_stack: Vec<TextStyle>,
-    pub(crate) theme_stack: Vec<Box<dyn Any>>,
+    pub(crate) theme_stack: Vec<Arc<dyn Any + Send + Sync>>,
     pub(crate) new_parents: HashMap<usize, usize>,
     pub(crate) views_to_notify_if_ancestors_change: HashMap<usize, SmallVec<[usize; 2]>>,
     titlebar_height: f32,
@@ -1336,18 +1337,21 @@ impl<'a> WindowContext<'a> {
         self.window.text_style_stack.pop();
     }
 
-    pub fn theme<T: 'static>(&self) -> &T {
+    pub fn theme<T: 'static + Send + Sync>(&self) -> Arc<T> {
         self.window
             .theme_stack
             .iter()
             .rev()
-            .find_map(|theme| theme.downcast_ref())
+            .find_map(|theme| {
+                let entry = Arc::clone(theme);
+                entry.downcast::<T>().ok()
+            })
             .ok_or_else(|| anyhow!("no theme provided of type {}", type_name::<T>()))
             .unwrap()
     }
 
-    pub fn push_theme<T: 'static>(&mut self, theme: T) {
-        self.window.theme_stack.push(Box::new(theme));
+    pub fn push_theme<T: 'static + Send + Sync>(&mut self, theme: T) {
+        self.window.theme_stack.push(Arc::new(theme));
     }
 
     pub fn pop_theme(&mut self) {

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

@@ -98,7 +98,12 @@ impl FontCache {
         }
 
         Err(anyhow!(
-            "could not find a non-empty font family matching one of the given names"
+            "could not find a non-empty font family matching one of the given names: {}",
+            names
+                .iter()
+                .map(|name| format!("`{name}`"))
+                .collect::<Vec<_>>()
+                .join(", ")
         ))
     }
 

crates/gpui2/src/style.rs πŸ”—

@@ -320,174 +320,114 @@ use crate as gpui2;
 //
 // Example:
 // // Sets the padding to 0.5rem, just like class="p-2" in Tailwind.
-// fn p_2(mut self) -> Self where Self: Sized;
-pub trait StyleHelpers: Styleable<Style = Style> {
+// fn p_2(mut self) -> Self;
+pub trait StyleHelpers: Sized + Styleable<Style = Style> {
     styleable_helpers!();
 
-    fn full(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn full(mut self) -> Self {
         self.declared_style().size.width = Some(relative(1.).into());
         self.declared_style().size.height = Some(relative(1.).into());
         self
     }
 
-    fn relative(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn relative(mut self) -> Self {
         self.declared_style().position = Some(Position::Relative);
         self
     }
 
-    fn absolute(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn absolute(mut self) -> Self {
         self.declared_style().position = Some(Position::Absolute);
         self
     }
 
-    fn block(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn block(mut self) -> Self {
         self.declared_style().display = Some(Display::Block);
         self
     }
 
-    fn flex(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex(mut self) -> Self {
         self.declared_style().display = Some(Display::Flex);
         self
     }
 
-    fn flex_col(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex_col(mut self) -> Self {
         self.declared_style().flex_direction = Some(FlexDirection::Column);
         self
     }
 
-    fn flex_row(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex_row(mut self) -> Self {
         self.declared_style().flex_direction = Some(FlexDirection::Row);
         self
     }
 
-    fn flex_1(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex_1(mut self) -> Self {
         self.declared_style().flex_grow = Some(1.);
         self.declared_style().flex_shrink = Some(1.);
         self.declared_style().flex_basis = Some(relative(0.).into());
         self
     }
 
-    fn flex_auto(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex_auto(mut self) -> Self {
         self.declared_style().flex_grow = Some(1.);
         self.declared_style().flex_shrink = Some(1.);
         self.declared_style().flex_basis = Some(Length::Auto);
         self
     }
 
-    fn flex_initial(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex_initial(mut self) -> Self {
         self.declared_style().flex_grow = Some(0.);
         self.declared_style().flex_shrink = Some(1.);
         self.declared_style().flex_basis = Some(Length::Auto);
         self
     }
 
-    fn flex_none(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex_none(mut self) -> Self {
         self.declared_style().flex_grow = Some(0.);
         self.declared_style().flex_shrink = Some(0.);
         self
     }
 
-    fn grow(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn grow(mut self) -> Self {
         self.declared_style().flex_grow = Some(1.);
         self
     }
 
-    fn items_start(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn items_start(mut self) -> Self {
         self.declared_style().align_items = Some(AlignItems::FlexStart);
         self
     }
 
-    fn items_end(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn items_end(mut self) -> Self {
         self.declared_style().align_items = Some(AlignItems::FlexEnd);
         self
     }
 
-    fn items_center(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn items_center(mut self) -> Self {
         self.declared_style().align_items = Some(AlignItems::Center);
         self
     }
 
-    fn justify_between(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn justify_between(mut self) -> Self {
         self.declared_style().justify_content = Some(JustifyContent::SpaceBetween);
         self
     }
 
-    fn justify_center(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn justify_center(mut self) -> Self {
         self.declared_style().justify_content = Some(JustifyContent::Center);
         self
     }
 
-    fn justify_start(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn justify_start(mut self) -> Self {
         self.declared_style().justify_content = Some(JustifyContent::Start);
         self
     }
 
-    fn justify_end(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn justify_end(mut self) -> Self {
         self.declared_style().justify_content = Some(JustifyContent::End);
         self
     }
 
-    fn justify_around(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn justify_around(mut self) -> Self {
         self.declared_style().justify_content = Some(JustifyContent::SpaceAround);
         self
     }
@@ -495,7 +435,6 @@ pub trait StyleHelpers: Styleable<Style = Style> {
     fn fill<F>(mut self, fill: F) -> Self
     where
         F: Into<Fill>,
-        Self: Sized,
     {
         self.declared_style().fill = Some(fill.into());
         self
@@ -504,7 +443,6 @@ pub trait StyleHelpers: Styleable<Style = Style> {
     fn border_color<C>(mut self, border_color: C) -> Self
     where
         C: Into<Hsla>,
-        Self: Sized,
     {
         self.declared_style().border_color = Some(border_color.into());
         self
@@ -513,72 +451,47 @@ pub trait StyleHelpers: Styleable<Style = Style> {
     fn text_color<C>(mut self, color: C) -> Self
     where
         C: Into<Hsla>,
-        Self: Sized,
     {
         self.declared_style().text_color = Some(color.into());
         self
     }
 
-    fn text_xs(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_xs(mut self) -> Self {
         self.declared_style().font_size = Some(0.75);
         self
     }
 
-    fn text_sm(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_sm(mut self) -> Self {
         self.declared_style().font_size = Some(0.875);
         self
     }
 
-    fn text_base(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_base(mut self) -> Self {
         self.declared_style().font_size = Some(1.0);
         self
     }
 
-    fn text_lg(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_lg(mut self) -> Self {
         self.declared_style().font_size = Some(1.125);
         self
     }
 
-    fn text_xl(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_xl(mut self) -> Self {
         self.declared_style().font_size = Some(1.25);
         self
     }
 
-    fn text_2xl(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_2xl(mut self) -> Self {
         self.declared_style().font_size = Some(1.5);
         self
     }
 
-    fn text_3xl(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_3xl(mut self) -> Self {
         self.declared_style().font_size = Some(1.875);
         self
     }
 
-    fn font(mut self, family_name: impl Into<Arc<str>>) -> Self
-    where
-        Self: Sized,
-    {
+    fn font(mut self, family_name: impl Into<Arc<str>>) -> Self {
         self.declared_style().font_family = Some(family_name.into());
         self
     }

crates/language_tools/src/lsp_log.rs πŸ”—

@@ -8,8 +8,8 @@ use gpui::{
         ParentElement, Stack,
     },
     platform::{CursorStyle, MouseButton},
-    AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, View, ViewContext,
-    ViewHandle, WeakModelHandle,
+    AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, Subscription, View,
+    ViewContext, ViewHandle, WeakModelHandle,
 };
 use language::{Buffer, LanguageServerId, LanguageServerName};
 use lsp::IoKind;
@@ -53,10 +53,12 @@ pub struct LspLogView {
     current_server_id: Option<LanguageServerId>,
     is_showing_rpc_trace: bool,
     project: ModelHandle<Project>,
+    _log_store_subscription: Subscription,
 }
 
 pub struct LspLogToolbarItemView {
     log_view: Option<ViewHandle<LspLogView>>,
+    _log_view_subscription: Option<Subscription>,
     menu_open: bool,
 }
 
@@ -181,6 +183,13 @@ impl LogStore {
         });
 
         let server = project.read(cx).language_server_for_id(id);
+        if let Some(server) = server.as_deref() {
+            if server.has_notification_handler::<lsp::notification::LogMessage>() {
+                // Another event wants to re-add the server that was already added and subscribed to, avoid doing it again.
+                return Some(server_state.log_buffer.clone());
+            }
+        }
+
         let weak_project = project.downgrade();
         let io_tx = self.io_tx.clone();
         server_state._io_logs_subscription = server.as_ref().map(|server| {
@@ -366,12 +375,49 @@ impl LspLogView {
             .get(&project.downgrade())
             .and_then(|project| project.servers.keys().copied().next());
         let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""));
+        let _log_store_subscription = cx.observe(&log_store, |this, store, cx| {
+            (|| -> Option<()> {
+                let project_state = store.read(cx).projects.get(&this.project.downgrade())?;
+                if let Some(current_lsp) = this.current_server_id {
+                    if !project_state.servers.contains_key(&current_lsp) {
+                        if let Some(server) = project_state.servers.iter().next() {
+                            if this.is_showing_rpc_trace {
+                                this.show_rpc_trace_for_server(*server.0, cx)
+                            } else {
+                                this.show_logs_for_server(*server.0, cx)
+                            }
+                        } else {
+                            this.current_server_id = None;
+                            this.editor.update(cx, |editor, cx| {
+                                editor.set_read_only(false);
+                                editor.clear(cx);
+                                editor.set_read_only(true);
+                            });
+                            cx.notify();
+                        }
+                    }
+                } else {
+                    if let Some(server) = project_state.servers.iter().next() {
+                        if this.is_showing_rpc_trace {
+                            this.show_rpc_trace_for_server(*server.0, cx)
+                        } else {
+                            this.show_logs_for_server(*server.0, cx)
+                        }
+                    }
+                }
+
+                Some(())
+            })();
+
+            cx.notify();
+        });
         let mut this = Self {
             editor: Self::editor_for_buffer(project.clone(), buffer, cx),
             project,
             log_store,
             current_server_id: None,
             is_showing_rpc_trace: false,
+            _log_store_subscription,
         };
         if let Some(server_id) = server_id {
             this.show_logs_for_server(server_id, cx);
@@ -594,18 +640,22 @@ impl ToolbarItemView for LspLogToolbarItemView {
     fn set_active_pane_item(
         &mut self,
         active_pane_item: Option<&dyn ItemHandle>,
-        _: &mut ViewContext<Self>,
+        cx: &mut ViewContext<Self>,
     ) -> workspace::ToolbarItemLocation {
         self.menu_open = false;
         if let Some(item) = active_pane_item {
             if let Some(log_view) = item.downcast::<LspLogView>() {
                 self.log_view = Some(log_view.clone());
+                self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| {
+                    cx.notify();
+                }));
                 return ToolbarItemLocation::PrimaryLeft {
                     flex: Some((1., false)),
                 };
             }
         }
         self.log_view = None;
+        self._log_view_subscription = None;
         ToolbarItemLocation::Hidden
     }
 }
@@ -736,6 +786,7 @@ impl LspLogToolbarItemView {
         Self {
             menu_open: false,
             log_view: None,
+            _log_view_subscription: None,
         }
     }
 

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

@@ -605,6 +605,10 @@ impl LanguageServer {
         self.notification_handlers.lock().remove(T::METHOD);
     }
 
+    pub fn has_notification_handler<T: notification::Notification>(&self) -> bool {
+        self.notification_handlers.lock().contains_key(T::METHOD)
+    }
+
     #[must_use]
     pub fn on_custom_notification<Params, F>(&self, method: &'static str, mut f: F) -> Subscription
     where
@@ -712,11 +716,11 @@ impl LanguageServer {
         }
     }
 
-    pub fn name<'a>(self: &'a Arc<Self>) -> &'a str {
+    pub fn name(&self) -> &str {
         &self.name
     }
 
-    pub fn capabilities<'a>(self: &'a Arc<Self>) -> &'a ServerCapabilities {
+    pub fn capabilities(&self) -> &ServerCapabilities {
         &self.capabilities
     }
 

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

@@ -25,7 +25,8 @@ pub struct Picker<D: PickerDelegate> {
     max_size: Vector2F,
     theme: Arc<Mutex<Box<dyn Fn(&theme::Theme) -> theme::Picker>>>,
     confirmed: bool,
-    pending_update_matches: Task<Option<()>>,
+    pending_update_matches: Option<Task<Option<()>>>,
+    confirm_on_update: Option<bool>,
     has_focus: bool,
 }
 
@@ -208,7 +209,8 @@ impl<D: PickerDelegate> Picker<D> {
             max_size: vec2f(540., 420.),
             theme,
             confirmed: false,
-            pending_update_matches: Task::ready(None),
+            pending_update_matches: None,
+            confirm_on_update: None,
             has_focus: false,
         };
         this.update_matches(String::new(), cx);
@@ -263,11 +265,13 @@ impl<D: PickerDelegate> Picker<D> {
     pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
         let update = self.delegate.update_matches(query, cx);
         self.matches_updated(cx);
-        self.pending_update_matches = cx.spawn(|this, mut cx| async move {
+        self.pending_update_matches = Some(cx.spawn(|this, mut cx| async move {
             update.await;
-            this.update(&mut cx, |this, cx| this.matches_updated(cx))
-                .log_err()
-        });
+            this.update(&mut cx, |this, cx| {
+                this.matches_updated(cx);
+            })
+            .log_err()
+        }));
     }
 
     fn matches_updated(&mut self, cx: &mut ViewContext<Self>) {
@@ -278,6 +282,11 @@ impl<D: PickerDelegate> Picker<D> {
             ScrollTarget::Show(index)
         };
         self.list_state.scroll_to(target);
+        self.pending_update_matches = None;
+        if let Some(secondary) = self.confirm_on_update.take() {
+            self.confirmed = true;
+            self.delegate.confirm(secondary, cx)
+        }
         cx.notify();
     }
 
@@ -331,13 +340,21 @@ impl<D: PickerDelegate> Picker<D> {
     }
 
     pub fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
-        self.confirmed = true;
-        self.delegate.confirm(false, cx);
+        if self.pending_update_matches.is_some() {
+            self.confirm_on_update = Some(false)
+        } else {
+            self.confirmed = true;
+            self.delegate.confirm(false, cx);
+        }
     }
 
     pub fn secondary_confirm(&mut self, _: &SecondaryConfirm, cx: &mut ViewContext<Self>) {
-        self.confirmed = true;
-        self.delegate.confirm(true, cx);
+        if self.pending_update_matches.is_some() {
+            self.confirm_on_update = Some(true)
+        } else {
+            self.confirmed = true;
+            self.delegate.confirm(true, cx);
+        }
     }
 
     fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {

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

@@ -2273,11 +2273,13 @@ impl Project {
                 };
 
                 for (_, _, server) in self.language_servers_for_worktree(worktree_id) {
+                    let text = include_text(server.as_ref()).then(|| buffer.read(cx).text());
+
                     server
                         .notify::<lsp::notification::DidSaveTextDocument>(
                             lsp::DidSaveTextDocumentParams {
                                 text_document: text_document.clone(),
-                                text: None,
+                                text,
                             },
                         )
                         .log_err();
@@ -8034,24 +8036,27 @@ fn subscribe_for_copilot_events(
             copilot::Event::CopilotLanguageServerStarted => {
                 match copilot.read(cx).language_server() {
                     Some((name, copilot_server)) => {
-                        let new_server_id = copilot_server.server_id();
-                        let weak_project = cx.weak_handle();
-                        let copilot_log_subscription = copilot_server
-                            .on_notification::<copilot::request::LogMessage, _>(
-                                move |params, mut cx| {
-                                    if let Some(project) = weak_project.upgrade(&mut cx) {
-                                        project.update(&mut cx, |_, cx| {
-                                            cx.emit(Event::LanguageServerLog(
-                                                new_server_id,
-                                                params.message,
-                                            ));
-                                        })
-                                    }
-                                },
-                            );
-                        project.supplementary_language_servers.insert(new_server_id, (name.clone(), Arc::clone(copilot_server)));
-                        project.copilot_log_subscription = Some(copilot_log_subscription);
-                        cx.emit(Event::LanguageServerAdded(new_server_id));
+                        // Another event wants to re-add the server that was already added and subscribed to, avoid doing it again.
+                        if !copilot_server.has_notification_handler::<copilot::request::LogMessage>() {
+                            let new_server_id = copilot_server.server_id();
+                            let weak_project = cx.weak_handle();
+                            let copilot_log_subscription = copilot_server
+                                .on_notification::<copilot::request::LogMessage, _>(
+                                    move |params, mut cx| {
+                                        if let Some(project) = weak_project.upgrade(&mut cx) {
+                                            project.update(&mut cx, |_, cx| {
+                                                cx.emit(Event::LanguageServerLog(
+                                                    new_server_id,
+                                                    params.message,
+                                                ));
+                                            })
+                                        }
+                                    },
+                                );
+                            project.supplementary_language_servers.insert(new_server_id, (name.clone(), Arc::clone(copilot_server)));
+                            project.copilot_log_subscription = Some(copilot_log_subscription);
+                            cx.emit(Event::LanguageServerAdded(new_server_id));
+                        }
                     }
                     None => debug_panic!("Received Copilot language server started event, but no language server is running"),
                 }
@@ -8308,3 +8313,19 @@ async fn wait_for_loading_buffer(
         receiver.next().await;
     }
 }
+
+fn include_text(server: &lsp::LanguageServer) -> bool {
+    server
+        .capabilities()
+        .text_document_sync
+        .as_ref()
+        .and_then(|sync| match sync {
+            lsp::TextDocumentSyncCapability::Kind(_) => None,
+            lsp::TextDocumentSyncCapability::Options(options) => options.save.as_ref(),
+        })
+        .and_then(|save_options| match save_options {
+            lsp::TextDocumentSyncSaveOptions::Supported(_) => None,
+            lsp::TextDocumentSyncSaveOptions::SaveOptions(options) => options.include_text,
+        })
+        .unwrap_or(false)
+}

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

@@ -69,7 +69,7 @@ impl ProjectSymbolsDelegate {
             &self.external_match_candidates,
             query,
             false,
-            MAX_MATCHES - visible_matches.len(),
+            MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
             &Default::default(),
             cx.background().clone(),
         ));

crates/quick_action_bar/Cargo.toml πŸ”—

@@ -9,7 +9,7 @@ path = "src/quick_action_bar.rs"
 doctest = false
 
 [dependencies]
-ai = { path = "../ai" }
+assistant = { path = "../assistant" }
 editor = { path = "../editor" }
 gpui = { path = "../gpui" }
 search = { path = "../search" }

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

@@ -1,4 +1,4 @@
-use ai::{assistant::InlineAssist, AssistantPanel};
+use assistant::{assistant_panel::InlineAssist, AssistantPanel};
 use editor::Editor;
 use gpui::{
     elements::{Empty, Flex, MouseEventHandler, ParentElement, Svg},
@@ -48,24 +48,26 @@ impl View for QuickActionBar {
             return Empty::new().into_any();
         };
 
-        let inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
-        let mut bar = Flex::row().with_child(render_quick_action_bar_button(
-            0,
-            "icons/inlay_hint.svg",
-            inlay_hints_enabled,
-            (
-                "Toggle Inlay Hints".to_string(),
-                Some(Box::new(editor::ToggleInlayHints)),
-            ),
-            cx,
-            |this, cx| {
-                if let Some(editor) = this.active_editor() {
-                    editor.update(cx, |editor, cx| {
-                        editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx);
-                    });
-                }
-            },
-        ));
+        let mut bar = Flex::row();
+        if editor.read(cx).supports_inlay_hints(cx) {
+            bar = bar.with_child(render_quick_action_bar_button(
+                0,
+                "icons/inlay_hint.svg",
+                editor.read(cx).inlay_hints_enabled(),
+                (
+                    "Toggle Inlay Hints".to_string(),
+                    Some(Box::new(editor::ToggleInlayHints)),
+                ),
+                cx,
+                |this, cx| {
+                    if let Some(editor) = this.active_editor() {
+                        editor.update(cx, |editor, cx| {
+                            editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx);
+                        });
+                    }
+                },
+            ));
+        }
 
         if editor.read(cx).buffer().read(cx).is_singleton() {
             let search_bar_shown = !self.buffer_search_bar.read(cx).is_dismissed();
@@ -163,12 +165,18 @@ impl ToolbarItemView for QuickActionBar {
 
                 if let Some(editor) = active_item.downcast::<Editor>() {
                     let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
+                    let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
                     self._inlay_hints_enabled_subscription =
                         Some(cx.observe(&editor, move |_, editor, cx| {
-                            let new_inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
-                            if inlay_hints_enabled != new_inlay_hints_enabled {
-                                inlay_hints_enabled = new_inlay_hints_enabled;
-                                cx.notify();
+                            let editor = editor.read(cx);
+                            let new_inlay_hints_enabled = editor.inlay_hints_enabled();
+                            let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
+                            let should_notify = inlay_hints_enabled != new_inlay_hints_enabled
+                                || supports_inlay_hints != new_supports_inlay_hints;
+                            inlay_hints_enabled = new_inlay_hints_enabled;
+                            supports_inlay_hints = new_supports_inlay_hints;
+                            if should_notify {
+                                cx.notify()
                             }
                         }));
                     ToolbarItemLocation::PrimaryRight { flex: None }

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

@@ -347,9 +347,9 @@ impl View for BufferSearchBar {
 
         Flex::row()
             .with_child(query_column)
+            .with_child(mode_column)
             .with_children(switches_column)
             .with_children(replacement)
-            .with_child(mode_column)
             .with_child(nav_column)
             .contained()
             .with_style(theme.search.container)
@@ -539,6 +539,23 @@ impl BufferSearchBar {
             .map(|searchable_item| searchable_item.query_suggestion(cx))
     }
 
+    pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext<Self>) {
+        if replacement.is_none() {
+            self.replace_enabled = false;
+            return;
+        }
+        self.replace_enabled = true;
+        self.replacement_editor
+            .update(cx, |replacement_editor, cx| {
+                replacement_editor
+                    .buffer()
+                    .update(cx, |replacement_buffer, cx| {
+                        let len = replacement_buffer.len(cx);
+                        replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx);
+                    });
+            });
+    }
+
     pub fn search(
         &mut self,
         query: &str,
@@ -679,6 +696,22 @@ impl BufferSearchBar {
         }
     }
 
+    pub fn select_last_match(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(searchable_item) = self.active_searchable_item.as_ref() {
+            if let Some(matches) = self
+                .searchable_items_with_matches
+                .get(&searchable_item.downgrade())
+            {
+                if matches.len() == 0 {
+                    return;
+                }
+                let new_match_index = matches.len() - 1;
+                searchable_item.update_matches(matches, cx);
+                searchable_item.activate_match(new_match_index, matches, cx);
+            }
+        }
+    }
+
     fn select_next_match_on_pane(
         pane: &mut Pane,
         action: &SelectNextMatch,
@@ -946,7 +979,7 @@ impl BufferSearchBar {
             cx.propagate_action();
         }
     }
-    fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
+    pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
         if !self.dismissed && self.active_search.is_some() {
             if let Some(searchable_item) = self.active_searchable_item.as_ref() {
                 if let Some(query) = self.active_search.as_ref() {

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

@@ -60,7 +60,7 @@ pub fn init(cx: &mut AppContext) {
     cx.set_global(ActiveSettings::default());
     cx.add_action(ProjectSearchView::deploy);
     cx.add_action(ProjectSearchView::move_focus_to_results);
-    cx.add_action(ProjectSearchBar::search);
+    cx.add_action(ProjectSearchBar::confirm);
     cx.add_action(ProjectSearchBar::search_in_new);
     cx.add_action(ProjectSearchBar::select_next_match);
     cx.add_action(ProjectSearchBar::select_prev_match);
@@ -330,7 +330,7 @@ impl View for ProjectSearchView {
             // If Text -> Major: "Text search all files and folders", Minor: {...}
 
             let current_mode = self.current_mode;
-            let major_text = if model.pending_search.is_some() {
+            let mut major_text = if model.pending_search.is_some() {
                 Cow::Borrowed("Searching...")
             } else if model.no_results.is_some_and(|v| v) {
                 Cow::Borrowed("No Results")
@@ -344,9 +344,18 @@ impl View for ProjectSearchView {
                 }
             };
 
+            let mut show_minor_text = true;
             let semantic_status = self.semantic_state.as_ref().and_then(|semantic| {
                 let status = semantic.index_status;
                 match status {
+                    SemanticIndexStatus::NotAuthenticated => {
+                        major_text = Cow::Borrowed("Not Authenticated");
+                        show_minor_text = false;
+                        Some(
+                            "API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables"
+                                .to_string(),
+                        )
+                    }
                     SemanticIndexStatus::Indexed => Some("Indexing complete".to_string()),
                     SemanticIndexStatus::Indexing {
                         remaining_files,
@@ -388,10 +397,13 @@ impl View for ProjectSearchView {
                         let mut minor_text = Vec::new();
                         minor_text.push("".into());
                         minor_text.extend(semantic_status);
-                        minor_text.push("Simply explain the code you are looking to find.".into());
-                        minor_text.push(
-                            "ex. 'prompt user for permissions to index their project'".into(),
-                        );
+                        if show_minor_text {
+                            minor_text
+                                .push("Simply explain the code you are looking to find.".into());
+                            minor_text.push(
+                                "ex. 'prompt user for permissions to index their project'".into(),
+                            );
+                        }
                         minor_text
                     }
                     _ => vec![
@@ -1359,9 +1371,18 @@ impl ProjectSearchBar {
             })
         }
     }
-    fn search(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+        let mut should_propagate = true;
         if let Some(search_view) = self.active_project_search.as_ref() {
-            search_view.update(cx, |search_view, cx| search_view.search(cx));
+            search_view.update(cx, |search_view, cx| {
+                if !search_view.replacement_editor.is_focused(cx) {
+                    should_propagate = false;
+                    search_view.search(cx);
+                }
+            });
+        }
+        if should_propagate {
+            cx.propagate_action();
         }
     }
 
@@ -1666,6 +1687,28 @@ impl View for ProjectSearchBar {
         "ProjectSearchBar"
     }
 
+    fn update_keymap_context(
+        &self,
+        keymap: &mut gpui::keymap_matcher::KeymapContext,
+        cx: &AppContext,
+    ) {
+        Self::reset_to_default_keymap_context(keymap);
+        let in_replace = self
+            .active_project_search
+            .as_ref()
+            .map(|search| {
+                search
+                    .read(cx)
+                    .replacement_editor
+                    .read_with(cx, |_, cx| cx.is_self_focused())
+            })
+            .flatten()
+            .unwrap_or(false);
+        if in_replace {
+            keymap.add_identifier("in_replace");
+        }
+    }
+
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
         if let Some(_search) = self.active_project_search.as_ref() {
             let search = _search.read(cx);
@@ -1915,9 +1958,9 @@ impl View for ProjectSearchBar {
 
             Flex::row()
                 .with_child(query_column)
+                .with_child(mode_column)
                 .with_child(switches_column)
                 .with_children(replacement)
-                .with_child(mode_column)
                 .with_child(nav_column)
                 .contained()
                 .with_style(theme.search.container)

crates/semantic_index/Cargo.toml πŸ”—

@@ -9,6 +9,7 @@ path = "src/semantic_index.rs"
 doctest = false
 
 [dependencies]
+ai = { path = "../ai" }
 collections = { path = "../collections" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
@@ -26,22 +27,19 @@ futures.workspace = true
 ordered-float.workspace = true
 smol.workspace = true
 rusqlite = { version = "0.27.0", features = ["blob", "array", "modern_sqlite"] }
-isahc.workspace = true
 log.workspace = true
 tree-sitter.workspace = true
 lazy_static.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 async-trait.workspace = true
-bincode = "1.3.3"
-matrixmultiply = "0.3.7"
 tiktoken-rs = "0.5.0"
 parking_lot.workspace = true
 rand.workspace = true
 schemars.workspace = true
 globset.workspace = true
 sha1 = "0.10.5"
-parse_duration = "2.1.1"
+ndarray = { version = "0.15.0" }
 
 [dev-dependencies]
 collections = { path = "../collections", features = ["test-support"] }

crates/semantic_index/examples/eval.rs πŸ”—

@@ -1,10 +1,10 @@
+use ai::embedding::OpenAIEmbeddings;
 use anyhow::{anyhow, Result};
 use client::{self, UserStore};
 use gpui::{AsyncAppContext, ModelHandle, Task};
 use language::LanguageRegistry;
 use node_runtime::RealNodeRuntime;
 use project::{Project, RealFs};
-use semantic_index::embedding::OpenAIEmbeddings;
 use semantic_index::semantic_index_settings::SemanticIndexSettings;
 use semantic_index::{SearchResult, SemanticIndex};
 use serde::{Deserialize, Serialize};
@@ -456,7 +456,7 @@ fn main() {
         let languages = Arc::new(languages);
 
         let node_runtime = RealNodeRuntime::new(http.clone());
-        languages::init(languages.clone(), node_runtime.clone());
+        languages::init(languages.clone(), node_runtime.clone(), cx);
         language::init(cx);
 
         project::Project::init(&client, cx);

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

@@ -1,19 +1,19 @@
 use crate::{
-    embedding::Embedding,
     parsing::{Span, SpanDigest},
     SEMANTIC_INDEX_VERSION,
 };
+use ai::embedding::Embedding;
 use anyhow::{anyhow, Context, Result};
 use collections::HashMap;
 use futures::channel::oneshot;
 use gpui::executor;
+use ndarray::{Array1, Array2};
 use ordered_float::OrderedFloat;
 use project::{search::PathMatcher, Fs};
 use rpc::proto::Timestamp;
 use rusqlite::params;
 use rusqlite::types::Value;
 use std::{
-    cmp::Reverse,
     future::Future,
     ops::Range,
     path::{Path, PathBuf},
@@ -23,6 +23,13 @@ use std::{
 };
 use util::TryFutureExt;
 
+pub fn argsort<T: Ord>(data: &[T]) -> Vec<usize> {
+    let mut indices = (0..data.len()).collect::<Vec<_>>();
+    indices.sort_by_key(|&i| &data[i]);
+    indices.reverse();
+    indices
+}
+
 #[derive(Debug)]
 pub struct FileRecord {
     pub id: usize,
@@ -409,23 +416,91 @@ impl VectorDatabase {
         limit: usize,
         file_ids: &[i64],
     ) -> impl Future<Output = Result<Vec<(i64, OrderedFloat<f32>)>>> {
-        let query_embedding = query_embedding.clone();
         let file_ids = file_ids.to_vec();
+        let query = query_embedding.clone().0;
+        let query = Array1::from_vec(query);
         self.transact(move |db| {
-            let mut results = Vec::<(i64, OrderedFloat<f32>)>::with_capacity(limit + 1);
-            Self::for_each_span(db, &file_ids, |id, embedding| {
-                let similarity = embedding.similarity(&query_embedding);
-                let ix = match results
-                    .binary_search_by_key(&Reverse(similarity), |(_, s)| Reverse(*s))
-                {
-                    Ok(ix) => ix,
-                    Err(ix) => ix,
-                };
-                results.insert(ix, (id, similarity));
-                results.truncate(limit);
-            })?;
+            let mut query_statement = db.prepare(
+                "
+                    SELECT
+                        id, embedding
+                    FROM
+                        spans
+                    WHERE
+                        file_id IN rarray(?)
+                    ",
+            )?;
+
+            let deserialized_rows = query_statement
+                .query_map(params![ids_to_sql(&file_ids)], |row| {
+                    Ok((row.get::<_, usize>(0)?, row.get::<_, Embedding>(1)?))
+                })?
+                .filter_map(|row| row.ok())
+                .collect::<Vec<(usize, Embedding)>>();
+
+            if deserialized_rows.len() == 0 {
+                return Ok(Vec::new());
+            }
+
+            // Get Length of Embeddings Returned
+            let embedding_len = deserialized_rows[0].1 .0.len();
+
+            let batch_n = 1000;
+            let mut batches = Vec::new();
+            let mut batch_ids = Vec::new();
+            let mut batch_embeddings: Vec<f32> = Vec::new();
+            deserialized_rows.iter().for_each(|(id, embedding)| {
+                batch_ids.push(id);
+                batch_embeddings.extend(&embedding.0);
+
+                if batch_ids.len() == batch_n {
+                    let embeddings = std::mem::take(&mut batch_embeddings);
+                    let ids = std::mem::take(&mut batch_ids);
+                    let array =
+                        Array2::from_shape_vec((ids.len(), embedding_len.clone()), embeddings);
+                    match array {
+                        Ok(array) => {
+                            batches.push((ids, array));
+                        }
+                        Err(err) => log::error!("Failed to deserialize to ndarray: {:?}", err),
+                    }
+                }
+            });
 
-            anyhow::Ok(results)
+            if batch_ids.len() > 0 {
+                let array = Array2::from_shape_vec(
+                    (batch_ids.len(), embedding_len),
+                    batch_embeddings.clone(),
+                );
+                match array {
+                    Ok(array) => {
+                        batches.push((batch_ids.clone(), array));
+                    }
+                    Err(err) => log::error!("Failed to deserialize to ndarray: {:?}", err),
+                }
+            }
+
+            let mut ids: Vec<usize> = Vec::new();
+            let mut results = Vec::new();
+            for (batch_ids, array) in batches {
+                let scores = array
+                    .dot(&query.t())
+                    .to_vec()
+                    .iter()
+                    .map(|score| OrderedFloat(*score))
+                    .collect::<Vec<OrderedFloat<f32>>>();
+                results.extend(scores);
+                ids.extend(batch_ids);
+            }
+
+            let sorted_idx = argsort(&results);
+            let mut sorted_results = Vec::new();
+            let last_idx = limit.min(sorted_idx.len());
+            for idx in &sorted_idx[0..last_idx] {
+                sorted_results.push((ids[*idx] as i64, results[*idx]))
+            }
+
+            Ok(sorted_results)
         })
     }
 
@@ -468,31 +543,6 @@ impl VectorDatabase {
         })
     }
 
-    fn for_each_span(
-        db: &rusqlite::Connection,
-        file_ids: &[i64],
-        mut f: impl FnMut(i64, Embedding),
-    ) -> Result<()> {
-        let mut query_statement = db.prepare(
-            "
-            SELECT
-                id, embedding
-            FROM
-                spans
-            WHERE
-                file_id IN rarray(?)
-            ",
-        )?;
-
-        query_statement
-            .query_map(params![ids_to_sql(&file_ids)], |row| {
-                Ok((row.get(0)?, row.get::<_, Embedding>(1)?))
-            })?
-            .filter_map(|row| row.ok())
-            .for_each(|(id, embedding)| f(id, embedding));
-        Ok(())
-    }
-
     pub fn spans_for_ids(
         &self,
         ids: &[i64],

crates/semantic_index/src/embedding_queue.rs πŸ”—

@@ -1,4 +1,5 @@
-use crate::{embedding::EmbeddingProvider, parsing::Span, JobHandle};
+use crate::{parsing::Span, JobHandle};
+use ai::embedding::EmbeddingProvider;
 use gpui::executor::Background;
 use parking_lot::Mutex;
 use smol::channel;

crates/semantic_index/src/parsing.rs πŸ”—

@@ -1,4 +1,4 @@
-use crate::embedding::{Embedding, EmbeddingProvider};
+use ai::embedding::{Embedding, EmbeddingProvider};
 use anyhow::{anyhow, Result};
 use language::{Grammar, Language};
 use rusqlite::{

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

@@ -1,5 +1,4 @@
 mod db;
-pub mod embedding;
 mod embedding_queue;
 mod parsing;
 pub mod semantic_index_settings;
@@ -8,14 +7,15 @@ pub mod semantic_index_settings;
 mod semantic_index_tests;
 
 use crate::semantic_index_settings::SemanticIndexSettings;
+use ai::embedding::{Embedding, EmbeddingProvider, OpenAIEmbeddings};
 use anyhow::{anyhow, Result};
 use collections::{BTreeMap, HashMap, HashSet};
 use db::VectorDatabase;
-use embedding::{Embedding, EmbeddingProvider, OpenAIEmbeddings};
 use embedding_queue::{EmbeddingQueue, FileToEmbed};
 use futures::{future, FutureExt, StreamExt};
 use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
 use language::{Anchor, Bias, Buffer, Language, LanguageRegistry};
+use lazy_static::lazy_static;
 use ordered_float::OrderedFloat;
 use parking_lot::Mutex;
 use parsing::{CodeContextRetriever, Span, SpanDigest, PARSEABLE_ENTIRE_FILE_TYPES};
@@ -24,6 +24,7 @@ use project::{search::PathMatcher, Fs, PathChange, Project, ProjectEntryId, Work
 use smol::channel;
 use std::{
     cmp::Reverse,
+    env,
     future::Future,
     mem,
     ops::Range,
@@ -38,6 +39,10 @@ const SEMANTIC_INDEX_VERSION: usize = 11;
 const BACKGROUND_INDEXING_DELAY: Duration = Duration::from_secs(5 * 60);
 const EMBEDDING_QUEUE_FLUSH_TIMEOUT: Duration = Duration::from_millis(250);
 
+lazy_static! {
+    static ref OPENAI_API_KEY: Option<String> = env::var("OPENAI_API_KEY").ok();
+}
+
 pub fn init(
     fs: Arc<dyn Fs>,
     http_client: Arc<dyn HttpClient>,
@@ -100,6 +105,7 @@ pub fn init(
 
 #[derive(Copy, Clone, Debug)]
 pub enum SemanticIndexStatus {
+    NotAuthenticated,
     NotIndexed,
     Indexed,
     Indexing {
@@ -275,6 +281,10 @@ impl SemanticIndex {
     }
 
     pub fn status(&self, project: &ModelHandle<Project>) -> SemanticIndexStatus {
+        if !self.embedding_provider.is_authenticated() {
+            return SemanticIndexStatus::NotAuthenticated;
+        }
+
         if let Some(project_state) = self.projects.get(&project.downgrade()) {
             if project_state
                 .worktrees
@@ -694,12 +704,14 @@ impl SemanticIndex {
         let embedding_provider = self.embedding_provider.clone();
 
         cx.spawn(|this, mut cx| async move {
+            index.await?;
+            let t0 = Instant::now();
             let query = embedding_provider
                 .embed_batch(vec![query])
                 .await?
                 .pop()
                 .ok_or_else(|| anyhow!("could not embed query"))?;
-            index.await?;
+            log::trace!("Embedding Search Query: {:?}ms", t0.elapsed().as_millis());
 
             let search_start = Instant::now();
             let modified_buffer_results = this.update(&mut cx, |this, cx| {
@@ -777,10 +789,15 @@ impl SemanticIndex {
 
             let batch_n = cx.background().num_cpus();
             let ids_len = file_ids.clone().len();
-            let batch_size = if ids_len <= batch_n {
-                ids_len
-            } else {
-                ids_len / batch_n
+            let minimum_batch_size = 50;
+
+            let batch_size = {
+                let size = ids_len / batch_n;
+                if size < minimum_batch_size {
+                    minimum_batch_size
+                } else {
+                    size
+                }
             };
 
             let mut batch_results = Vec::new();
@@ -812,6 +829,7 @@ impl SemanticIndex {
                             Ok(ix) => ix,
                             Err(ix) => ix,
                         };
+
                         results.insert(ix, (id, similarity));
                         results.truncate(limit);
                     }
@@ -846,7 +864,6 @@ impl SemanticIndex {
             })?;
 
             let buffers = futures::future::join_all(tasks).await;
-
             Ok(buffers
                 .into_iter()
                 .zip(ranges)
@@ -965,6 +982,10 @@ impl SemanticIndex {
         project: ModelHandle<Project>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<()>> {
+        if !self.embedding_provider.is_authenticated() {
+            return Task::ready(Err(anyhow!("user is not authenticated")));
+        }
+
         if !self.projects.contains_key(&project.downgrade()) {
             let subscription = cx.subscribe(&project, |this, project, event, cx| match event {
                 project::Event::WorktreeAdded | project::Event::WorktreeRemoved(_) => {

crates/semantic_index/src/semantic_index_tests.rs πŸ”—

@@ -1,10 +1,10 @@
 use crate::{
-    embedding::{DummyEmbeddings, Embedding, EmbeddingProvider},
     embedding_queue::EmbeddingQueue,
     parsing::{subtract_ranges, CodeContextRetriever, Span, SpanDigest},
     semantic_index_settings::SemanticIndexSettings,
     FileToEmbed, JobHandle, SearchResult, SemanticIndex, EMBEDDING_QUEUE_FLUSH_TIMEOUT,
 };
+use ai::embedding::{DummyEmbeddings, Embedding, EmbeddingProvider};
 use anyhow::Result;
 use async_trait::async_trait;
 use gpui::{executor::Deterministic, Task, TestAppContext};
@@ -1267,6 +1267,9 @@ impl FakeEmbeddingProvider {
 
 #[async_trait]
 impl EmbeddingProvider for FakeEmbeddingProvider {
+    fn is_authenticated(&self) -> bool {
+        true
+    }
     fn truncate(&self, span: &str) -> (String, usize) {
         (span.to_string(), 1)
     }

crates/storybook/Cargo.toml πŸ”—

@@ -9,14 +9,17 @@ name = "storybook"
 path = "src/storybook.rs"
 
 [dependencies]
-gpui2 = { path = "../gpui2" }
 anyhow.workspace = true
+clap = { version = "4.4", features = ["derive", "string"] }
+gpui2 = { path = "../gpui2" }
 log.workspace = true
 rust-embed.workspace = true
 serde.workspace = true
 settings = { path = "../settings" }
 simplelog = "0.9"
+strum = { version = "0.25.0", features = ["derive"] }
 theme = { path = "../theme" }
+ui = { path = "../ui" }
 util = { path = "../util" }
 
 [dev-dependencies]

crates/storybook/src/collab_panel.rs πŸ”—

@@ -1,10 +1,10 @@
-use crate::theme::{theme, Theme};
 use gpui2::{
     elements::{div, div::ScrollState, img, svg},
     style::{StyleHelpers, Styleable},
     ArcCow, Element, IntoElement, ParentElement, ViewContext,
 };
 use std::marker::PhantomData;
+use ui::{theme, Theme};
 
 #[derive(Element)]
 pub struct CollabPanelElement<V: 'static> {
@@ -52,12 +52,12 @@ impl<V: 'static> CollabPanelElement<V> {
                             //:: https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state
                             // .group()
                             // List Section Header
-                            .child(self.list_section_header("#CRDB", true, theme))
+                            .child(self.list_section_header("#CRDB", true, &theme))
                             // List Item Large
                             .child(self.list_item(
                                 "http://github.com/maxbrunsfeld.png?s=50",
                                 "maxbrunsfeld",
-                                theme,
+                                &theme,
                             )),
                     )
                     .child(
@@ -65,31 +65,31 @@ impl<V: 'static> CollabPanelElement<V> {
                             .py_2()
                             .flex()
                             .flex_col()
-                            .child(self.list_section_header("CHANNELS", true, theme)),
+                            .child(self.list_section_header("CHANNELS", true, &theme)),
                     )
                     .child(
                         div()
                             .py_2()
                             .flex()
                             .flex_col()
-                            .child(self.list_section_header("CONTACTS", true, theme))
+                            .child(self.list_section_header("CONTACTS", true, &theme))
                             .children(
                                 std::iter::repeat_with(|| {
                                     vec![
                                         self.list_item(
                                             "http://github.com/as-cii.png?s=50",
                                             "as-cii",
-                                            theme,
+                                            &theme,
                                         ),
                                         self.list_item(
                                             "http://github.com/nathansobo.png?s=50",
                                             "nathansobo",
-                                            theme,
+                                            &theme,
                                         ),
                                         self.list_item(
                                             "http://github.com/maxbrunsfeld.png?s=50",
                                             "maxbrunsfeld",
-                                            theme,
+                                            &theme,
                                         ),
                                     ]
                                 })

crates/storybook/src/stories/components/breadcrumb.rs πŸ”—

@@ -0,0 +1,16 @@
+use gpui2::{Element, IntoElement, ParentElement, ViewContext};
+use ui::breadcrumb;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct BreadcrumbStory {}
+
+impl BreadcrumbStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container()
+            .child(Story::title_for::<_, ui::Breadcrumb>())
+            .child(Story::label("Default"))
+            .child(breadcrumb())
+    }
+}

crates/storybook/src/stories/components/facepile.rs πŸ”—

@@ -0,0 +1,50 @@
+use gpui2::elements::div;
+use gpui2::style::StyleHelpers;
+use gpui2::{Element, IntoElement, ParentElement, ViewContext};
+use ui::prelude::*;
+use ui::{avatar, facepile, theme};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct FacepileStory {}
+
+impl FacepileStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        let avatars = vec![
+            avatar("https://avatars.githubusercontent.com/u/1714999?v=4"),
+            avatar("https://avatars.githubusercontent.com/u/482957?v=4"),
+            avatar("https://avatars.githubusercontent.com/u/1789?v=4"),
+        ];
+
+        Story::container()
+            .child(Story::title_for::<_, ui::Facepile>())
+            .child(Story::label("Default"))
+            .child(
+                div()
+                    .flex()
+                    .gap_3()
+                    .child(facepile(avatars.clone().into_iter().take(1)))
+                    .child(facepile(avatars.clone().into_iter().take(2)))
+                    .child(facepile(avatars.clone().into_iter().take(3))),
+            )
+            .child(Story::label("Rounded rectangle avatars"))
+            .child({
+                let shape = Shape::RoundedRectangle;
+
+                let avatars = avatars
+                    .clone()
+                    .into_iter()
+                    .map(|avatar| avatar.shape(Shape::RoundedRectangle));
+
+                div()
+                    .flex()
+                    .gap_3()
+                    .child(facepile(avatars.clone().take(1)))
+                    .child(facepile(avatars.clone().take(2)))
+                    .child(facepile(avatars.clone().take(3)))
+            })
+    }
+}

crates/storybook/src/stories/components/toolbar.rs πŸ”—

@@ -0,0 +1,16 @@
+use gpui2::{Element, IntoElement, ParentElement, ViewContext};
+use ui::toolbar;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct ToolbarStory {}
+
+impl ToolbarStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container()
+            .child(Story::title_for::<_, ui::Toolbar>())
+            .child(Story::label("Default"))
+            .child(toolbar())
+    }
+}

crates/storybook/src/stories/components/traffic_lights.rs πŸ”—

@@ -0,0 +1,18 @@
+use gpui2::{Element, IntoElement, ParentElement, ViewContext};
+use ui::{theme, traffic_lights};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct TrafficLightsStory {}
+
+impl TrafficLightsStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        Story::container()
+            .child(Story::title_for::<_, ui::TrafficLights>())
+            .child(Story::label("Default"))
+            .child(traffic_lights())
+    }
+}

crates/storybook/src/stories/elements/avatar.rs πŸ”—

@@ -0,0 +1,26 @@
+use gpui2::{Element, IntoElement, ParentElement, ViewContext};
+use ui::prelude::*;
+use ui::{avatar, theme};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct AvatarStory {}
+
+impl AvatarStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        Story::container()
+            .child(Story::title_for::<_, ui::Avatar>())
+            .child(Story::label("Default"))
+            .child(avatar(
+                "https://avatars.githubusercontent.com/u/1714999?v=4",
+            ))
+            .child(Story::label("Rounded rectangle"))
+            .child(
+                avatar("https://avatars.githubusercontent.com/u/1714999?v=4")
+                    .shape(Shape::RoundedRectangle),
+            )
+    }
+}

crates/storybook/src/story.rs πŸ”—

@@ -0,0 +1,38 @@
+use gpui2::elements::div;
+use gpui2::style::StyleHelpers;
+use gpui2::{rgb, Element, Hsla, ParentElement};
+
+pub struct Story {}
+
+impl Story {
+    pub fn container<V: 'static>() -> div::Div<V> {
+        div()
+            .size_full()
+            .flex()
+            .flex_col()
+            .pt_2()
+            .px_4()
+            .font("Zed Mono Extended")
+            .fill(rgb::<Hsla>(0x282c34))
+    }
+
+    pub fn title<V: 'static>(title: &str) -> impl Element<V> {
+        div()
+            .text_xl()
+            .text_color(rgb::<Hsla>(0xffffff))
+            .child(title.to_owned())
+    }
+
+    pub fn title_for<V: 'static, T>() -> impl Element<V> {
+        Self::title(std::any::type_name::<T>())
+    }
+
+    pub fn label<V: 'static>(label: &str) -> impl Element<V> {
+        div()
+            .mt_4()
+            .mb_2()
+            .text_xs()
+            .text_color(rgb::<Hsla>(0xffffff))
+            .child(label.to_owned())
+    }
+}

crates/storybook/src/story_selector.rs πŸ”—

@@ -0,0 +1,76 @@
+use std::{str::FromStr, sync::OnceLock};
+
+use anyhow::{anyhow, Context};
+use clap::builder::PossibleValue;
+use clap::ValueEnum;
+use strum::{EnumIter, EnumString, IntoEnumIterator};
+
+#[derive(Debug, Clone, Copy, strum::Display, EnumString, EnumIter)]
+#[strum(serialize_all = "snake_case")]
+pub enum ElementStory {
+    Avatar,
+}
+
+#[derive(Debug, Clone, Copy, strum::Display, EnumString, EnumIter)]
+#[strum(serialize_all = "snake_case")]
+pub enum ComponentStory {
+    Breadcrumb,
+    Facepile,
+    Toolbar,
+    TrafficLights,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum StorySelector {
+    Element(ElementStory),
+    Component(ComponentStory),
+}
+
+impl FromStr for StorySelector {
+    type Err = anyhow::Error;
+
+    fn from_str(raw_story_name: &str) -> std::result::Result<Self, Self::Err> {
+        let story = raw_story_name.to_ascii_lowercase();
+
+        if let Some((_, story)) = story.split_once("elements/") {
+            let element_story = ElementStory::from_str(story)
+                .with_context(|| format!("story not found for element '{story}'"))?;
+
+            return Ok(Self::Element(element_story));
+        }
+
+        if let Some((_, story)) = story.split_once("components/") {
+            let component_story = ComponentStory::from_str(story)
+                .with_context(|| format!("story not found for component '{story}'"))?;
+
+            return Ok(Self::Component(component_story));
+        }
+
+        Err(anyhow!("story not found for '{raw_story_name}'"))
+    }
+}
+
+/// The list of all stories available in the storybook.
+static ALL_STORIES: OnceLock<Vec<StorySelector>> = OnceLock::new();
+
+impl ValueEnum for StorySelector {
+    fn value_variants<'a>() -> &'a [Self] {
+        let stories = ALL_STORIES.get_or_init(|| {
+            let element_stories = ElementStory::iter().map(Self::Element);
+            let component_stories = ComponentStory::iter().map(Self::Component);
+
+            element_stories.chain(component_stories).collect::<Vec<_>>()
+        });
+
+        stories
+    }
+
+    fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
+        let value = match self {
+            Self::Element(story) => format!("elements/{story}"),
+            Self::Component(story) => format!("components/{story}"),
+        };
+
+        Some(PossibleValue::new(value))
+    }
+}

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

@@ -1,31 +1,45 @@
 #![allow(dead_code, unused_variables)]
 
-use crate::theme::Theme;
+mod collab_panel;
+mod stories;
+mod story;
+mod story_selector;
+mod workspace;
+
 use ::theme as legacy_theme;
-use element_ext::ElementExt;
-use gpui2::{serde_json, vec2f, view, Element, RectF, ViewContext, WindowBounds};
+use clap::Parser;
+use gpui2::{serde_json, vec2f, view, Element, IntoElement, RectF, ViewContext, WindowBounds};
 use legacy_theme::ThemeSettings;
 use log::LevelFilter;
 use settings::{default_settings, SettingsStore};
 use simplelog::SimpleLogger;
+use stories::components::breadcrumb::BreadcrumbStory;
+use stories::components::facepile::FacepileStory;
+use stories::components::toolbar::ToolbarStory;
+use stories::components::traffic_lights::TrafficLightsStory;
+use stories::elements::avatar::AvatarStory;
+use ui::{ElementExt, Theme};
 
-mod collab_panel;
-mod components;
-mod element_ext;
-mod prelude;
-mod theme;
-mod ui;
-mod workspace;
+use crate::story_selector::{ComponentStory, ElementStory, StorySelector};
 
 gpui2::actions! {
     storybook,
     [ToggleInspector]
 }
 
+#[derive(Parser)]
+#[command(author, version, about, long_about = None)]
+struct Args {
+    #[arg(value_enum)]
+    story: Option<StorySelector>,
+}
+
 fn main() {
     SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
 
-    gpui2::App::new(Assets).unwrap().run(|cx| {
+    let args = Args::parse();
+
+    gpui2::App::new(Assets).unwrap().run(move |cx| {
         let mut store = SettingsStore::default();
         store
             .set_default_settings(default_settings().as_ref(), cx)
@@ -40,19 +54,36 @@ fn main() {
                 center: true,
                 ..Default::default()
             },
-            |cx| {
-                view(|cx| {
-                    // cx.enable_inspector();
-                    storybook(&mut ViewContext::new(cx))
-                })
+            |cx| match args.story {
+                Some(StorySelector::Element(ElementStory::Avatar)) => {
+                    view(|cx| render_story(&mut ViewContext::new(cx), AvatarStory::default()))
+                }
+                Some(StorySelector::Component(ComponentStory::Breadcrumb)) => {
+                    view(|cx| render_story(&mut ViewContext::new(cx), BreadcrumbStory::default()))
+                }
+                Some(StorySelector::Component(ComponentStory::Facepile)) => {
+                    view(|cx| render_story(&mut ViewContext::new(cx), FacepileStory::default()))
+                }
+                Some(StorySelector::Component(ComponentStory::Toolbar)) => {
+                    view(|cx| render_story(&mut ViewContext::new(cx), ToolbarStory::default()))
+                }
+                Some(StorySelector::Component(ComponentStory::TrafficLights)) => view(|cx| {
+                    render_story(&mut ViewContext::new(cx), TrafficLightsStory::default())
+                }),
+                None => {
+                    view(|cx| render_story(&mut ViewContext::new(cx), WorkspaceElement::default()))
+                }
             },
         );
         cx.platform().activate(true);
     });
 }
 
-fn storybook<V: 'static>(cx: &mut ViewContext<V>) -> impl Element<V> {
-    workspace().themed(current_theme(cx))
+fn render_story<V: 'static, S: IntoElement<V>>(
+    cx: &mut ViewContext<V>,
+    story: S,
+) -> impl Element<V> {
+    story.into_element().themed(current_theme(cx))
 }
 
 // Nathan: During the transition to gpui2, we will include the base theme on the legacy Theme struct.
@@ -75,7 +106,7 @@ fn current_theme<V: 'static>(cx: &mut ViewContext<V>) -> Theme {
 use anyhow::{anyhow, Result};
 use gpui2::AssetSource;
 use rust_embed::RustEmbed;
-use workspace::workspace;
+use workspace::WorkspaceElement;
 
 #[derive(RustEmbed)]
 #[folder = "../../assets"]

crates/storybook/src/ui.rs πŸ”—

@@ -1,23 +0,0 @@
-mod element;
-pub use element::avatar::*;
-pub use element::details::*;
-pub use element::icon::*;
-pub use element::icon_button::*;
-pub use element::indicator::*;
-pub use element::input::*;
-pub use element::label::*;
-pub use element::text_button::*;
-pub use element::tool_divider::*;
-
-mod component;
-pub use component::facepile::*;
-pub use component::follow_group::*;
-pub use component::list_item::*;
-pub use component::tab::*;
-
-mod module;
-pub use module::chat_panel::*;
-pub use module::project_panel::*;
-pub use module::status_bar::*;
-pub use module::tab_bar::*;
-pub use module::title_bar::*;

crates/storybook/src/ui/element.rs πŸ”—

@@ -1,9 +0,0 @@
-pub(crate) mod avatar;
-pub(crate) mod details;
-pub(crate) mod icon;
-pub(crate) mod icon_button;
-pub(crate) mod indicator;
-pub(crate) mod input;
-pub(crate) mod label;
-pub(crate) mod text_button;
-pub(crate) mod tool_divider;

crates/storybook/src/ui/element/icon.rs πŸ”—

@@ -1,73 +0,0 @@
-use crate::theme::theme;
-use gpui2::elements::svg;
-use gpui2::style::StyleHelpers;
-use gpui2::IntoElement;
-use gpui2::{Element, ViewContext};
-
-// Icon::Hash
-// icon(IconAsset::Hash).color(IconColor::Warning)
-// Icon::new(IconAsset::Hash).color(IconColor::Warning)
-
-#[derive(Default, PartialEq, Copy, Clone)]
-pub enum IconAsset {
-    Ai,
-    ArrowLeft,
-    ArrowRight,
-    #[default]
-    ArrowUpRight,
-    Bolt,
-    Hash,
-    File,
-    Folder,
-    FolderOpen,
-    ChevronDown,
-    ChevronUp,
-    ChevronLeft,
-    ChevronRight,
-}
-
-impl IconAsset {
-    pub fn path(self) -> &'static str {
-        match self {
-            IconAsset::Ai => "icons/ai.svg",
-            IconAsset::ArrowLeft => "icons/arrow_left.svg",
-            IconAsset::ArrowRight => "icons/arrow_right.svg",
-            IconAsset::ArrowUpRight => "icons/arrow_up_right.svg",
-            IconAsset::Bolt => "icons/bolt.svg",
-            IconAsset::Hash => "icons/hash.svg",
-            IconAsset::ChevronDown => "icons/chevron_down.svg",
-            IconAsset::ChevronUp => "icons/chevron_up.svg",
-            IconAsset::ChevronLeft => "icons/chevron_left.svg",
-            IconAsset::ChevronRight => "icons/chevron_right.svg",
-            IconAsset::File => "icons/file_icons/file.svg",
-            IconAsset::Folder => "icons/file_icons/folder.svg",
-            IconAsset::FolderOpen => "icons/file_icons/folder_open.svg",
-        }
-    }
-}
-
-#[derive(Element, Clone)]
-pub struct Icon {
-    asset: IconAsset,
-}
-
-pub fn icon(asset: IconAsset) -> Icon {
-    Icon { asset }
-}
-
-// impl Icon {
-//     pub fn new(asset: IconAsset) -> Icon {
-//         Icon { asset }
-//     }
-// }
-
-impl Icon {
-    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = theme(cx);
-
-        svg()
-            .path(self.asset.path())
-            .size_4()
-            .fill(theme.lowest.base.default.foreground)
-    }
-}

crates/storybook/src/ui/module/project_panel.rs πŸ”—

@@ -1,97 +0,0 @@
-use crate::{
-    prelude::{InteractionState, ToggleState},
-    theme::theme,
-    ui::{details, input, label, list_item, IconAsset, LabelColor},
-};
-use gpui2::{
-    elements::{div, div::ScrollState},
-    style::StyleHelpers,
-    ParentElement, ViewContext,
-};
-use gpui2::{Element, IntoElement};
-use std::marker::PhantomData;
-
-#[derive(Element)]
-pub struct ProjectPanel<V: 'static> {
-    view_type: PhantomData<V>,
-    scroll_state: ScrollState,
-}
-
-pub fn project_panel<V: 'static>(scroll_state: ScrollState) -> ProjectPanel<V> {
-    ProjectPanel {
-        view_type: PhantomData,
-        scroll_state,
-    }
-}
-
-impl<V: 'static> ProjectPanel<V> {
-    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = theme(cx);
-
-        div()
-            .w_56()
-            .h_full()
-            .flex()
-            .flex_col()
-            .fill(theme.middle.base.default.background)
-            .child(
-                div()
-                    .w_56()
-                    .flex()
-                    .flex_col()
-                    .overflow_y_scroll(self.scroll_state.clone())
-                    .child(details("This is a long string that should wrap when it keeps going for a long time.").meta_text("6 h ago)"))
-                    .child(
-                        div().flex().flex_col().children(
-                            std::iter::repeat_with(|| {
-                                vec![
-                                    list_item(label("sqlez").color(LabelColor::Modified))
-                                        .left_icon(IconAsset::FolderOpen.into())
-                                        .indent_level(0)
-                                        .set_toggle(ToggleState::NotToggled),
-                                    list_item(label("storybook").color(LabelColor::Modified))
-                                        .left_icon(IconAsset::FolderOpen.into())
-                                        .indent_level(0)
-                                        .set_toggle(ToggleState::Toggled),
-                                    list_item(label("docs").color(LabelColor::Default))
-                                        .left_icon(IconAsset::Folder.into())
-                                        .indent_level(1)
-                                        .set_toggle(ToggleState::Toggled),
-                                    list_item(label("src").color(LabelColor::Modified))
-                                        .left_icon(IconAsset::FolderOpen.into())
-                                        .indent_level(2)
-                                        .set_toggle(ToggleState::Toggled),
-                                    list_item(label("ui").color(LabelColor::Modified))
-                                        .left_icon(IconAsset::FolderOpen.into())
-                                        .indent_level(3)
-                                        .set_toggle(ToggleState::Toggled),
-                                    list_item(label("component").color(LabelColor::Created))
-                                        .left_icon(IconAsset::FolderOpen.into())
-                                        .indent_level(4)
-                                        .set_toggle(ToggleState::Toggled),
-                                    list_item(label("facepile.rs").color(LabelColor::Default))
-                                        .left_icon(IconAsset::File.into())
-                                        .indent_level(5),
-                                    list_item(label("follow_group.rs").color(LabelColor::Default))
-                                        .left_icon(IconAsset::File.into())
-                                        .indent_level(5),
-                                    list_item(label("list_item.rs").color(LabelColor::Created))
-                                        .left_icon(IconAsset::File.into())
-                                        .indent_level(5),
-                                    list_item(label("tab.rs").color(LabelColor::Default))
-                                        .left_icon(IconAsset::File.into())
-                                        .indent_level(5),
-                                ]
-                            })
-                            .take(10)
-                            .flatten(),
-                        ),
-                    ),
-            )
-            .child(
-                input("Find something...")
-                    .value("buffe".to_string())
-                    .state(InteractionState::Focused),
-            )
-    }
-}

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

@@ -1,24 +1,17 @@
-use crate::{
-    theme::theme,
-    ui::{chat_panel, project_panel, status_bar, tab_bar, title_bar},
-};
 use gpui2::{
     elements::{div, div::ScrollState},
     style::StyleHelpers,
     Element, IntoElement, ParentElement, ViewContext,
 };
+use ui::{chat_panel, project_panel, status_bar, tab_bar, theme, title_bar, toolbar};
 
 #[derive(Element, Default)]
-struct WorkspaceElement {
+pub struct WorkspaceElement {
     left_scroll_state: ScrollState,
     right_scroll_state: ScrollState,
     tab_bar_scroll_state: ScrollState,
 }
 
-pub fn workspace<V: 'static>() -> impl Element<V> {
-    WorkspaceElement::default()
-}
-
 impl WorkspaceElement {
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
@@ -52,7 +45,8 @@ impl WorkspaceElement {
                                     .flex()
                                     .flex_col()
                                     .flex_1()
-                                    .child(tab_bar(self.tab_bar_scroll_state.clone())),
+                                    .child(tab_bar(self.tab_bar_scroll_state.clone()))
+                                    .child(toolbar()),
                             ),
                     )
                     .child(chat_panel(self.right_scroll_state.clone())),

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

@@ -284,12 +284,7 @@ impl TerminalView {
     pub fn deploy_context_menu(&mut self, position: Vector2F, cx: &mut ViewContext<Self>) {
         let menu_entries = vec![
             ContextMenuItem::action("Clear", Clear),
-            ContextMenuItem::action(
-                "Close",
-                pane::CloseActiveItem {
-                    save_behavior: None,
-                },
-            ),
+            ContextMenuItem::action("Close", pane::CloseActiveItem { save_intent: None }),
         ];
 
         self.context_menu.update(cx, |menu, cx| {

crates/ui/Cargo.toml πŸ”—

@@ -0,0 +1,12 @@
+[package]
+name = "ui"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[dependencies]
+anyhow.workspace = true
+gpui2 = { path = "../gpui2" }
+serde.workspace = true
+settings = { path = "../settings" }
+theme = { path = "../theme" }

crates/ui/doc/elevation.md πŸ”—

@@ -0,0 +1,57 @@
+# Elevation
+
+Elevation in Zed applies to all surfaces and components. Elevation is categorized into levels.
+
+Elevation accomplishes the following:
+- Allows surfaces to move in front of or behind others, such as content scrolling beneath app top bars.
+- Reflects spatial relationships, for instance, how a floating action button’s shadow intimates its disconnection from a collection of cards.
+- Directs attention to structures at the highest elevation, like a temporary dialog arising in front of other surfaces.
+
+Elevations are the initial elevation values assigned to components by default.
+
+Components may transition to a higher elevation in some cases, like user interations.
+
+On such occasions, components transition to predetermined dynamic elevation offsets. These are the typical elevations to which components move when they are not at rest.
+
+## Understanding Elevation
+
+Elevation can be thought of as the physical closeness of an element to the user. Elements with lower elevations are physically further away from the user on the z-axis and appear to be underneath elements with higher elevations.
+
+Material Design 3 has a some great visualizations of elevation that may be helpful to understanding the mental modal of elevation. [Material Design – Elevation](https://m3.material.io/styles/elevation/overview)
+
+## Elevation Levels
+
+Zed integrates six unique elevation levels in its design system. The elevation of a surface is expressed as a whole number ranging from 0 to 5, both numbers inclusive. A component’s elevation is ascertained by combining the component’s resting elevation with any dynamic elevation offsets.
+
+The levels are detailed as follows:
+
+0. App Background
+1. UI Surface
+2. Elevated Elements
+3. Wash
+4. Focused Element
+5. Dragged Element
+
+### 0. App Background
+
+The app background constitutes the lowest elevation layer, appearing behind all other surfaces and components. It is predominantly used for the background color of the app.
+
+### 1. UI Surface
+
+The UI Surface is the standard elevation for components and is placed above the app background. It is generally used for the background color of the app bar, card, and sheet.
+
+### 2. Elevated Elements
+
+Elevated elements appear above the UI surface layer surfaces and components. Elevated elements are predominantly used for creating popovers, context menus, and tooltips.
+
+### 3. Wash
+
+Wash denotes a distinct elevation reserved to isolate app UI layers from high elevation components such as modals, notifications, and overlaid panels. The wash may not consistently be visible when these components are active. This layer is often referred to as a scrim or overlay and the background color of the wash is typically deployed in its design.
+
+### 4. Focused Element
+
+Focused elements obtain a higher elevation above surfaces and components at wash elevation. They are often used for modals, notifications, and overlaid panels and indicate that they are the sole element the user is interacting with at the moment.
+
+### 5. Dragged Element
+
+Dragged elements gain the highest elevation, thus appearing above surfaces and components at the elevation of focused elements. These are typically used for elements that are being dragged, following the cursor

crates/storybook/src/components.rs β†’ crates/ui/src/components.rs πŸ”—

@@ -1,8 +1,53 @@
-use gpui2::{
-    elements::div, interactive::Interactive, platform::MouseButton, style::StyleHelpers, ArcCow,
-    Element, EventContext, IntoElement, ParentElement, ViewContext,
-};
-use std::{marker::PhantomData, rc::Rc};
+mod breadcrumb;
+mod chat_panel;
+mod collab_panel;
+mod command_palette;
+mod facepile;
+mod follow_group;
+mod icon_button;
+mod list;
+mod list_item;
+mod list_section_header;
+mod palette;
+mod palette_item;
+mod project_panel;
+mod status_bar;
+mod tab;
+mod tab_bar;
+mod title_bar;
+mod toolbar;
+mod traffic_lights;
+mod workspace;
+
+pub use breadcrumb::*;
+pub use chat_panel::*;
+pub use collab_panel::*;
+pub use command_palette::*;
+pub use facepile::*;
+pub use follow_group::*;
+pub use icon_button::*;
+pub use list::*;
+pub use list_item::*;
+pub use list_section_header::*;
+pub use palette::*;
+pub use palette_item::*;
+pub use project_panel::*;
+pub use status_bar::*;
+pub use tab::*;
+pub use tab_bar::*;
+pub use title_bar::*;
+pub use toolbar::*;
+pub use traffic_lights::*;
+pub use workspace::*;
+
+use std::marker::PhantomData;
+use std::rc::Rc;
+
+use gpui2::elements::div;
+use gpui2::interactive::Interactive;
+use gpui2::platform::MouseButton;
+use gpui2::style::StyleHelpers;
+use gpui2::{ArcCow, Element, EventContext, IntoElement, ParentElement, ViewContext};
 
 struct ButtonHandlers<V, D> {
     click: Option<Rc<dyn Fn(&mut V, &D, &mut EventContext<V>)>>,

crates/ui/src/components/breadcrumb.rs πŸ”—

@@ -0,0 +1,36 @@
+use gpui2::elements::div;
+use gpui2::style::{StyleHelpers, Styleable};
+use gpui2::{Element, IntoElement, ParentElement, ViewContext};
+
+use crate::theme;
+
+#[derive(Element)]
+pub struct Breadcrumb {}
+
+pub fn breadcrumb() -> Breadcrumb {
+    Breadcrumb {}
+}
+
+impl Breadcrumb {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .px_1()
+            .flex()
+            .flex_row()
+            // TODO: Read font from theme (or settings?).
+            .font("Zed Mono Extended")
+            .text_sm()
+            .text_color(theme.middle.base.default.foreground)
+            .rounded_md()
+            .hover()
+            .fill(theme.highest.base.hovered.background)
+            // TODO: Replace hardcoded breadcrumbs.
+            .child("crates/ui/src/components/toolbar.rs")
+            .child(" β€Ί ")
+            .child("impl Breadcrumb")
+            .child(" β€Ί ")
+            .child("fn render")
+    }
+}

crates/storybook/src/ui/module/chat_panel.rs β†’ crates/ui/src/components/chat_panel.rs πŸ”—

@@ -1,12 +1,13 @@
 use std::marker::PhantomData;
 
-use crate::theme::theme;
-use crate::ui::icon_button;
 use gpui2::elements::div::ScrollState;
 use gpui2::style::StyleHelpers;
 use gpui2::{elements::div, IntoElement};
 use gpui2::{Element, ParentElement, ViewContext};
 
+use crate::theme::theme;
+use crate::{icon_button, IconAsset};
+
 #[derive(Element)]
 pub struct ChatPanel<V: 'static> {
     view_type: PhantomData<V>,
@@ -57,8 +58,8 @@ impl<V: 'static> ChatPanel<V> {
                             .flex()
                             .items_center()
                             .gap_px()
-                            .child(icon_button("icons/plus.svg"))
-                            .child(icon_button("icons/split.svg")),
+                            .child(icon_button().icon(IconAsset::Plus))
+                            .child(icon_button().icon(IconAsset::Split)),
                     ),
             )
     }

crates/ui/src/components/collab_panel.rs πŸ”—

@@ -0,0 +1,177 @@
+use crate::theme::{theme, Theme};
+use gpui2::{
+    elements::{div, div::ScrollState, img, svg},
+    style::{StyleHelpers, Styleable},
+    ArcCow, Element, IntoElement, ParentElement, ViewContext,
+};
+use std::marker::PhantomData;
+
+#[derive(Element)]
+pub struct CollabPanelElement<V: 'static> {
+    view_type: PhantomData<V>,
+    scroll_state: ScrollState,
+}
+
+// When I improve child view rendering, I'd like to have V implement a trait  that
+// provides the scroll state, among other things.
+pub fn collab_panel<V: 'static>(scroll_state: ScrollState) -> CollabPanelElement<V> {
+    CollabPanelElement {
+        view_type: PhantomData,
+        scroll_state,
+    }
+}
+
+impl<V: 'static> CollabPanelElement<V> {
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        // Panel
+        div()
+            .w_64()
+            .h_full()
+            .flex()
+            .flex_col()
+            .font("Zed Sans Extended")
+            .text_color(theme.middle.base.default.foreground)
+            .border_color(theme.middle.base.default.border)
+            .border()
+            .fill(theme.middle.base.default.background)
+            .child(
+                div()
+                    .w_full()
+                    .flex()
+                    .flex_col()
+                    .overflow_y_scroll(self.scroll_state.clone())
+                    // List Container
+                    .child(
+                        div()
+                            .fill(theme.lowest.base.default.background)
+                            .pb_1()
+                            .border_color(theme.lowest.base.default.border)
+                            .border_b()
+                            //:: https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state
+                            // .group()
+                            // List Section Header
+                            .child(self.list_section_header("#CRDB", true, &theme))
+                            // List Item Large
+                            .child(self.list_item(
+                                "http://github.com/maxbrunsfeld.png?s=50",
+                                "maxbrunsfeld",
+                                &theme,
+                            )),
+                    )
+                    .child(
+                        div()
+                            .py_2()
+                            .flex()
+                            .flex_col()
+                            .child(self.list_section_header("CHANNELS", true, &theme)),
+                    )
+                    .child(
+                        div()
+                            .py_2()
+                            .flex()
+                            .flex_col()
+                            .child(self.list_section_header("CONTACTS", true, &theme))
+                            .children(
+                                std::iter::repeat_with(|| {
+                                    vec![
+                                        self.list_item(
+                                            "http://github.com/as-cii.png?s=50",
+                                            "as-cii",
+                                            &theme,
+                                        ),
+                                        self.list_item(
+                                            "http://github.com/nathansobo.png?s=50",
+                                            "nathansobo",
+                                            &theme,
+                                        ),
+                                        self.list_item(
+                                            "http://github.com/maxbrunsfeld.png?s=50",
+                                            "maxbrunsfeld",
+                                            &theme,
+                                        ),
+                                    ]
+                                })
+                                .take(3)
+                                .flatten(),
+                            ),
+                    ),
+            )
+            .child(
+                div()
+                    .h_7()
+                    .px_2()
+                    .border_t()
+                    .border_color(theme.middle.variant.default.border)
+                    .flex()
+                    .items_center()
+                    .child(
+                        div()
+                            .text_sm()
+                            .text_color(theme.middle.variant.default.foreground)
+                            .child("Find..."),
+                    ),
+            )
+    }
+
+    fn list_section_header(
+        &self,
+        label: impl Into<ArcCow<'static, str>>,
+        expanded: bool,
+        theme: &Theme,
+    ) -> impl Element<V> {
+        div()
+            .h_7()
+            .px_2()
+            .flex()
+            .justify_between()
+            .items_center()
+            .child(div().flex().gap_1().text_sm().child(label))
+            .child(
+                div().flex().h_full().gap_1().items_center().child(
+                    svg()
+                        .path(if expanded {
+                            "icons/caret_down.svg"
+                        } else {
+                            "icons/caret_up.svg"
+                        })
+                        .w_3p5()
+                        .h_3p5()
+                        .fill(theme.middle.variant.default.foreground),
+                ),
+            )
+    }
+
+    fn list_item(
+        &self,
+        avatar_uri: impl Into<ArcCow<'static, str>>,
+        label: impl Into<ArcCow<'static, str>>,
+        theme: &Theme,
+    ) -> impl Element<V> {
+        div()
+            .h_7()
+            .px_2()
+            .flex()
+            .items_center()
+            .hover()
+            .fill(theme.lowest.variant.hovered.background)
+            .active()
+            .fill(theme.lowest.variant.pressed.background)
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .gap_1()
+                    .text_sm()
+                    .child(
+                        img()
+                            .uri(avatar_uri)
+                            .size_3p5()
+                            .rounded_full()
+                            .fill(theme.middle.positive.default.foreground),
+                    )
+                    .child(label),
+            )
+    }
+}

crates/ui/src/components/command_palette.rs πŸ”—

@@ -0,0 +1,31 @@
+use gpui2::elements::div;
+use gpui2::{elements::div::ScrollState, ViewContext};
+use gpui2::{Element, IntoElement, ParentElement};
+use std::marker::PhantomData;
+
+use crate::{example_editor_actions, palette, OrderMethod};
+
+#[derive(Element)]
+pub struct CommandPalette<V: 'static> {
+    view_type: PhantomData<V>,
+    scroll_state: ScrollState,
+}
+
+pub fn command_palette<V: 'static>(scroll_state: ScrollState) -> CommandPalette<V> {
+    CommandPalette {
+        view_type: PhantomData,
+        scroll_state,
+    }
+}
+
+impl<V: 'static> CommandPalette<V> {
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        div().child(
+            palette(self.scroll_state.clone())
+                .items(example_editor_actions())
+                .placeholder("Execute a command...")
+                .empty_string("No items found.")
+                .default_order(OrderMethod::Ascending),
+        )
+    }
+}

crates/storybook/src/ui/component/facepile.rs β†’ crates/ui/src/components/facepile.rs πŸ”—

@@ -1,15 +1,18 @@
-use crate::{theme::theme, ui::Avatar};
+use gpui2::elements::div;
 use gpui2::style::StyleHelpers;
-use gpui2::{elements::div, IntoElement};
-use gpui2::{Element, ParentElement, ViewContext};
+use gpui2::{Element, IntoElement, ParentElement, ViewContext};
+
+use crate::{theme, Avatar};
 
 #[derive(Element)]
 pub struct Facepile {
     players: Vec<Avatar>,
 }
 
-pub fn facepile(players: Vec<Avatar>) -> Facepile {
-    Facepile { players }
+pub fn facepile<P: Iterator<Item = Avatar>>(players: P) -> Facepile {
+    Facepile {
+        players: players.collect(),
+    }
 }
 
 impl Facepile {

crates/storybook/src/ui/component/follow_group.rs β†’ crates/ui/src/components/follow_group.rs πŸ”—

@@ -1,8 +1,8 @@
-use crate::theme::theme;
-use crate::ui::{facepile, indicator, Avatar};
+use gpui2::elements::div;
 use gpui2::style::StyleHelpers;
-use gpui2::{elements::div, IntoElement};
-use gpui2::{Element, ParentElement, ViewContext};
+use gpui2::{Element, IntoElement, ParentElement, ViewContext};
+
+use crate::{facepile, indicator, theme, Avatar};
 
 #[derive(Element)]
 pub struct FollowGroup {
@@ -46,7 +46,7 @@ impl FollowGroup {
                     .px_1()
                     .rounded_lg()
                     .fill(player_bg)
-                    .child(facepile(self.players.clone())),
+                    .child(facepile(self.players.clone().into_iter())),
             )
     }
 }

crates/storybook/src/ui/element/icon_button.rs β†’ crates/ui/src/components/icon_button.rs πŸ”—

@@ -1,26 +1,47 @@
-use crate::prelude::{ButtonVariant, InteractionState};
-use crate::theme::theme;
-use gpui2::elements::svg;
+use gpui2::elements::div;
 use gpui2::style::{StyleHelpers, Styleable};
-use gpui2::{elements::div, IntoElement};
-use gpui2::{Element, ParentElement, ViewContext};
+use gpui2::{Element, IntoElement, ParentElement, ViewContext};
+
+use crate::{icon, theme, IconColor};
+use crate::{prelude::*, IconAsset};
 
 #[derive(Element)]
 pub struct IconButton {
-    path: &'static str,
+    icon: IconAsset,
+    color: IconColor,
     variant: ButtonVariant,
     state: InteractionState,
 }
 
-pub fn icon_button(path: &'static str) -> IconButton {
+pub fn icon_button() -> IconButton {
     IconButton {
-        path,
+        icon: IconAsset::default(),
+        color: IconColor::default(),
         variant: ButtonVariant::default(),
         state: InteractionState::default(),
     }
 }
 
 impl IconButton {
+    pub fn new(icon: IconAsset) -> Self {
+        Self {
+            icon,
+            color: IconColor::default(),
+            variant: ButtonVariant::default(),
+            state: InteractionState::default(),
+        }
+    }
+
+    pub fn icon(mut self, icon: IconAsset) -> Self {
+        self.icon = icon;
+        self
+    }
+
+    pub fn color(mut self, color: IconColor) -> Self {
+        self.color = color;
+        self
+    }
+
     pub fn variant(mut self, variant: ButtonVariant) -> Self {
         self.variant = variant;
         self
@@ -34,13 +55,10 @@ impl IconButton {
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
 
-        let icon_color;
-
-        if self.state == InteractionState::Disabled {
-            icon_color = theme.highest.base.disabled.foreground;
-        } else {
-            icon_color = theme.highest.base.default.foreground;
-        }
+        let icon_color = match (self.state, self.color) {
+            (InteractionState::Disabled, _) => IconColor::Disabled,
+            _ => self.color,
+        };
 
         let mut div = div();
         if self.variant == ButtonVariant::Filled {
@@ -57,6 +75,6 @@ impl IconButton {
             .fill(theme.highest.base.hovered.background)
             .active()
             .fill(theme.highest.base.pressed.background)
-            .child(svg().path(self.path).w_4().h_4().fill(icon_color))
+            .child(icon(self.icon).color(icon_color))
     }
 }

crates/ui/src/components/list.rs πŸ”—

@@ -0,0 +1,64 @@
+use crate::theme::theme;
+use crate::tokens::token;
+use crate::{icon, label, prelude::*, IconAsset, LabelColor, ListItem, ListSectionHeader};
+use gpui2::style::StyleHelpers;
+use gpui2::{elements::div, IntoElement};
+use gpui2::{Element, ParentElement, ViewContext};
+
+#[derive(Element)]
+pub struct List {
+    header: Option<ListSectionHeader>,
+    items: Vec<ListItem>,
+    empty_message: &'static str,
+    toggle: Option<ToggleState>,
+    // footer: Option<ListSectionFooter>,
+}
+
+pub fn list(items: Vec<ListItem>) -> List {
+    List {
+        header: None,
+        items,
+        empty_message: "No items",
+        toggle: None,
+    }
+}
+
+impl List {
+    pub fn header(mut self, header: ListSectionHeader) -> Self {
+        self.header = Some(header);
+        self
+    }
+
+    pub fn empty_message(mut self, empty_message: &'static str) -> Self {
+        self.empty_message = empty_message;
+        self
+    }
+
+    pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
+        self.toggle = Some(toggle);
+        self
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let token = token();
+
+        let disclosure_control = match self.toggle {
+            Some(ToggleState::NotToggled) => Some(icon(IconAsset::ChevronRight)),
+            Some(ToggleState::Toggled) => Some(icon(IconAsset::ChevronDown)),
+            None => None,
+        };
+
+        div()
+            .py_1()
+            .flex()
+            .flex_col()
+            .children(self.header.map(|h| h))
+            .children(
+                self.items
+                    .is_empty()
+                    .then(|| label(self.empty_message).color(LabelColor::Muted)),
+            )
+            .children(self.items.iter().cloned())
+    }
+}

crates/storybook/src/ui/component/list_item.rs β†’ crates/ui/src/components/list_item.rs πŸ”—

@@ -1,17 +1,18 @@
-use crate::prelude::{InteractionState, ToggleState};
+use crate::prelude::{DisclosureControlVisibility, InteractionState, ToggleState};
 use crate::theme::theme;
-use crate::ui::{icon, IconAsset, Label};
-use gpui2::geometry::rems;
+use crate::tokens::token;
+use crate::{icon, IconAsset, Label};
 use gpui2::style::{StyleHelpers, Styleable};
 use gpui2::{elements::div, IntoElement};
 use gpui2::{Element, ParentElement, ViewContext};
 
-#[derive(Element)]
+#[derive(Element, Clone)]
 pub struct ListItem {
     label: Label,
     left_icon: Option<IconAsset>,
     indent_level: u32,
     state: InteractionState,
+    disclosure_control_style: DisclosureControlVisibility,
     toggle: Option<ToggleState>,
 }
 
@@ -20,6 +21,7 @@ pub fn list_item(label: Label) -> ListItem {
         label,
         indent_level: 0,
         left_icon: None,
+        disclosure_control_style: DisclosureControlVisibility::default(),
         state: InteractionState::default(),
         toggle: None,
     }
@@ -46,8 +48,30 @@ impl ListItem {
         self
     }
 
+    pub fn disclosure_control_style(
+        mut self,
+        disclosure_control_style: DisclosureControlVisibility,
+    ) -> Self {
+        self.disclosure_control_style = disclosure_control_style;
+        self
+    }
+
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
+        let token = token();
+        let mut disclosure_control = match self.toggle {
+            Some(ToggleState::NotToggled) => Some(div().child(icon(IconAsset::ChevronRight))),
+            Some(ToggleState::Toggled) => Some(div().child(icon(IconAsset::ChevronDown))),
+            None => Some(div()),
+        };
+
+        match self.disclosure_control_style {
+            DisclosureControlVisibility::OnHover => {
+                disclosure_control =
+                    disclosure_control.map(|c| div().absolute().neg_left_5().child(c));
+            }
+            DisclosureControlVisibility::Always => {}
+        }
 
         div()
             .fill(theme.middle.base.default.background)
@@ -56,31 +80,31 @@ impl ListItem {
             .active()
             .fill(theme.middle.base.pressed.background)
             .relative()
+            .py_1()
             .child(
                 div()
-                    .h_7()
+                    .h_6()
                     .px_2()
                     // .ml(rems(0.75 * self.indent_level as f32))
                     .children((0..self.indent_level).map(|_| {
-                        div().w(rems(0.75)).h_full().flex().justify_center().child(
-                            div()
-                                .w_px()
-                                .h_full()
-                                .fill(theme.middle.base.default.border)
-                                .hover()
-                                .fill(theme.middle.warning.default.border)
-                                .active()
-                                .fill(theme.middle.negative.default.border),
-                        )
+                        div()
+                            .w(token.list_indent_depth)
+                            .h_full()
+                            .flex()
+                            .justify_center()
+                            .child(
+                                div()
+                                    .ml_px()
+                                    .w_px()
+                                    .h_full()
+                                    .fill(theme.middle.base.default.border),
+                            )
                     }))
                     .flex()
-                    .gap_2()
+                    .gap_1()
                     .items_center()
-                    .children(match self.toggle {
-                        Some(ToggleState::NotToggled) => Some(icon(IconAsset::ChevronRight)),
-                        Some(ToggleState::Toggled) => Some(icon(IconAsset::ChevronDown)),
-                        None => None,
-                    })
+                    .relative()
+                    .children(disclosure_control)
                     .children(self.left_icon.map(|i| icon(i)))
                     .child(self.label.clone()),
             )

crates/ui/src/components/list_section_header.rs πŸ”—

@@ -0,0 +1,88 @@
+use crate::prelude::{InteractionState, ToggleState};
+use crate::theme::theme;
+use crate::tokens::token;
+use crate::{icon, label, IconAsset, LabelColor, LabelSize};
+use gpui2::style::{StyleHelpers, Styleable};
+use gpui2::{elements::div, IntoElement};
+use gpui2::{Element, ParentElement, ViewContext};
+
+#[derive(Element, Clone, Copy)]
+pub struct ListSectionHeader {
+    label: &'static str,
+    left_icon: Option<IconAsset>,
+    state: InteractionState,
+    toggle: Option<ToggleState>,
+}
+
+pub fn list_section_header(label: &'static str) -> ListSectionHeader {
+    ListSectionHeader {
+        label,
+        left_icon: None,
+        state: InteractionState::default(),
+        toggle: None,
+    }
+}
+
+impl ListSectionHeader {
+    pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
+        self.toggle = Some(toggle);
+        self
+    }
+
+    pub fn left_icon(mut self, left_icon: Option<IconAsset>) -> Self {
+        self.left_icon = left_icon;
+        self
+    }
+
+    pub fn state(mut self, state: InteractionState) -> Self {
+        self.state = state;
+        self
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let token = token();
+
+        let disclosure_control = match self.toggle {
+            Some(ToggleState::NotToggled) => Some(div().child(icon(IconAsset::ChevronRight))),
+            Some(ToggleState::Toggled) => Some(div().child(icon(IconAsset::ChevronDown))),
+            None => Some(div()),
+        };
+
+        div()
+            .flex()
+            .flex_1()
+            .w_full()
+            .fill(theme.middle.base.default.background)
+            .hover()
+            .fill(theme.middle.base.hovered.background)
+            .active()
+            .fill(theme.middle.base.pressed.background)
+            .relative()
+            .py_1()
+            .child(
+                div()
+                    .h_6()
+                    .px_2()
+                    .flex()
+                    .flex_1()
+                    .w_full()
+                    .gap_1()
+                    .items_center()
+                    .justify_between()
+                    .child(
+                        div()
+                            .flex()
+                            .gap_1()
+                            .items_center()
+                            .children(self.left_icon.map(|i| icon(i)))
+                            .child(
+                                label(self.label.clone())
+                                    .color(LabelColor::Muted)
+                                    .size(LabelSize::Small),
+                            ),
+                    )
+                    .children(disclosure_control),
+            )
+    }
+}

crates/ui/src/components/palette.rs πŸ”—

@@ -0,0 +1,124 @@
+use std::marker::PhantomData;
+
+use crate::prelude::OrderMethod;
+use crate::theme::theme;
+use crate::{label, palette_item, LabelColor, PaletteItem};
+use gpui2::elements::div::ScrollState;
+use gpui2::style::{StyleHelpers, Styleable};
+use gpui2::{elements::div, IntoElement};
+use gpui2::{Element, ParentElement, ViewContext};
+
+#[derive(Element)]
+pub struct Palette<V: 'static> {
+    view_type: PhantomData<V>,
+    scroll_state: ScrollState,
+    input_placeholder: &'static str,
+    empty_string: &'static str,
+    items: Vec<PaletteItem>,
+    default_order: OrderMethod,
+}
+
+pub fn palette<V: 'static>(scroll_state: ScrollState) -> Palette<V> {
+    Palette {
+        view_type: PhantomData,
+        scroll_state,
+        input_placeholder: "Find something...",
+        empty_string: "No items found.",
+        items: vec![],
+        default_order: OrderMethod::default(),
+    }
+}
+
+impl<V: 'static> Palette<V> {
+    pub fn items(mut self, mut items: Vec<PaletteItem>) -> Self {
+        items.sort_by_key(|item| item.label);
+        self.items = items;
+        self
+    }
+
+    pub fn placeholder(mut self, input_placeholder: &'static str) -> Self {
+        self.input_placeholder = input_placeholder;
+        self
+    }
+
+    pub fn empty_string(mut self, empty_string: &'static str) -> Self {
+        self.empty_string = empty_string;
+        self
+    }
+
+    // TODO: Hook up sort order
+    pub fn default_order(mut self, default_order: OrderMethod) -> Self {
+        self.default_order = default_order;
+        self
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .w_96()
+            .rounded_lg()
+            .fill(theme.lowest.base.default.background)
+            .border()
+            .border_color(theme.lowest.base.default.border)
+            .flex()
+            .flex_col()
+            .child(
+                div()
+                    .flex()
+                    .flex_col()
+                    .gap_px()
+                    .child(
+                        div().py_0p5().px_1().flex().flex_col().child(
+                            div().px_2().py_0p5().child(
+                                label(self.input_placeholder).color(LabelColor::Placeholder),
+                            ),
+                        ),
+                    )
+                    .child(div().h_px().w_full().fill(theme.lowest.base.default.border))
+                    .child(
+                        div()
+                            .py_0p5()
+                            .px_1()
+                            .flex()
+                            .flex_col()
+                            .grow()
+                            .max_h_96()
+                            .overflow_y_scroll(self.scroll_state.clone())
+                            .children(
+                                vec![if self.items.is_empty() {
+                                    Some(
+                                        div()
+                                            .flex()
+                                            .flex_row()
+                                            .justify_between()
+                                            .px_2()
+                                            .py_1()
+                                            .child(
+                                                label(self.empty_string).color(LabelColor::Muted),
+                                            ),
+                                    )
+                                } else {
+                                    None
+                                }]
+                                .into_iter()
+                                .flatten(),
+                            )
+                            .children(self.items.iter().map(|item| {
+                                div()
+                                    .flex()
+                                    .flex_row()
+                                    .justify_between()
+                                    .px_2()
+                                    .py_0p5()
+                                    .rounded_lg()
+                                    .hover()
+                                    .fill(theme.lowest.base.hovered.background)
+                                    .active()
+                                    .fill(theme.lowest.base.pressed.background)
+                                    .child(palette_item(item.label, item.keybinding))
+                            })),
+                    ),
+            )
+    }
+}

crates/ui/src/components/palette_item.rs πŸ”—

@@ -0,0 +1,63 @@
+use crate::theme::theme;
+use crate::{label, LabelColor, LabelSize};
+use gpui2::elements::div;
+use gpui2::style::StyleHelpers;
+use gpui2::{Element, IntoElement};
+use gpui2::{ParentElement, ViewContext};
+
+#[derive(Element)]
+pub struct PaletteItem {
+    pub label: &'static str,
+    pub keybinding: Option<&'static str>,
+}
+
+pub fn palette_item(label: &'static str, keybinding: Option<&'static str>) -> PaletteItem {
+    PaletteItem { label, keybinding }
+}
+
+impl PaletteItem {
+    pub fn label(mut self, label: &'static str) -> Self {
+        self.label = label;
+        self
+    }
+
+    pub fn keybinding(mut self, keybinding: Option<&'static str>) -> Self {
+        self.keybinding = keybinding;
+        self
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        let keybinding_label = match self.keybinding {
+            Some(keybind) => label(keybind)
+                .color(LabelColor::Muted)
+                .size(LabelSize::Small),
+            None => label(""),
+        };
+
+        div()
+            .flex()
+            .flex_row()
+            .grow()
+            .justify_between()
+            .child(label(self.label))
+            .child(
+                self.keybinding
+                    .map(|_| {
+                        div()
+                            .flex()
+                            .items_center()
+                            .justify_center()
+                            .px_1()
+                            .py_0()
+                            .my_0p5()
+                            .rounded_md()
+                            .text_sm()
+                            .fill(theme.lowest.on.default.background)
+                            .child(keybinding_label)
+                    })
+                    .unwrap_or_else(|| div()),
+            )
+    }
+}

crates/ui/src/components/project_panel.rs πŸ”—

@@ -0,0 +1,62 @@
+use crate::{
+    input, list, list_section_header, prelude::*, static_project_panel_project_items,
+    static_project_panel_single_items, theme,
+};
+
+use gpui2::{
+    elements::{div, div::ScrollState},
+    style::StyleHelpers,
+    ParentElement, ViewContext,
+};
+use gpui2::{Element, IntoElement};
+use std::marker::PhantomData;
+
+#[derive(Element)]
+pub struct ProjectPanel<V: 'static> {
+    view_type: PhantomData<V>,
+    scroll_state: ScrollState,
+}
+
+pub fn project_panel<V: 'static>(scroll_state: ScrollState) -> ProjectPanel<V> {
+    ProjectPanel {
+        view_type: PhantomData,
+        scroll_state,
+    }
+}
+
+impl<V: 'static> ProjectPanel<V> {
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .w_56()
+            .h_full()
+            .flex()
+            .flex_col()
+            .fill(theme.middle.base.default.background)
+            .child(
+                div()
+                    .w_56()
+                    .flex()
+                    .flex_col()
+                    .overflow_y_scroll(self.scroll_state.clone())
+                    .child(
+                        list(static_project_panel_single_items())
+                            .header(list_section_header("FILES").set_toggle(ToggleState::Toggled))
+                            .empty_message("No files in directory")
+                            .set_toggle(ToggleState::Toggled),
+                    )
+                    .child(
+                        list(static_project_panel_project_items())
+                            .header(list_section_header("PROJECT").set_toggle(ToggleState::Toggled))
+                            .empty_message("No folders in directory")
+                            .set_toggle(ToggleState::Toggled),
+                    ),
+            )
+            .child(
+                input("Find something...")
+                    .value("buffe".to_string())
+                    .state(InteractionState::Focused),
+            )
+    }
+}

crates/storybook/src/ui/module/status_bar.rs β†’ crates/ui/src/components/status_bar.rs πŸ”—

@@ -1,11 +1,12 @@
 use std::marker::PhantomData;
 
-use crate::theme::{theme, Theme};
-use crate::ui::{icon_button, text_button, tool_divider};
 use gpui2::style::StyleHelpers;
 use gpui2::{elements::div, IntoElement};
 use gpui2::{Element, ParentElement, ViewContext};
 
+use crate::theme::{theme, Theme};
+use crate::{icon_button, text_button, tool_divider, IconAsset};
+
 #[derive(Default, PartialEq)]
 pub enum Tool {
     #[default]
@@ -96,8 +97,8 @@ impl<V: 'static> StatusBar<V> {
             .justify_between()
             .w_full()
             .fill(theme.lowest.base.default.background)
-            .child(self.left_tools(theme))
-            .child(self.right_tools(theme))
+            .child(self.left_tools(&theme))
+            .child(self.right_tools(&theme))
     }
 
     fn left_tools(&self, theme: &Theme) -> impl Element<V> {
@@ -105,10 +106,10 @@ impl<V: 'static> StatusBar<V> {
             .flex()
             .items_center()
             .gap_1()
-            .child(icon_button("icons/project.svg"))
-            .child(icon_button("icons/hash.svg"))
+            .child(icon_button().icon(IconAsset::FileTree))
+            .child(icon_button().icon(IconAsset::Hash))
             .child(tool_divider())
-            .child(icon_button("icons/error.svg"))
+            .child(icon_button().icon(IconAsset::XCircle))
     }
     fn right_tools(&self, theme: &Theme) -> impl Element<V> {
         div()
@@ -129,8 +130,8 @@ impl<V: 'static> StatusBar<V> {
                     .flex()
                     .items_center()
                     .gap_1()
-                    .child(icon_button("icons/copilot.svg"))
-                    .child(icon_button("icons/feedback.svg")),
+                    .child(icon_button().icon(IconAsset::Copilot))
+                    .child(icon_button().icon(IconAsset::Envelope)),
             )
             .child(tool_divider())
             .child(
@@ -138,9 +139,9 @@ impl<V: 'static> StatusBar<V> {
                     .flex()
                     .items_center()
                     .gap_1()
-                    .child(icon_button("icons/terminal.svg"))
-                    .child(icon_button("icons/conversations.svg"))
-                    .child(icon_button("icons/ai.svg")),
+                    .child(icon_button().icon(IconAsset::Terminal))
+                    .child(icon_button().icon(IconAsset::MessageBubbles))
+                    .child(icon_button().icon(IconAsset::Ai)),
             )
     }
 }

crates/storybook/src/ui/component/tab.rs β†’ crates/ui/src/components/tab.rs πŸ”—

@@ -1,7 +1,8 @@
-use crate::theme::theme;
+use gpui2::elements::div;
 use gpui2::style::{StyleHelpers, Styleable};
-use gpui2::{elements::div, IntoElement};
-use gpui2::{Element, ParentElement, ViewContext};
+use gpui2::{Element, IntoElement, ParentElement, ViewContext};
+
+use crate::theme;
 
 #[derive(Element)]
 pub struct Tab {

crates/storybook/src/ui/module/tab_bar.rs β†’ crates/ui/src/components/tab_bar.rs πŸ”—

@@ -1,13 +1,14 @@
 use std::marker::PhantomData;
 
-use crate::prelude::InteractionState;
-use crate::theme::theme;
-use crate::ui::{icon_button, tab};
 use gpui2::elements::div::ScrollState;
 use gpui2::style::StyleHelpers;
 use gpui2::{elements::div, IntoElement};
 use gpui2::{Element, ParentElement, ViewContext};
 
+use crate::prelude::InteractionState;
+use crate::theme::theme;
+use crate::{icon_button, tab, IconAsset};
+
 #[derive(Element)]
 pub struct TabBar<V: 'static> {
     view_type: PhantomData<V>,
@@ -43,11 +44,12 @@ impl<V: 'static> TabBar<V> {
                             .items_center()
                             .gap_px()
                             .child(
-                                icon_button("icons/arrow_left.svg")
+                                icon_button()
+                                    .icon(IconAsset::ArrowLeft)
                                     .state(InteractionState::Enabled.if_enabled(can_navigate_back)),
                             )
                             .child(
-                                icon_button("icons/arrow_right.svg").state(
+                                icon_button().icon(IconAsset::ArrowRight).state(
                                     InteractionState::Enabled.if_enabled(can_navigate_forward),
                                 ),
                             ),
@@ -83,8 +85,8 @@ impl<V: 'static> TabBar<V> {
                             .flex()
                             .items_center()
                             .gap_px()
-                            .child(icon_button("icons/plus.svg"))
-                            .child(icon_button("icons/split.svg")),
+                            .child(icon_button().icon(IconAsset::Plus))
+                            .child(icon_button().icon(IconAsset::Split)),
                     ),
             )
     }

crates/storybook/src/ui/module/title_bar.rs β†’ crates/ui/src/components/title_bar.rs πŸ”—

@@ -1,11 +1,14 @@
 use std::marker::PhantomData;
 
-use crate::prelude::Shape;
-use crate::theme::theme;
-use crate::ui::{avatar, follow_group, icon_button, text_button, tool_divider};
+use gpui2::elements::div;
 use gpui2::style::StyleHelpers;
-use gpui2::{elements::div, IntoElement};
-use gpui2::{Element, ParentElement, ViewContext};
+use gpui2::{Element, IntoElement, ParentElement, ViewContext};
+
+use crate::prelude::Shape;
+use crate::{
+    avatar, follow_group, icon_button, text_button, theme, tool_divider, traffic_lights, IconAsset,
+    IconColor,
+};
 
 #[derive(Element)]
 pub struct TitleBar<V: 'static> {
@@ -40,34 +43,7 @@ impl<V: 'static> TitleBar<V> {
                     .h_full()
                     .gap_4()
                     .px_2()
-                    // === Traffic Lights === //
-                    .child(
-                        div()
-                            .flex()
-                            .items_center()
-                            .gap_2()
-                            .child(
-                                div()
-                                    .w_3()
-                                    .h_3()
-                                    .rounded_full()
-                                    .fill(theme.lowest.positive.default.foreground),
-                            )
-                            .child(
-                                div()
-                                    .w_3()
-                                    .h_3()
-                                    .rounded_full()
-                                    .fill(theme.lowest.warning.default.foreground),
-                            )
-                            .child(
-                                div()
-                                    .w_3()
-                                    .h_3()
-                                    .rounded_full()
-                                    .fill(theme.lowest.negative.default.foreground),
-                            ),
-                    )
+                    .child(traffic_lights())
                     // === Project Info === //
                     .child(
                         div()
@@ -92,8 +68,8 @@ impl<V: 'static> TitleBar<V> {
                             .flex()
                             .items_center()
                             .gap_1()
-                            .child(icon_button("icons/stop_sharing.svg"))
-                            .child(icon_button("icons/exit.svg")),
+                            .child(icon_button().icon(IconAsset::FolderX))
+                            .child(icon_button().icon(IconAsset::Close)),
                     )
                     .child(tool_divider())
                     .child(
@@ -102,9 +78,13 @@ impl<V: 'static> TitleBar<V> {
                             .flex()
                             .items_center()
                             .gap_1()
-                            .child(icon_button("icons/mic.svg"))
-                            .child(icon_button("icons/speaker-loud.svg"))
-                            .child(icon_button("icons/desktop.svg")),
+                            .child(icon_button().icon(IconAsset::Mic))
+                            .child(icon_button().icon(IconAsset::AudioOn))
+                            .child(
+                                icon_button()
+                                    .icon(IconAsset::Screen)
+                                    .color(IconColor::Accent),
+                            ),
                     )
                     .child(
                         div().px_2().flex().items_center().child(

crates/ui/src/components/toolbar.rs πŸ”—

@@ -0,0 +1,35 @@
+use gpui2::elements::div;
+use gpui2::style::StyleHelpers;
+use gpui2::{Element, IntoElement, ParentElement, ViewContext};
+
+use crate::{breadcrumb, theme, IconAsset, IconButton};
+
+pub struct ToolbarItem {}
+
+#[derive(Element)]
+pub struct Toolbar {
+    items: Vec<ToolbarItem>,
+}
+
+pub fn toolbar() -> Toolbar {
+    Toolbar { items: Vec::new() }
+}
+
+impl Toolbar {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .p_2()
+            .flex()
+            .justify_between()
+            .child(breadcrumb())
+            .child(
+                div()
+                    .flex()
+                    .child(IconButton::new(IconAsset::InlayHint))
+                    .child(IconButton::new(IconAsset::MagnifyingGlass))
+                    .child(IconButton::new(IconAsset::MagicWand)),
+            )
+    }
+}

crates/ui/src/components/traffic_lights.rs πŸ”—

@@ -0,0 +1,30 @@
+use gpui2::elements::div;
+use gpui2::style::StyleHelpers;
+use gpui2::{Element, Hsla, IntoElement, ParentElement, ViewContext};
+
+use crate::theme;
+
+#[derive(Element)]
+pub struct TrafficLights {}
+
+pub fn traffic_lights() -> TrafficLights {
+    TrafficLights {}
+}
+
+impl TrafficLights {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .flex()
+            .items_center()
+            .gap_2()
+            .child(traffic_light(theme.lowest.negative.default.foreground))
+            .child(traffic_light(theme.lowest.warning.default.foreground))
+            .child(traffic_light(theme.lowest.positive.default.foreground))
+    }
+}
+
+fn traffic_light<V: 'static, C: Into<Hsla>>(fill: C) -> div::Div<V> {
+    div().w_3().h_3().rounded_full().fill(fill.into())
+}

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

@@ -0,0 +1,80 @@
+use crate::{chat_panel, collab_panel, project_panel, status_bar, tab_bar, theme, title_bar};
+
+use gpui2::{
+    elements::{div, div::ScrollState},
+    style::StyleHelpers,
+    Element, IntoElement, ParentElement, ViewContext,
+};
+
+#[derive(Element, Default)]
+struct WorkspaceElement {
+    project_panel_scroll_state: ScrollState,
+    collab_panel_scroll_state: ScrollState,
+    right_scroll_state: ScrollState,
+    tab_bar_scroll_state: ScrollState,
+    palette_scroll_state: ScrollState,
+}
+
+pub fn workspace<V: 'static>() -> impl Element<V> {
+    WorkspaceElement::default()
+}
+
+impl WorkspaceElement {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            // Elevation Level 0
+            .size_full()
+            .flex()
+            .flex_col()
+            .font("Zed Sans Extended")
+            .gap_0()
+            .justify_start()
+            .items_start()
+            .text_color(theme.lowest.base.default.foreground)
+            .fill(theme.lowest.base.default.background)
+            .relative()
+            // Elevation Level 1
+            .child(title_bar())
+            .child(
+                div()
+                    .flex_1()
+                    .w_full()
+                    .flex()
+                    .flex_row()
+                    .overflow_hidden()
+                    .child(project_panel(self.project_panel_scroll_state.clone()))
+                    .child(collab_panel(self.collab_panel_scroll_state.clone()))
+                    .child(
+                        div()
+                            .h_full()
+                            .flex_1()
+                            .fill(theme.highest.base.default.background)
+                            .child(
+                                div()
+                                    .flex()
+                                    .flex_col()
+                                    .flex_1()
+                                    .child(tab_bar(self.tab_bar_scroll_state.clone())),
+                            ),
+                    )
+                    .child(chat_panel(self.right_scroll_state.clone())),
+            )
+            .child(status_bar())
+        // Elevation Level 3
+        // .child(
+        //     div()
+        //         .absolute()
+        //         .top_0()
+        //         .left_0()
+        //         .size_full()
+        //         .flex()
+        //         .justify_center()
+        //         .items_center()
+        //         // .fill(theme.lowest.base.default.background)
+        //         // Elevation Level 4
+        //         .child(command_palette(self.palette_scroll_state.clone())),
+        // )
+    }
+}

crates/storybook/src/element_ext.rs β†’ crates/ui/src/element_ext.rs πŸ”—

@@ -1,7 +1,9 @@
-use crate::theme::{Theme, Themed};
-use gpui2::Element;
 use std::marker::PhantomData;
 
+use gpui2::Element;
+
+use crate::theme::{Theme, Themed};
+
 pub trait ElementExt<V: 'static>: Element<V> {
     fn themed(self, theme: Theme) -> Themed<V, Self>
     where

crates/ui/src/elements.rs πŸ”—

@@ -0,0 +1,17 @@
+mod avatar;
+mod details;
+mod icon;
+mod indicator;
+mod input;
+mod label;
+mod text_button;
+mod tool_divider;
+
+pub use avatar::*;
+pub use details::*;
+pub use icon::*;
+pub use indicator::*;
+pub use input::*;
+pub use label::*;
+pub use text_button::*;
+pub use tool_divider::*;

crates/storybook/src/ui/element/avatar.rs β†’ crates/ui/src/elements/avatar.rs πŸ”—

@@ -1,9 +1,9 @@
-use crate::prelude::Shape;
-use crate::theme::theme;
 use gpui2::elements::img;
 use gpui2::style::StyleHelpers;
-use gpui2::{ArcCow, IntoElement};
-use gpui2::{Element, ViewContext};
+use gpui2::{ArcCow, Element, IntoElement, ViewContext};
+
+use crate::prelude::*;
+use crate::theme;
 
 #[derive(Element, Clone)]
 pub struct Avatar {

crates/storybook/src/ui/element/details.rs β†’ crates/ui/src/elements/details.rs πŸ”—

@@ -1,8 +1,8 @@
-use crate::theme::theme;
 use gpui2::elements::div;
 use gpui2::style::StyleHelpers;
-use gpui2::{Element, ViewContext};
-use gpui2::{IntoElement, ParentElement};
+use gpui2::{Element, IntoElement, ParentElement, ViewContext};
+
+use crate::theme;
 
 #[derive(Element, Clone)]
 pub struct Details {

crates/ui/src/elements/icon.rs πŸ”—

@@ -0,0 +1,155 @@
+use std::sync::Arc;
+
+use crate::theme::theme;
+use crate::Theme;
+use gpui2::elements::svg;
+use gpui2::style::StyleHelpers;
+use gpui2::{Element, ViewContext};
+use gpui2::{Hsla, IntoElement};
+
+#[derive(Default, PartialEq, Copy, Clone)]
+pub enum IconColor {
+    #[default]
+    Default,
+    Muted,
+    Disabled,
+    Placeholder,
+    Accent,
+    Error,
+    Warning,
+    Success,
+    Info,
+}
+
+impl IconColor {
+    pub fn color(self, theme: Arc<Theme>) -> Hsla {
+        match self {
+            IconColor::Default => theme.lowest.base.default.foreground,
+            IconColor::Muted => theme.lowest.variant.default.foreground,
+            IconColor::Disabled => theme.lowest.base.disabled.foreground,
+            IconColor::Placeholder => theme.lowest.base.disabled.foreground,
+            IconColor::Accent => theme.lowest.accent.default.foreground,
+            IconColor::Error => theme.lowest.negative.default.foreground,
+            IconColor::Warning => theme.lowest.warning.default.foreground,
+            IconColor::Success => theme.lowest.positive.default.foreground,
+            IconColor::Info => theme.lowest.accent.default.foreground,
+        }
+    }
+}
+
+#[derive(Default, PartialEq, Copy, Clone)]
+pub enum IconAsset {
+    Ai,
+    ArrowLeft,
+    ArrowRight,
+    ArrowUpRight,
+    AudioOff,
+    AudioOn,
+    Bolt,
+    ChevronDown,
+    ChevronLeft,
+    ChevronRight,
+    ChevronUp,
+    Close,
+    ExclamationTriangle,
+    File,
+    FileDoc,
+    FileGit,
+    FileLock,
+    FileRust,
+    FileToml,
+    FileTree,
+    Folder,
+    FolderOpen,
+    FolderX,
+    #[default]
+    Hash,
+    InlayHint,
+    MagicWand,
+    MagnifyingGlass,
+    MessageBubbles,
+    Mic,
+    MicMute,
+    Plus,
+    Screen,
+    Split,
+    Terminal,
+    XCircle,
+    Copilot,
+    Envelope,
+}
+
+impl IconAsset {
+    pub fn path(self) -> &'static str {
+        match self {
+            IconAsset::Ai => "icons/ai.svg",
+            IconAsset::ArrowLeft => "icons/arrow_left.svg",
+            IconAsset::ArrowRight => "icons/arrow_right.svg",
+            IconAsset::ArrowUpRight => "icons/arrow_up_right.svg",
+            IconAsset::AudioOff => "icons/speaker-off.svg",
+            IconAsset::AudioOn => "icons/speaker-loud.svg",
+            IconAsset::Bolt => "icons/bolt.svg",
+            IconAsset::ChevronDown => "icons/chevron_down.svg",
+            IconAsset::ChevronLeft => "icons/chevron_left.svg",
+            IconAsset::ChevronRight => "icons/chevron_right.svg",
+            IconAsset::ChevronUp => "icons/chevron_up.svg",
+            IconAsset::Close => "icons/x.svg",
+            IconAsset::ExclamationTriangle => "icons/warning.svg",
+            IconAsset::File => "icons/file_icons/file.svg",
+            IconAsset::FileDoc => "icons/file_icons/book.svg",
+            IconAsset::FileGit => "icons/file_icons/git.svg",
+            IconAsset::FileLock => "icons/file_icons/lock.svg",
+            IconAsset::FileRust => "icons/file_icons/rust.svg",
+            IconAsset::FileToml => "icons/file_icons/toml.svg",
+            IconAsset::FileTree => "icons/project.svg",
+            IconAsset::Folder => "icons/file_icons/folder.svg",
+            IconAsset::FolderOpen => "icons/file_icons/folder_open.svg",
+            IconAsset::FolderX => "icons/stop_sharing.svg",
+            IconAsset::Hash => "icons/hash.svg",
+            IconAsset::InlayHint => "icons/inlay_hint.svg",
+            IconAsset::MagicWand => "icons/magic-wand.svg",
+            IconAsset::MagnifyingGlass => "icons/magnifying_glass.svg",
+            IconAsset::MessageBubbles => "icons/conversations.svg",
+            IconAsset::Mic => "icons/mic.svg",
+            IconAsset::MicMute => "icons/mic-mute.svg",
+            IconAsset::Plus => "icons/plus.svg",
+            IconAsset::Screen => "icons/desktop.svg",
+            IconAsset::Split => "icons/split.svg",
+            IconAsset::Terminal => "icons/terminal.svg",
+            IconAsset::XCircle => "icons/error.svg",
+            IconAsset::Copilot => "icons/copilot.svg",
+            IconAsset::Envelope => "icons/feedback.svg",
+        }
+    }
+}
+
+#[derive(Element, Clone)]
+pub struct Icon {
+    asset: IconAsset,
+    color: IconColor,
+}
+
+pub fn icon(asset: IconAsset) -> Icon {
+    Icon {
+        asset,
+        color: IconColor::default(),
+    }
+}
+
+impl Icon {
+    pub fn color(mut self, color: IconColor) -> Self {
+        self.color = color;
+        self
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let fill = self.color.color(theme);
+
+        svg()
+            .flex_none()
+            .path(self.asset.path())
+            .size_4()
+            .fill(fill)
+    }
+}

crates/storybook/src/ui/element/indicator.rs β†’ crates/ui/src/elements/indicator.rs πŸ”—

@@ -1,7 +1,8 @@
-use crate::theme::theme;
+use gpui2::elements::div;
 use gpui2::style::StyleHelpers;
-use gpui2::{elements::div, IntoElement};
-use gpui2::{Element, ViewContext};
+use gpui2::{Element, IntoElement, ViewContext};
+
+use crate::theme;
 
 #[derive(Element)]
 pub struct Indicator {

crates/storybook/src/ui/element/input.rs β†’ crates/ui/src/elements/input.rs πŸ”—

@@ -1,8 +1,9 @@
-use crate::prelude::{InputVariant, InteractionState};
-use crate::theme::theme;
+use gpui2::elements::div;
 use gpui2::style::{StyleHelpers, Styleable};
-use gpui2::{elements::div, IntoElement};
-use gpui2::{Element, ParentElement, ViewContext};
+use gpui2::{Element, IntoElement, ParentElement, ViewContext};
+
+use crate::prelude::*;
+use crate::theme;
 
 #[derive(Element)]
 pub struct Input {

crates/storybook/src/ui/element/label.rs β†’ crates/ui/src/elements/label.rs πŸ”—

@@ -8,22 +8,33 @@ use gpui2::{IntoElement, ParentElement};
 pub enum LabelColor {
     #[default]
     Default,
+    Muted,
     Created,
     Modified,
     Deleted,
     Hidden,
+    Placeholder,
+}
+
+#[derive(Default, PartialEq, Copy, Clone)]
+pub enum LabelSize {
+    #[default]
+    Default,
+    Small,
 }
 
 #[derive(Element, Clone)]
 pub struct Label {
     label: &'static str,
     color: LabelColor,
+    size: LabelSize,
 }
 
 pub fn label(label: &'static str) -> Label {
     Label {
         label,
         color: LabelColor::Default,
+        size: LabelSize::Default,
     }
 }
 
@@ -33,17 +44,32 @@ impl Label {
         self
     }
 
+    pub fn size(mut self, size: LabelSize) -> Self {
+        self.size = size;
+        self
+    }
+
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
 
         let color = match self.color {
             LabelColor::Default => theme.lowest.base.default.foreground,
+            LabelColor::Muted => theme.lowest.variant.default.foreground,
             LabelColor::Created => theme.lowest.positive.default.foreground,
             LabelColor::Modified => theme.lowest.warning.default.foreground,
             LabelColor::Deleted => theme.lowest.negative.default.foreground,
             LabelColor::Hidden => theme.lowest.variant.default.foreground,
+            LabelColor::Placeholder => theme.lowest.base.disabled.foreground,
         };
 
-        div().text_sm().text_color(color).child(self.label.clone())
+        let mut div = div();
+
+        if self.size == LabelSize::Small {
+            div = div.text_xs();
+        } else {
+            div = div.text_sm();
+        }
+
+        div.text_color(color).child(self.label.clone())
     }
 }

crates/storybook/src/ui/element/text_button.rs β†’ crates/ui/src/elements/text_button.rs πŸ”—

@@ -1,8 +1,9 @@
-use crate::prelude::{ButtonVariant, InteractionState};
-use crate::theme::theme;
+use gpui2::elements::div;
 use gpui2::style::{StyleHelpers, Styleable};
-use gpui2::{elements::div, IntoElement};
-use gpui2::{Element, ParentElement, ViewContext};
+use gpui2::{Element, IntoElement, ParentElement, ViewContext};
+
+use crate::prelude::*;
+use crate::theme;
 
 #[derive(Element)]
 pub struct TextButton {

crates/storybook/src/ui/element/tool_divider.rs β†’ crates/ui/src/elements/tool_divider.rs πŸ”—

@@ -1,7 +1,8 @@
-use crate::theme::theme;
+use gpui2::elements::div;
 use gpui2::style::StyleHelpers;
-use gpui2::{elements::div, IntoElement};
-use gpui2::{Element, ViewContext};
+use gpui2::{Element, IntoElement, ViewContext};
+
+use crate::theme;
 
 #[derive(Element)]
 pub struct ToolDivider {}

crates/ui/src/lib.rs πŸ”—

@@ -0,0 +1,17 @@
+#![allow(dead_code, unused_variables)]
+
+mod components;
+mod element_ext;
+mod elements;
+pub mod prelude;
+mod static_data;
+mod theme;
+mod tokens;
+
+pub use crate::theme::*;
+pub use components::*;
+pub use element_ext::*;
+pub use elements::*;
+pub use prelude::*;
+pub use static_data::*;
+pub use tokens::*;

crates/storybook/src/prelude.rs β†’ crates/ui/src/prelude.rs πŸ”—

@@ -1,3 +1,11 @@
+#[derive(Default, PartialEq)]
+pub enum OrderMethod {
+    #[default]
+    Ascending,
+    Descending,
+    MostRecent,
+}
+
 #[derive(Default, PartialEq)]
 pub enum ButtonVariant {
     #[default]
@@ -19,6 +27,13 @@ pub enum Shape {
     RoundedRectangle,
 }
 
+#[derive(Default, PartialEq, Clone, Copy)]
+pub enum DisclosureControlVisibility {
+    #[default]
+    OnHover,
+    Always,
+}
+
 #[derive(Default, PartialEq, Clone, Copy)]
 pub enum InteractionState {
     #[default]

crates/ui/src/static_data.rs πŸ”—

@@ -0,0 +1,166 @@
+use crate::{
+    label, list_item, palette_item, IconAsset, LabelColor, ListItem, PaletteItem, ToggleState,
+};
+
+pub fn static_project_panel_project_items() -> Vec<ListItem> {
+    vec![
+        list_item(label("zed"))
+            .left_icon(IconAsset::FolderOpen.into())
+            .indent_level(0)
+            .set_toggle(ToggleState::Toggled),
+        list_item(label(".cargo"))
+            .left_icon(IconAsset::Folder.into())
+            .indent_level(1),
+        list_item(label(".config"))
+            .left_icon(IconAsset::Folder.into())
+            .indent_level(1),
+        list_item(label(".git").color(LabelColor::Hidden))
+            .left_icon(IconAsset::Folder.into())
+            .indent_level(1),
+        list_item(label(".cargo"))
+            .left_icon(IconAsset::Folder.into())
+            .indent_level(1),
+        list_item(label(".idea").color(LabelColor::Hidden))
+            .left_icon(IconAsset::Folder.into())
+            .indent_level(1),
+        list_item(label("assets"))
+            .left_icon(IconAsset::Folder.into())
+            .indent_level(1)
+            .set_toggle(ToggleState::Toggled),
+        list_item(label("cargo-target").color(LabelColor::Hidden))
+            .left_icon(IconAsset::Folder.into())
+            .indent_level(1),
+        list_item(label("crates"))
+            .left_icon(IconAsset::FolderOpen.into())
+            .indent_level(1)
+            .set_toggle(ToggleState::Toggled),
+        list_item(label("activity_indicator"))
+            .left_icon(IconAsset::Folder.into())
+            .indent_level(2),
+        list_item(label("ai"))
+            .left_icon(IconAsset::Folder.into())
+            .indent_level(2),
+        list_item(label("audio"))
+            .left_icon(IconAsset::Folder.into())
+            .indent_level(2),
+        list_item(label("auto_update"))
+            .left_icon(IconAsset::Folder.into())
+            .indent_level(2),
+        list_item(label("breadcrumbs"))
+            .left_icon(IconAsset::Folder.into())
+            .indent_level(2),
+        list_item(label("call"))
+            .left_icon(IconAsset::Folder.into())
+            .indent_level(2),
+        list_item(label("sqlez").color(LabelColor::Modified))
+            .left_icon(IconAsset::Folder.into())
+            .indent_level(2)
+            .set_toggle(ToggleState::NotToggled),
+        list_item(label("gpui2"))
+            .left_icon(IconAsset::FolderOpen.into())
+            .indent_level(2)
+            .set_toggle(ToggleState::Toggled),
+        list_item(label("src"))
+            .left_icon(IconAsset::FolderOpen.into())
+            .indent_level(3)
+            .set_toggle(ToggleState::Toggled),
+        list_item(label("derrive_element.rs"))
+            .left_icon(IconAsset::FileRust.into())
+            .indent_level(4),
+        list_item(label("storybook").color(LabelColor::Modified))
+            .left_icon(IconAsset::FolderOpen.into())
+            .indent_level(1)
+            .set_toggle(ToggleState::Toggled),
+        list_item(label("docs").color(LabelColor::Default))
+            .left_icon(IconAsset::Folder.into())
+            .indent_level(2)
+            .set_toggle(ToggleState::Toggled),
+        list_item(label("src").color(LabelColor::Modified))
+            .left_icon(IconAsset::FolderOpen.into())
+            .indent_level(3)
+            .set_toggle(ToggleState::Toggled),
+        list_item(label("ui").color(LabelColor::Modified))
+            .left_icon(IconAsset::FolderOpen.into())
+            .indent_level(4)
+            .set_toggle(ToggleState::Toggled),
+        list_item(label("component").color(LabelColor::Created))
+            .left_icon(IconAsset::FolderOpen.into())
+            .indent_level(5)
+            .set_toggle(ToggleState::Toggled),
+        list_item(label("facepile.rs").color(LabelColor::Default))
+            .left_icon(IconAsset::FileRust.into())
+            .indent_level(6),
+        list_item(label("follow_group.rs").color(LabelColor::Default))
+            .left_icon(IconAsset::FileRust.into())
+            .indent_level(6),
+        list_item(label("list_item.rs").color(LabelColor::Created))
+            .left_icon(IconAsset::FileRust.into())
+            .indent_level(6),
+        list_item(label("tab.rs").color(LabelColor::Default))
+            .left_icon(IconAsset::FileRust.into())
+            .indent_level(6),
+        list_item(label("target").color(LabelColor::Hidden))
+            .left_icon(IconAsset::Folder.into())
+            .indent_level(1),
+        list_item(label(".dockerignore"))
+            .left_icon(IconAsset::File.into())
+            .indent_level(1),
+        list_item(label(".DS_Store").color(LabelColor::Hidden))
+            .left_icon(IconAsset::File.into())
+            .indent_level(1),
+        list_item(label("Cargo.lock"))
+            .left_icon(IconAsset::FileLock.into())
+            .indent_level(1),
+        list_item(label("Cargo.toml"))
+            .left_icon(IconAsset::FileToml.into())
+            .indent_level(1),
+        list_item(label("Dockerfile"))
+            .left_icon(IconAsset::File.into())
+            .indent_level(1),
+        list_item(label("Procfile"))
+            .left_icon(IconAsset::File.into())
+            .indent_level(1),
+        list_item(label("README.md"))
+            .left_icon(IconAsset::FileDoc.into())
+            .indent_level(1),
+    ]
+}
+
+pub fn static_project_panel_single_items() -> Vec<ListItem> {
+    vec![
+        list_item(label("todo.md"))
+            .left_icon(IconAsset::FileDoc.into())
+            .indent_level(0),
+        list_item(label("README.md"))
+            .left_icon(IconAsset::FileDoc.into())
+            .indent_level(0),
+        list_item(label("config.json"))
+            .left_icon(IconAsset::File.into())
+            .indent_level(0),
+    ]
+}
+
+pub fn example_editor_actions() -> Vec<PaletteItem> {
+    vec![
+        palette_item("New File", Some("Ctrl+N")),
+        palette_item("Open File", Some("Ctrl+O")),
+        palette_item("Save File", Some("Ctrl+S")),
+        palette_item("Cut", Some("Ctrl+X")),
+        palette_item("Copy", Some("Ctrl+C")),
+        palette_item("Paste", Some("Ctrl+V")),
+        palette_item("Undo", Some("Ctrl+Z")),
+        palette_item("Redo", Some("Ctrl+Shift+Z")),
+        palette_item("Find", Some("Ctrl+F")),
+        palette_item("Replace", Some("Ctrl+R")),
+        palette_item("Jump to Line", None),
+        palette_item("Select All", None),
+        palette_item("Deselect All", None),
+        palette_item("Switch Document", None),
+        palette_item("Insert Line Below", None),
+        palette_item("Insert Line Above", None),
+        palette_item("Move Line Up", None),
+        palette_item("Move Line Down", None),
+        palette_item("Toggle Comment", None),
+        palette_item("Delete Line", None),
+    ]
+}

crates/storybook/src/theme.rs β†’ crates/ui/src/theme.rs πŸ”—

@@ -1,9 +1,13 @@
-use gpui2::{
-    color::Hsla, element::Element, serde_json, AppContext, IntoElement, Vector2F, ViewContext,
-    WindowContext,
-};
-use serde::{de::Visitor, Deserialize, Deserializer};
-use std::{collections::HashMap, fmt, marker::PhantomData};
+use std::collections::HashMap;
+use std::fmt;
+use std::marker::PhantomData;
+use std::sync::Arc;
+
+use gpui2::color::Hsla;
+use gpui2::element::Element;
+use gpui2::{serde_json, AppContext, IntoElement, Vector2F, ViewContext, WindowContext};
+use serde::de::Visitor;
+use serde::{Deserialize, Deserializer};
 use theme::ThemeSettings;
 
 #[derive(Deserialize, Clone, Default, Debug)]
@@ -187,6 +191,6 @@ fn preferred_theme<V: 'static>(cx: &AppContext) -> Theme {
         .clone()
 }
 
-pub fn theme<'a>(cx: &'a WindowContext) -> &'a Theme {
+pub fn theme(cx: &WindowContext) -> Arc<Theme> {
     cx.theme::<Theme>()
 }

crates/ui/src/tokens.rs πŸ”—

@@ -0,0 +1,18 @@
+use gpui2::geometry::AbsoluteLength;
+
+#[derive(Clone, Copy)]
+pub struct Token {
+    pub list_indent_depth: AbsoluteLength,
+}
+
+impl Default for Token {
+    fn default() -> Self {
+        Self {
+            list_indent_depth: AbsoluteLength::Rems(0.5),
+        }
+    }
+}
+
+pub fn token() -> Token {
+    Token::default()
+}

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

@@ -41,6 +41,8 @@ pub fn truncate(s: &str, max_chars: usize) -> &str {
     }
 }
 
+/// Removes characters from the end of the string if it's length is greater than `max_chars` and
+/// appends "..." to the string. Returns string unchanged if it's length is smaller than max_chars.
 pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String {
     debug_assert!(max_chars >= 5);
 
@@ -51,6 +53,18 @@ pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String {
     }
 }
 
+/// Removes characters from the front of the string if it's length is greater than `max_chars` and
+/// prepends the string with "...". Returns string unchanged if it's length is smaller than max_chars.
+pub fn truncate_and_remove_front(s: &str, max_chars: usize) -> String {
+    debug_assert!(max_chars >= 5);
+
+    let truncation_ix = s.char_indices().map(|(i, _)| i).nth_back(max_chars);
+    match truncation_ix {
+        Some(length) => "…".to_string() + &s[length..],
+        None => s.to_string(),
+    }
+}
+
 pub fn post_inc<T: From<u8> + AddAssign<T> + Copy>(value: &mut T) -> T {
     let prev = *value;
     *value += T::from(1);

crates/vim/Cargo.toml πŸ”—

@@ -34,6 +34,8 @@ settings = { path = "../settings" }
 workspace = { path = "../workspace" }
 theme = { path = "../theme" }
 language_selector = { path = "../language_selector"}
+diagnostics = { path = "../diagnostics" }
+zed-actions = { path = "../zed-actions" }
 
 [dev-dependencies]
 indoc.workspace = true

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

@@ -0,0 +1,438 @@
+use command_palette::CommandInterceptResult;
+use editor::{SortLinesCaseInsensitive, SortLinesCaseSensitive};
+use gpui::{impl_actions, Action, AppContext};
+use serde_derive::Deserialize;
+use workspace::{SaveIntent, Workspace};
+
+use crate::{
+    motion::{EndOfDocument, Motion},
+    normal::{
+        move_cursor,
+        search::{FindCommand, ReplaceCommand},
+        JoinLines,
+    },
+    state::Mode,
+    Vim,
+};
+
+#[derive(Debug, Clone, PartialEq, Deserialize)]
+pub struct GoToLine {
+    pub line: u32,
+}
+
+impl_actions!(vim, [GoToLine]);
+
+pub fn init(cx: &mut AppContext) {
+    cx.add_action(|_: &mut Workspace, action: &GoToLine, cx| {
+        Vim::update(cx, |vim, cx| {
+            vim.switch_mode(Mode::Normal, false, cx);
+            move_cursor(vim, Motion::StartOfDocument, Some(action.line as usize), cx);
+        });
+    });
+}
+
+pub fn command_interceptor(mut query: &str, _: &AppContext) -> Option<CommandInterceptResult> {
+    // Note: this is a very poor simulation of vim's command palette.
+    // In the future we should adjust it to handle parsing range syntax,
+    // and then calling the appropriate commands with/without ranges.
+    //
+    // We also need to support passing arguments to commands like :w
+    // (ideally with filename autocompletion).
+    //
+    // For now, you can only do a replace on the % range, and you can
+    // only use a specific line number range to "go to line"
+    while query.starts_with(":") {
+        query = &query[1..];
+    }
+
+    let (name, action) = match query {
+        // save and quit
+        "w" | "wr" | "wri" | "writ" | "write" => (
+            "write",
+            workspace::Save {
+                save_intent: Some(SaveIntent::Save),
+            }
+            .boxed_clone(),
+        ),
+        "w!" | "wr!" | "wri!" | "writ!" | "write!" => (
+            "write!",
+            workspace::Save {
+                save_intent: Some(SaveIntent::Overwrite),
+            }
+            .boxed_clone(),
+        ),
+        "q" | "qu" | "qui" | "quit" => (
+            "quit",
+            workspace::CloseActiveItem {
+                save_intent: Some(SaveIntent::Close),
+            }
+            .boxed_clone(),
+        ),
+        "q!" | "qu!" | "qui!" | "quit!" => (
+            "quit!",
+            workspace::CloseActiveItem {
+                save_intent: Some(SaveIntent::Skip),
+            }
+            .boxed_clone(),
+        ),
+        "wq" => (
+            "wq",
+            workspace::CloseActiveItem {
+                save_intent: Some(SaveIntent::Save),
+            }
+            .boxed_clone(),
+        ),
+        "wq!" => (
+            "wq!",
+            workspace::CloseActiveItem {
+                save_intent: Some(SaveIntent::Overwrite),
+            }
+            .boxed_clone(),
+        ),
+        "x" | "xi" | "xit" | "exi" | "exit" => (
+            "exit",
+            workspace::CloseActiveItem {
+                save_intent: Some(SaveIntent::SaveAll),
+            }
+            .boxed_clone(),
+        ),
+        "x!" | "xi!" | "xit!" | "exi!" | "exit!" => (
+            "exit!",
+            workspace::CloseActiveItem {
+                save_intent: Some(SaveIntent::Overwrite),
+            }
+            .boxed_clone(),
+        ),
+        "up" | "upd" | "upda" | "updat" | "update" => (
+            "update",
+            workspace::Save {
+                save_intent: Some(SaveIntent::SaveAll),
+            }
+            .boxed_clone(),
+        ),
+        "wa" | "wal" | "wall" => (
+            "wall",
+            workspace::SaveAll {
+                save_intent: Some(SaveIntent::SaveAll),
+            }
+            .boxed_clone(),
+        ),
+        "wa!" | "wal!" | "wall!" => (
+            "wall!",
+            workspace::SaveAll {
+                save_intent: Some(SaveIntent::Overwrite),
+            }
+            .boxed_clone(),
+        ),
+        "qa" | "qal" | "qall" | "quita" | "quital" | "quitall" => (
+            "quitall",
+            workspace::CloseAllItemsAndPanes {
+                save_intent: Some(SaveIntent::Close),
+            }
+            .boxed_clone(),
+        ),
+        "qa!" | "qal!" | "qall!" | "quita!" | "quital!" | "quitall!" => (
+            "quitall!",
+            workspace::CloseAllItemsAndPanes {
+                save_intent: Some(SaveIntent::Skip),
+            }
+            .boxed_clone(),
+        ),
+        "xa" | "xal" | "xall" => (
+            "xall",
+            workspace::CloseAllItemsAndPanes {
+                save_intent: Some(SaveIntent::SaveAll),
+            }
+            .boxed_clone(),
+        ),
+        "xa!" | "xal!" | "xall!" => (
+            "xall!",
+            workspace::CloseAllItemsAndPanes {
+                save_intent: Some(SaveIntent::Overwrite),
+            }
+            .boxed_clone(),
+        ),
+        "wqa" | "wqal" | "wqall" => (
+            "wqall",
+            workspace::CloseAllItemsAndPanes {
+                save_intent: Some(SaveIntent::SaveAll),
+            }
+            .boxed_clone(),
+        ),
+        "wqa!" | "wqal!" | "wqall!" => (
+            "wqall!",
+            workspace::CloseAllItemsAndPanes {
+                save_intent: Some(SaveIntent::Overwrite),
+            }
+            .boxed_clone(),
+        ),
+        "cq" | "cqu" | "cqui" | "cquit" | "cq!" | "cqu!" | "cqui!" | "cquit!" => {
+            ("cquit!", zed_actions::Quit.boxed_clone())
+        }
+
+        // pane management
+        "sp" | "spl" | "spli" | "split" => ("split", workspace::SplitUp.boxed_clone()),
+        "vs" | "vsp" | "vspl" | "vspli" | "vsplit" => {
+            ("vsplit", workspace::SplitLeft.boxed_clone())
+        }
+        "new" => (
+            "new",
+            workspace::NewFileInDirection(workspace::SplitDirection::Up).boxed_clone(),
+        ),
+        "vne" | "vnew" => (
+            "vnew",
+            workspace::NewFileInDirection(workspace::SplitDirection::Left).boxed_clone(),
+        ),
+        "tabe" | "tabed" | "tabedi" | "tabedit" => ("tabedit", workspace::NewFile.boxed_clone()),
+        "tabnew" => ("tabnew", workspace::NewFile.boxed_clone()),
+
+        "tabn" | "tabne" | "tabnex" | "tabnext" => {
+            ("tabnext", workspace::ActivateNextItem.boxed_clone())
+        }
+        "tabp" | "tabpr" | "tabpre" | "tabprev" | "tabprevi" | "tabprevio" | "tabpreviou"
+        | "tabprevious" => ("tabprevious", workspace::ActivatePrevItem.boxed_clone()),
+        "tabN" | "tabNe" | "tabNex" | "tabNext" => {
+            ("tabNext", workspace::ActivatePrevItem.boxed_clone())
+        }
+        "tabc" | "tabcl" | "tabclo" | "tabclos" | "tabclose" => (
+            "tabclose",
+            workspace::CloseActiveItem {
+                save_intent: Some(SaveIntent::Close),
+            }
+            .boxed_clone(),
+        ),
+
+        // quickfix / loclist (merged together for now)
+        "cl" | "cli" | "clis" | "clist" => ("clist", diagnostics::Deploy.boxed_clone()),
+        "cc" => ("cc", editor::Hover.boxed_clone()),
+        "ll" => ("ll", editor::Hover.boxed_clone()),
+        "cn" | "cne" | "cnex" | "cnext" => ("cnext", editor::GoToDiagnostic.boxed_clone()),
+        "lne" | "lnex" | "lnext" => ("cnext", editor::GoToDiagnostic.boxed_clone()),
+
+        "cpr" | "cpre" | "cprev" | "cprevi" | "cprevio" | "cpreviou" | "cprevious" => {
+            ("cprevious", editor::GoToPrevDiagnostic.boxed_clone())
+        }
+        "cN" | "cNe" | "cNex" | "cNext" => ("cNext", editor::GoToPrevDiagnostic.boxed_clone()),
+        "lp" | "lpr" | "lpre" | "lprev" | "lprevi" | "lprevio" | "lpreviou" | "lprevious" => {
+            ("lprevious", editor::GoToPrevDiagnostic.boxed_clone())
+        }
+        "lN" | "lNe" | "lNex" | "lNext" => ("lNext", editor::GoToPrevDiagnostic.boxed_clone()),
+
+        // modify the buffer (should accept [range])
+        "j" | "jo" | "joi" | "join" => ("join", JoinLines.boxed_clone()),
+        "d" | "de" | "del" | "dele" | "delet" | "delete" | "dl" | "dell" | "delel" | "deletl"
+        | "deletel" | "dp" | "dep" | "delp" | "delep" | "deletp" | "deletep" => {
+            ("delete", editor::DeleteLine.boxed_clone())
+        }
+        "sor" | "sor " | "sort" | "sort " => ("sort", SortLinesCaseSensitive.boxed_clone()),
+        "sor i" | "sort i" => ("sort i", SortLinesCaseInsensitive.boxed_clone()),
+
+        // goto (other ranges handled under _ => )
+        "$" => ("$", EndOfDocument.boxed_clone()),
+
+        _ => {
+            if query.starts_with("/") || query.starts_with("?") {
+                (
+                    query,
+                    FindCommand {
+                        query: query[1..].to_string(),
+                        backwards: query.starts_with("?"),
+                    }
+                    .boxed_clone(),
+                )
+            } else if query.starts_with("%") {
+                (
+                    query,
+                    ReplaceCommand {
+                        query: query.to_string(),
+                    }
+                    .boxed_clone(),
+                )
+            } else if let Ok(line) = query.parse::<u32>() {
+                (query, GoToLine { line }.boxed_clone())
+            } else {
+                return None;
+            }
+        }
+    };
+
+    let string = ":".to_owned() + name;
+    let positions = generate_positions(&string, query);
+
+    Some(CommandInterceptResult {
+        action,
+        string,
+        positions,
+    })
+}
+
+fn generate_positions(string: &str, query: &str) -> Vec<usize> {
+    let mut positions = Vec::new();
+    let mut chars = query.chars().into_iter();
+
+    let Some(mut current) = chars.next() else {
+        return positions;
+    };
+
+    for (i, c) in string.chars().enumerate() {
+        if c == current {
+            positions.push(i);
+            if let Some(c) = chars.next() {
+                current = c;
+            } else {
+                break;
+            }
+        }
+    }
+
+    positions
+}
+
+#[cfg(test)]
+mod test {
+    use std::path::Path;
+
+    use crate::test::{NeovimBackedTestContext, VimTestContext};
+    use gpui::{executor::Foreground, TestAppContext};
+    use indoc::indoc;
+
+    #[gpui::test]
+    async fn test_command_basics(cx: &mut TestAppContext) {
+        if let Foreground::Deterministic { cx_id: _, executor } = cx.foreground().as_ref() {
+            executor.run_until_parked();
+        }
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {"
+            Λ‡a
+            b
+            c"})
+            .await;
+
+        cx.simulate_shared_keystrokes([":", "j", "enter"]).await;
+
+        // hack: our cursor positionining after a join command is wrong
+        cx.simulate_shared_keystrokes(["^"]).await;
+        cx.assert_shared_state(indoc! {
+            "Λ‡a b
+            c"
+        })
+        .await;
+    }
+
+    #[gpui::test]
+    async fn test_command_goto(cx: &mut TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {"
+            Λ‡a
+            b
+            c"})
+            .await;
+        cx.simulate_shared_keystrokes([":", "3", "enter"]).await;
+        cx.assert_shared_state(indoc! {"
+            a
+            b
+            Λ‡c"})
+            .await;
+    }
+
+    #[gpui::test]
+    async fn test_command_replace(cx: &mut TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {"
+            Λ‡a
+            b
+            c"})
+            .await;
+        cx.simulate_shared_keystrokes([":", "%", "s", "/", "b", "/", "d", "enter"])
+            .await;
+        cx.assert_shared_state(indoc! {"
+            a
+            Λ‡d
+            c"})
+            .await;
+        cx.simulate_shared_keystrokes([
+            ":", "%", "s", ":", ".", ":", "\\", "0", "\\", "0", "enter",
+        ])
+        .await;
+        cx.assert_shared_state(indoc! {"
+            aa
+            dd
+            Λ‡cc"})
+            .await;
+    }
+
+    #[gpui::test]
+    async fn test_command_search(cx: &mut TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {"
+                Λ‡a
+                b
+                a
+                c"})
+            .await;
+        cx.simulate_shared_keystrokes([":", "/", "b", "enter"])
+            .await;
+        cx.assert_shared_state(indoc! {"
+                a
+                Λ‡b
+                a
+                c"})
+            .await;
+        cx.simulate_shared_keystrokes([":", "?", "a", "enter"])
+            .await;
+        cx.assert_shared_state(indoc! {"
+                Λ‡a
+                b
+                a
+                c"})
+            .await;
+    }
+
+    #[gpui::test]
+    async fn test_command_write(cx: &mut TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+        let path = Path::new("/root/dir/file.rs");
+        let fs = cx.workspace(|workspace, cx| workspace.project().read(cx).fs().clone());
+
+        cx.simulate_keystrokes(["i", "@", "escape"]);
+        cx.simulate_keystrokes([":", "w", "enter"]);
+
+        assert_eq!(fs.load(&path).await.unwrap(), "@\n");
+
+        fs.as_fake()
+            .write_file_internal(path, "oops\n".to_string())
+            .unwrap();
+
+        // conflict!
+        cx.simulate_keystrokes(["i", "@", "escape"]);
+        cx.simulate_keystrokes([":", "w", "enter"]);
+        let window = cx.window;
+        assert!(window.has_pending_prompt(cx.cx));
+        // "Cancel"
+        window.simulate_prompt_answer(0, cx.cx);
+        assert_eq!(fs.load(&path).await.unwrap(), "oops\n");
+        assert!(!window.has_pending_prompt(cx.cx));
+        // force overwrite
+        cx.simulate_keystrokes([":", "w", "!", "enter"]);
+        assert!(!window.has_pending_prompt(cx.cx));
+        assert_eq!(fs.load(&path).await.unwrap(), "@@\n");
+    }
+
+    #[gpui::test]
+    async fn test_command_quit(cx: &mut TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        cx.simulate_keystrokes([":", "n", "e", "w", "enter"]);
+        cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 2));
+        cx.simulate_keystrokes([":", "q", "enter"]);
+        cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 1));
+        cx.simulate_keystrokes([":", "n", "e", "w", "enter"]);
+        cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 2));
+        cx.simulate_keystrokes([":", "q", "a", "enter"]);
+        cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 0));
+    }
+}

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

@@ -4,7 +4,7 @@ mod delete;
 mod paste;
 pub(crate) mod repeat;
 mod scroll;
-mod search;
+pub(crate) mod search;
 pub mod substitute;
 mod yank;
 
@@ -168,7 +168,12 @@ pub fn normal_object(object: Object, cx: &mut WindowContext) {
     })
 }
 
-fn move_cursor(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
+pub(crate) fn move_cursor(
+    vim: &mut Vim,
+    motion: Motion,
+    times: Option<usize>,
+    cx: &mut WindowContext,
+) {
     vim.update_active_editor(cx, |editor, cx| {
         editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
             s.move_cursors_with(|map, cursor, goal| {

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

@@ -15,19 +15,19 @@ actions!(
 
 pub fn init(cx: &mut AppContext) {
     cx.add_action(|_: &mut Workspace, _: &LineDown, cx| {
-        scroll(cx, |c| ScrollAmount::Line(c.unwrap_or(1.)))
+        scroll(cx, false, |c| ScrollAmount::Line(c.unwrap_or(1.)))
     });
     cx.add_action(|_: &mut Workspace, _: &LineUp, cx| {
-        scroll(cx, |c| ScrollAmount::Line(-c.unwrap_or(1.)))
+        scroll(cx, false, |c| ScrollAmount::Line(-c.unwrap_or(1.)))
     });
     cx.add_action(|_: &mut Workspace, _: &PageDown, cx| {
-        scroll(cx, |c| ScrollAmount::Page(c.unwrap_or(1.)))
+        scroll(cx, false, |c| ScrollAmount::Page(c.unwrap_or(1.)))
     });
     cx.add_action(|_: &mut Workspace, _: &PageUp, cx| {
-        scroll(cx, |c| ScrollAmount::Page(-c.unwrap_or(1.)))
+        scroll(cx, false, |c| ScrollAmount::Page(-c.unwrap_or(1.)))
     });
     cx.add_action(|_: &mut Workspace, _: &ScrollDown, cx| {
-        scroll(cx, |c| {
+        scroll(cx, true, |c| {
             if let Some(c) = c {
                 ScrollAmount::Line(c)
             } else {
@@ -36,7 +36,7 @@ pub fn init(cx: &mut AppContext) {
         })
     });
     cx.add_action(|_: &mut Workspace, _: &ScrollUp, cx| {
-        scroll(cx, |c| {
+        scroll(cx, true, |c| {
             if let Some(c) = c {
                 ScrollAmount::Line(-c)
             } else {
@@ -46,15 +46,27 @@ pub fn init(cx: &mut AppContext) {
     });
 }
 
-fn scroll(cx: &mut ViewContext<Workspace>, by: fn(c: Option<f32>) -> ScrollAmount) {
+fn scroll(
+    cx: &mut ViewContext<Workspace>,
+    move_cursor: bool,
+    by: fn(c: Option<f32>) -> ScrollAmount,
+) {
     Vim::update(cx, |vim, cx| {
         let amount = by(vim.take_count(cx).map(|c| c as f32));
-        vim.update_active_editor(cx, |editor, cx| scroll_editor(editor, &amount, cx));
+        vim.update_active_editor(cx, |editor, cx| {
+            scroll_editor(editor, move_cursor, &amount, cx)
+        });
     })
 }
 
-fn scroll_editor(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContext<Editor>) {
+fn scroll_editor(
+    editor: &mut Editor,
+    preserve_cursor_position: bool,
+    amount: &ScrollAmount,
+    cx: &mut ViewContext<Editor>,
+) {
     let should_move_cursor = editor.newest_selection_on_screen(cx).is_eq();
+    let old_top_anchor = editor.scroll_manager.anchor().anchor;
 
     editor.scroll_screen(amount, cx);
     if should_move_cursor {
@@ -68,8 +80,14 @@ fn scroll_editor(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContex
 
         editor.change_selections(None, cx, |s| {
             s.move_with(|map, selection| {
-                let head = selection.head();
+                let mut head = selection.head();
                 let top = top_anchor.to_display_point(map);
+
+                if preserve_cursor_position {
+                    let old_top = old_top_anchor.to_display_point(map);
+                    let new_row = top.row() + selection.head().row() - old_top.row();
+                    head = map.clip_point(DisplayPoint::new(new_row, head.column()), Bias::Left)
+                }
                 let min_row = top.row() + VERTICAL_SCROLL_MARGIN as u32;
                 let max_row = top.row() + visible_rows - VERTICAL_SCROLL_MARGIN as u32 - 1;
 
@@ -92,7 +110,10 @@ fn scroll_editor(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContex
 
 #[cfg(test)]
 mod test {
-    use crate::{state::Mode, test::VimTestContext};
+    use crate::{
+        state::Mode,
+        test::{NeovimBackedTestContext, VimTestContext},
+    };
     use gpui::geometry::vector::vec2f;
     use indoc::indoc;
     use language::Point;
@@ -148,10 +169,10 @@ mod test {
         });
         cx.simulate_keystrokes(["ctrl-d"]);
         cx.update_editor(|editor, cx| {
-            assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.0));
+            assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.0));
             assert_eq!(
                 editor.selections.newest(cx).range(),
-                Point::new(5, 0)..Point::new(5, 0)
+                Point::new(6, 0)..Point::new(6, 0)
             )
         });
 
@@ -162,11 +183,48 @@ mod test {
         });
         cx.simulate_keystrokes(["v", "ctrl-d"]);
         cx.update_editor(|editor, cx| {
-            assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.0));
+            assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.0));
             assert_eq!(
                 editor.selections.newest(cx).range(),
-                Point::new(0, 0)..Point::new(5, 1)
+                Point::new(0, 0)..Point::new(6, 1)
             )
         });
     }
+    #[gpui::test]
+    async fn test_ctrl_d_u(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_scroll_height(10).await;
+
+        pub fn sample_text(rows: usize, cols: usize, start_char: char) -> String {
+            let mut text = String::new();
+            for row in 0..rows {
+                let c: char = (start_char as u32 + row as u32) as u8 as char;
+                let mut line = c.to_string().repeat(cols);
+                if row < rows - 1 {
+                    line.push('\n');
+                }
+                text += &line;
+            }
+            text
+        }
+        let content = "Λ‡".to_owned() + &sample_text(26, 2, 'a');
+        cx.set_shared_state(&content).await;
+
+        // skip over the scrolloff at the top
+        // test ctrl-d
+        cx.simulate_shared_keystrokes(["4", "j", "ctrl-d"]).await;
+        cx.assert_state_matches().await;
+        cx.simulate_shared_keystrokes(["ctrl-d"]).await;
+        cx.assert_state_matches().await;
+        cx.simulate_shared_keystrokes(["g", "g", "ctrl-d"]).await;
+        cx.assert_state_matches().await;
+
+        // test ctrl-u
+        cx.simulate_shared_keystrokes(["ctrl-u"]).await;
+        cx.assert_state_matches().await;
+        cx.simulate_shared_keystrokes(["ctrl-d", "ctrl-d", "4", "j", "ctrl-u", "ctrl-u"])
+            .await;
+        cx.assert_state_matches().await;
+    }
 }

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

@@ -3,7 +3,7 @@ use search::{buffer_search, BufferSearchBar, SearchMode, SearchOptions};
 use serde_derive::Deserialize;
 use workspace::{searchable::Direction, Pane, Workspace};
 
-use crate::{state::SearchState, Vim};
+use crate::{motion::Motion, normal::move_cursor, state::SearchState, Vim};
 
 #[derive(Clone, Deserialize, PartialEq)]
 #[serde(rename_all = "camelCase")]
@@ -25,7 +25,29 @@ pub(crate) struct Search {
     backwards: bool,
 }
 
-impl_actions!(vim, [MoveToNext, MoveToPrev, Search]);
+#[derive(Debug, Clone, PartialEq, Deserialize)]
+pub struct FindCommand {
+    pub query: String,
+    pub backwards: bool,
+}
+
+#[derive(Debug, Clone, PartialEq, Deserialize)]
+pub struct ReplaceCommand {
+    pub query: String,
+}
+
+#[derive(Debug, Default)]
+struct Replacement {
+    search: String,
+    replacement: String,
+    should_replace_all: bool,
+    is_case_sensitive: bool,
+}
+
+impl_actions!(
+    vim,
+    [MoveToNext, MoveToPrev, Search, FindCommand, ReplaceCommand]
+);
 actions!(vim, [SearchSubmit]);
 
 pub(crate) fn init(cx: &mut AppContext) {
@@ -34,6 +56,9 @@ pub(crate) fn init(cx: &mut AppContext) {
     cx.add_action(search);
     cx.add_action(search_submit);
     cx.add_action(search_deploy);
+
+    cx.add_action(find_command);
+    cx.add_action(replace_command);
 }
 
 fn move_to_next(workspace: &mut Workspace, action: &MoveToNext, cx: &mut ViewContext<Workspace>) {
@@ -65,6 +90,7 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Works
                     cx.focus_self();
 
                     if query.is_empty() {
+                        search_bar.set_replacement(None, cx);
                         search_bar.set_search_options(SearchOptions::CASE_SENSITIVE, cx);
                         search_bar.activate_search_mode(SearchMode::Regex, cx);
                     }
@@ -151,6 +177,174 @@ pub fn move_to_internal(
     });
 }
 
+fn find_command(workspace: &mut Workspace, action: &FindCommand, cx: &mut ViewContext<Workspace>) {
+    let pane = workspace.active_pane().clone();
+    pane.update(cx, |pane, cx| {
+        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
+            let search = search_bar.update(cx, |search_bar, cx| {
+                if !search_bar.show(cx) {
+                    return None;
+                }
+                let mut query = action.query.clone();
+                if query == "" {
+                    query = search_bar.query(cx);
+                };
+
+                search_bar.activate_search_mode(SearchMode::Regex, cx);
+                Some(search_bar.search(&query, Some(SearchOptions::CASE_SENSITIVE), cx))
+            });
+            let Some(search) = search else { return };
+            let search_bar = search_bar.downgrade();
+            let direction = if action.backwards {
+                Direction::Prev
+            } else {
+                Direction::Next
+            };
+            cx.spawn(|_, mut cx| async move {
+                search.await?;
+                search_bar.update(&mut cx, |search_bar, cx| {
+                    search_bar.select_match(direction, 1, cx)
+                })?;
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
+        }
+    })
+}
+
+fn replace_command(
+    workspace: &mut Workspace,
+    action: &ReplaceCommand,
+    cx: &mut ViewContext<Workspace>,
+) {
+    let replacement = parse_replace_all(&action.query);
+    let pane = workspace.active_pane().clone();
+    pane.update(cx, |pane, cx| {
+        let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
+            return;
+        };
+        let search = search_bar.update(cx, |search_bar, cx| {
+            if !search_bar.show(cx) {
+                return None;
+            }
+
+            let mut options = SearchOptions::default();
+            if replacement.is_case_sensitive {
+                options.set(SearchOptions::CASE_SENSITIVE, true)
+            }
+            let search = if replacement.search == "" {
+                search_bar.query(cx)
+            } else {
+                replacement.search
+            };
+
+            search_bar.set_replacement(Some(&replacement.replacement), cx);
+            search_bar.activate_search_mode(SearchMode::Regex, cx);
+            Some(search_bar.search(&search, Some(options), cx))
+        });
+        let Some(search) = search else { return };
+        let search_bar = search_bar.downgrade();
+        cx.spawn(|_, mut cx| async move {
+            search.await?;
+            search_bar.update(&mut cx, |search_bar, cx| {
+                if replacement.should_replace_all {
+                    search_bar.select_last_match(cx);
+                    search_bar.replace_all(&Default::default(), cx);
+                    Vim::update(cx, |vim, cx| {
+                        move_cursor(
+                            vim,
+                            Motion::StartOfLine {
+                                display_lines: false,
+                            },
+                            None,
+                            cx,
+                        )
+                    })
+                }
+            })?;
+            anyhow::Ok(())
+        })
+        .detach_and_log_err(cx);
+    })
+}
+
+// convert a vim query into something more usable by zed.
+// we don't attempt to fully convert between the two regex syntaxes,
+// but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
+// and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
+fn parse_replace_all(query: &str) -> Replacement {
+    let mut chars = query.chars();
+    if Some('%') != chars.next() || Some('s') != chars.next() {
+        return Replacement::default();
+    }
+
+    let Some(delimeter) = chars.next() else {
+        return Replacement::default();
+    };
+
+    let mut search = String::new();
+    let mut replacement = String::new();
+    let mut flags = String::new();
+
+    let mut buffer = &mut search;
+
+    let mut escaped = false;
+    // 0 - parsing search
+    // 1 - parsing replacement
+    // 2 - parsing flags
+    let mut phase = 0;
+
+    for c in chars {
+        if escaped {
+            escaped = false;
+            if phase == 1 && c.is_digit(10) {
+                buffer.push('$')
+            // unescape escaped parens
+            } else if phase == 0 && c == '(' || c == ')' {
+            } else if c != delimeter {
+                buffer.push('\\')
+            }
+            buffer.push(c)
+        } else if c == '\\' {
+            escaped = true;
+        } else if c == delimeter {
+            if phase == 0 {
+                buffer = &mut replacement;
+                phase = 1;
+            } else if phase == 1 {
+                buffer = &mut flags;
+                phase = 2;
+            } else {
+                break;
+            }
+        } else {
+            // escape unescaped parens
+            if phase == 0 && c == '(' || c == ')' {
+                buffer.push('\\')
+            }
+            buffer.push(c)
+        }
+    }
+
+    let mut replacement = Replacement {
+        search,
+        replacement,
+        should_replace_all: true,
+        is_case_sensitive: true,
+    };
+
+    for c in flags.chars() {
+        match c {
+            'g' | 'I' => {}
+            'c' | 'n' => replacement.should_replace_all = false,
+            'i' => replacement.is_case_sensitive = false,
+            _ => {}
+        }
+    }
+
+    replacement
+}
+
 #[cfg(test)]
 mod test {
     use std::sync::Arc;

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

@@ -186,9 +186,6 @@ async fn test_selection_on_search(cx: &mut gpui::TestAppContext) {
         assert_eq!(bar.query(cx), "cc");
     });
 
-    // wait for the query editor change event to fire.
-    search_bar.next_notification(&cx).await;
-
     cx.update_editor(|editor, cx| {
         let highlights = editor.all_text_background_highlights(cx);
         assert_eq!(3, highlights.len());

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

@@ -1,7 +1,5 @@
 use std::ops::{Deref, DerefMut};
 
-use gpui::ContextHandle;
-
 use crate::state::Mode;
 
 use super::{ExemptionFeatures, NeovimBackedTestContext, SUPPORTED_FEATURES};
@@ -33,26 +31,17 @@ impl<'a, const COUNT: usize> NeovimBackedBindingTestContext<'a, COUNT> {
         self.consume().binding(keystrokes)
     }
 
-    pub async fn assert(
-        &mut self,
-        marked_positions: &str,
-    ) -> Option<(ContextHandle, ContextHandle)> {
+    pub async fn assert(&mut self, marked_positions: &str) {
         self.cx
             .assert_binding_matches(self.keystrokes_under_test, marked_positions)
-            .await
+            .await;
     }
 
-    pub async fn assert_exempted(
-        &mut self,
-        marked_positions: &str,
-        feature: ExemptionFeatures,
-    ) -> Option<(ContextHandle, ContextHandle)> {
+    pub async fn assert_exempted(&mut self, marked_positions: &str, feature: ExemptionFeatures) {
         if SUPPORTED_FEATURES.contains(&feature) {
             self.cx
                 .assert_binding_matches(self.keystrokes_under_test, marked_positions)
                 .await
-        } else {
-            None
         }
     }
 

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

@@ -1,9 +1,10 @@
+use editor::scroll::VERTICAL_SCROLL_MARGIN;
 use indoc::indoc;
 use settings::SettingsStore;
 use std::ops::{Deref, DerefMut, Range};
 
 use collections::{HashMap, HashSet};
-use gpui::ContextHandle;
+use gpui::{geometry::vector::vec2f, ContextHandle};
 use language::{
     language_settings::{AllLanguageSettings, SoftWrap},
     OffsetRangeExt,
@@ -106,26 +107,25 @@ impl<'a> NeovimBackedTestContext<'a> {
     pub async fn simulate_shared_keystrokes<const COUNT: usize>(
         &mut self,
         keystroke_texts: [&str; COUNT],
-    ) -> ContextHandle {
+    ) {
         for keystroke_text in keystroke_texts.into_iter() {
             self.recent_keystrokes.push(keystroke_text.to_string());
             self.neovim.send_keystroke(keystroke_text).await;
         }
-        self.simulate_keystrokes(keystroke_texts)
+        self.simulate_keystrokes(keystroke_texts);
     }
 
-    pub async fn set_shared_state(&mut self, marked_text: &str) -> ContextHandle {
+    pub async fn set_shared_state(&mut self, marked_text: &str) {
         let mode = if marked_text.contains("Β»") {
             Mode::Visual
         } else {
             Mode::Normal
         };
-        let context_handle = self.set_state(marked_text, mode);
+        self.set_state(marked_text, mode);
         self.last_set_state = Some(marked_text.to_string());
         self.recent_keystrokes = Vec::new();
         self.neovim.set_state(marked_text).await;
         self.is_dirty = true;
-        context_handle
     }
 
     pub async fn set_shared_wrap(&mut self, columns: u32) {
@@ -133,7 +133,9 @@ impl<'a> NeovimBackedTestContext<'a> {
             panic!("nvim doesn't support columns < 12")
         }
         self.neovim.set_option("wrap").await;
-        self.neovim.set_option("columns=12").await;
+        self.neovim
+            .set_option(&format!("columns={}", columns))
+            .await;
 
         self.update(|cx| {
             cx.update_global(|settings: &mut SettingsStore, cx| {
@@ -145,6 +147,20 @@ impl<'a> NeovimBackedTestContext<'a> {
         })
     }
 
+    pub async fn set_scroll_height(&mut self, rows: u32) {
+        // match Zed's scrolling behavior
+        self.neovim
+            .set_option(&format!("scrolloff={}", VERTICAL_SCROLL_MARGIN))
+            .await;
+        // +2 to account for the vim command UI at the bottom.
+        self.neovim.set_option(&format!("lines={}", rows + 2)).await;
+        let window = self.window;
+        let line_height =
+            self.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
+
+        window.simulate_resize(vec2f(1000., (rows as f32) * line_height), &mut self.cx);
+    }
+
     pub async fn set_neovim_option(&mut self, option: &str) {
         self.neovim.set_option(option).await;
     }
@@ -288,18 +304,18 @@ impl<'a> NeovimBackedTestContext<'a> {
         &mut self,
         keystrokes: [&str; COUNT],
         initial_state: &str,
-    ) -> Option<(ContextHandle, ContextHandle)> {
+    ) {
         if let Some(possible_exempted_keystrokes) = self.exemptions.get(initial_state) {
             match possible_exempted_keystrokes {
                 Some(exempted_keystrokes) => {
                     if exempted_keystrokes.contains(&format!("{keystrokes:?}")) {
                         // This keystroke was exempted for this insertion text
-                        return None;
+                        return;
                     }
                 }
                 None => {
                     // All keystrokes for this insertion text are exempted
-                    return None;
+                    return;
                 }
             }
         }
@@ -307,7 +323,6 @@ impl<'a> NeovimBackedTestContext<'a> {
         let _state_context = self.set_shared_state(initial_state).await;
         let _keystroke_context = self.simulate_shared_keystrokes(keystrokes).await;
         self.assert_state_matches().await;
-        Some((_state_context, _keystroke_context))
     }
 
     pub async fn assert_binding_matches_all<const COUNT: usize>(

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

@@ -65,7 +65,13 @@ impl NeovimConnection {
             // Ensure we don't create neovim connections in parallel
             let _lock = NEOVIM_LOCK.lock();
             let (nvim, join_handle, child) = new_child_cmd(
-                &mut Command::new("nvim").arg("--embed").arg("--clean"),
+                &mut Command::new("nvim")
+                    .arg("--embed")
+                    .arg("--clean")
+                    // disable swap (otherwise after about 1000 test runs you run out of swap file names)
+                    .arg("-n")
+                    // disable writing files (just in case)
+                    .arg("-m"),
                 handler,
             )
             .await

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

@@ -1,6 +1,7 @@
 #[cfg(test)]
 mod test;
 
+mod command;
 mod editor_events;
 mod insert;
 mod mode_indicator;
@@ -13,6 +14,7 @@ mod visual;
 
 use anyhow::Result;
 use collections::{CommandPaletteFilter, HashMap};
+use command_palette::CommandPaletteInterceptor;
 use editor::{movement, Editor, EditorMode, Event};
 use gpui::{
     actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, Action,
@@ -63,6 +65,7 @@ pub fn init(cx: &mut AppContext) {
     insert::init(cx);
     object::init(cx);
     motion::init(cx);
+    command::init(cx);
 
     // Vim Actions
     cx.add_action(|_: &mut Workspace, &SwitchMode(mode): &SwitchMode, cx| {
@@ -469,6 +472,12 @@ impl Vim {
                 }
             });
 
+            if self.enabled {
+                cx.set_global::<CommandPaletteInterceptor>(Box::new(command::command_interceptor));
+            } else if cx.has_global::<CommandPaletteInterceptor>() {
+                let _ = cx.remove_global::<CommandPaletteInterceptor>();
+            }
+
             cx.update_active_window(|cx| {
                 if self.enabled {
                     let active_editor = cx

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

@@ -1,3 +1,4 @@
+use anyhow::Result;
 use std::{cmp, sync::Arc};
 
 use collections::HashMap;
@@ -28,6 +29,8 @@ actions!(
         VisualDelete,
         VisualYank,
         OtherEnd,
+        SelectNext,
+        SelectPrevious,
     ]
 );
 
@@ -46,6 +49,9 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(other_end);
     cx.add_action(delete);
     cx.add_action(yank);
+
+    cx.add_action(select_next);
+    cx.add_action(select_previous);
 }
 
 pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
@@ -384,6 +390,50 @@ pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
     });
 }
 
+pub fn select_next(
+    _: &mut Workspace,
+    _: &SelectNext,
+    cx: &mut ViewContext<Workspace>,
+) -> Result<()> {
+    Vim::update(cx, |vim, cx| {
+        let count =
+            vim.take_count(cx)
+                .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
+        vim.update_active_editor(cx, |editor, cx| {
+            for _ in 0..count {
+                match editor.select_next(&Default::default(), cx) {
+                    Err(a) => return Err(a),
+                    _ => {}
+                }
+            }
+            Ok(())
+        })
+    })
+    .unwrap_or(Ok(()))
+}
+
+pub fn select_previous(
+    _: &mut Workspace,
+    _: &SelectPrevious,
+    cx: &mut ViewContext<Workspace>,
+) -> Result<()> {
+    Vim::update(cx, |vim, cx| {
+        let count =
+            vim.take_count(cx)
+                .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
+        vim.update_active_editor(cx, |editor, cx| {
+            for _ in 0..count {
+                match editor.select_previous(&Default::default(), cx) {
+                    Err(a) => return Err(a),
+                    _ => {}
+                }
+            }
+            Ok(())
+        })
+    })
+    .unwrap_or(Ok(()))
+}
+
 #[cfg(test)]
 mod test {
     use indoc::indoc;

crates/vim/test_data/test_command_replace.json πŸ”—

@@ -0,0 +1,29 @@
+{"Put":{"state":"Λ‡a\nb\nc"}}
+{"Key":":"}
+{"Key":"%"}
+{"Key":"s"}
+{"Key":"/"}
+{"Key":"b"}
+{"Key":"/"}
+{"Key":"d"}
+{"Key":"enter"}
+{"Get":{"state":"a\nˇd\nc","mode":"Normal"}}
+{"Key":":"}
+{"Key":"%"}
+{"Key":"s"}
+{"Key":":"}
+{"Key":"."}
+{"Key":":"}
+{"Key":"\\"}
+{"Key":"0"}
+{"Key":"\\"}
+{"Key":"0"}
+{"Key":"enter"}
+{"Get":{"state":"aa\ndd\nˇcc","mode":"Normal"}}
+{"Key":":"}
+{"Key":"%"}
+{"Key":"s"}
+{"Key":"/"}
+{"Key":"/"}
+{"Key":"/"}
+{"Key":"enter"}

crates/vim/test_data/test_command_search.json πŸ”—

@@ -0,0 +1,11 @@
+{"Put":{"state":"Λ‡a\nb\na\nc"}}
+{"Key":":"}
+{"Key":"/"}
+{"Key":"b"}
+{"Key":"enter"}
+{"Get":{"state":"a\nˇb\na\nc","mode":"Normal"}}
+{"Key":":"}
+{"Key":"?"}
+{"Key":"a"}
+{"Key":"enter"}
+{"Get":{"state":"Λ‡a\nb\na\nc","mode":"Normal"}}

crates/vim/test_data/test_ctrl_d_u.json πŸ”—

@@ -0,0 +1,22 @@
+{"SetOption":{"value":"scrolloff=3"}}
+{"SetOption":{"value":"lines=12"}}
+{"Put":{"state":"Λ‡aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz"}}
+{"Key":"4"}
+{"Key":"j"}
+{"Key":"ctrl-d"}
+{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\nˇjj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
+{"Key":"ctrl-d"}
+{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\nˇoo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
+{"Key":"g"}
+{"Key":"g"}
+{"Key":"ctrl-d"}
+{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nˇii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
+{"Key":"ctrl-u"}
+{"Get":{"state":"aa\nbb\ncc\nˇdd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
+{"Key":"ctrl-d"}
+{"Key":"ctrl-d"}
+{"Key":"4"}
+{"Key":"j"}
+{"Key":"ctrl-u"}
+{"Key":"ctrl-u"}
+{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nˇhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}

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

@@ -478,11 +478,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                         match item_event {
                             ItemEvent::CloseItem => {
                                 pane.update(cx, |pane, cx| {
-                                    pane.close_item_by_id(
-                                        item.id(),
-                                        crate::SaveBehavior::PromptOnWrite,
-                                        cx,
-                                    )
+                                    pane.close_item_by_id(item.id(), crate::SaveIntent::Close, cx)
                                 })
                                 .detach_and_log_err(cx);
                                 return;

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

@@ -42,18 +42,25 @@ use std::{
     },
 };
 use theme::{Theme, ThemeSettings};
+use util::truncate_and_remove_front;
 
 #[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
 #[serde(rename_all = "camelCase")]
-pub enum SaveBehavior {
-    /// ask before overwriting conflicting files (used by default with %s)
-    PromptOnConflict,
-    /// ask before writing any file that wouldn't be auto-saved (used by default with %w)
-    PromptOnWrite,
-    /// never prompt, write on conflict (used with vim's :w!)
-    SilentlyOverwrite,
-    /// skip all save-related behaviour (used with vim's :cq)
-    DontSave,
+pub enum SaveIntent {
+    /// write all files (even if unchanged)
+    /// prompt before overwriting on-disk changes
+    Save,
+    /// write any files that have local changes
+    /// prompt before overwriting on-disk changes
+    SaveAll,
+    /// always prompt for a new path
+    SaveAs,
+    /// prompt "you have unsaved changes" before writing
+    Close,
+    /// write all dirty files, don't prompt on conflict
+    Overwrite,
+    /// skip all save-related behavior
+    Skip,
 }
 
 #[derive(Clone, Deserialize, PartialEq)]
@@ -78,8 +85,15 @@ pub struct CloseItemsToTheRightById {
 }
 
 #[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[serde(rename_all = "camelCase")]
 pub struct CloseActiveItem {
-    pub save_behavior: Option<SaveBehavior>,
+    pub save_intent: Option<SaveIntent>,
+}
+
+#[derive(Clone, PartialEq, Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CloseAllItems {
+    pub save_intent: Option<SaveIntent>,
 }
 
 actions!(
@@ -92,7 +106,6 @@ actions!(
         CloseCleanItems,
         CloseItemsToTheLeft,
         CloseItemsToTheRight,
-        CloseAllItems,
         GoBack,
         GoForward,
         ReopenClosedItem,
@@ -103,7 +116,7 @@ actions!(
     ]
 );
 
-impl_actions!(pane, [ActivateItem, CloseActiveItem]);
+impl_actions!(pane, [ActivateItem, CloseActiveItem, CloseAllItems]);
 
 const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 
@@ -722,7 +735,7 @@ impl Pane {
         let active_item_id = self.items[self.active_item_index].id();
         Some(self.close_item_by_id(
             active_item_id,
-            action.save_behavior.unwrap_or(SaveBehavior::PromptOnWrite),
+            action.save_intent.unwrap_or(SaveIntent::Close),
             cx,
         ))
     }
@@ -730,12 +743,10 @@ impl Pane {
     pub fn close_item_by_id(
         &mut self,
         item_id_to_close: usize,
-        save_behavior: SaveBehavior,
+        save_intent: SaveIntent,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>> {
-        self.close_items(cx, save_behavior, move |view_id| {
-            view_id == item_id_to_close
-        })
+        self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close)
     }
 
     pub fn close_inactive_items(
@@ -748,11 +759,9 @@ impl Pane {
         }
 
         let active_item_id = self.items[self.active_item_index].id();
-        Some(
-            self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
-                item_id != active_item_id
-            }),
-        )
+        Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
+            item_id != active_item_id
+        }))
     }
 
     pub fn close_clean_items(
@@ -765,11 +774,9 @@ impl Pane {
             .filter(|item| !item.is_dirty(cx))
             .map(|item| item.id())
             .collect();
-        Some(
-            self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
-                item_ids.contains(&item_id)
-            }),
-        )
+        Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
+            item_ids.contains(&item_id)
+        }))
     }
 
     pub fn close_items_to_the_left(
@@ -794,7 +801,7 @@ impl Pane {
             .take_while(|item| item.id() != item_id)
             .map(|item| item.id())
             .collect();
-        self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
+        self.close_items(cx, SaveIntent::Close, move |item_id| {
             item_ids.contains(&item_id)
         })
     }
@@ -822,34 +829,77 @@ impl Pane {
             .take_while(|item| item.id() != item_id)
             .map(|item| item.id())
             .collect();
-        self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
+        self.close_items(cx, SaveIntent::Close, move |item_id| {
             item_ids.contains(&item_id)
         })
     }
 
     pub fn close_all_items(
         &mut self,
-        _: &CloseAllItems,
+        action: &CloseAllItems,
         cx: &mut ViewContext<Self>,
     ) -> Option<Task<Result<()>>> {
         if self.items.is_empty() {
             return None;
         }
 
-        Some(self.close_items(cx, SaveBehavior::PromptOnWrite, |_| true))
+        Some(
+            self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| {
+                true
+            }),
+        )
+    }
+
+    pub(super) fn file_names_for_prompt(
+        items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
+        all_dirty_items: usize,
+        cx: &AppContext,
+    ) -> String {
+        /// Quantity of item paths displayed in prompt prior to cutoff..
+        const FILE_NAMES_CUTOFF_POINT: usize = 10;
+        let mut file_names: Vec<_> = items
+            .filter_map(|item| {
+                item.project_path(cx).and_then(|project_path| {
+                    project_path
+                        .path
+                        .file_name()
+                        .and_then(|name| name.to_str().map(ToOwned::to_owned))
+                })
+            })
+            .take(FILE_NAMES_CUTOFF_POINT)
+            .collect();
+        let should_display_followup_text =
+            all_dirty_items > FILE_NAMES_CUTOFF_POINT || file_names.len() != all_dirty_items;
+        if should_display_followup_text {
+            let not_shown_files = all_dirty_items - file_names.len();
+            if not_shown_files == 1 {
+                file_names.push(".. 1 file not shown".into());
+            } else {
+                file_names.push(format!(".. {} files not shown", not_shown_files).into());
+            }
+        }
+        let file_names = file_names.join("\n");
+        format!(
+            "Do you want to save changes to the following {} files?\n{file_names}",
+            all_dirty_items
+        )
     }
 
     pub fn close_items(
         &mut self,
         cx: &mut ViewContext<Pane>,
-        save_behavior: SaveBehavior,
+        mut save_intent: SaveIntent,
         should_close: impl 'static + Fn(usize) -> bool,
     ) -> Task<Result<()>> {
         // Find the items to close.
         let mut items_to_close = Vec::new();
+        let mut dirty_items = Vec::new();
         for item in &self.items {
             if should_close(item.id()) {
                 items_to_close.push(item.boxed_clone());
+                if item.is_dirty(cx) {
+                    dirty_items.push(item.boxed_clone());
+                }
             }
         }
 
@@ -861,6 +911,22 @@ impl Pane {
 
         let workspace = self.workspace.clone();
         cx.spawn(|pane, mut cx| async move {
+            if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
+                let mut answer = pane.update(&mut cx, |_, cx| {
+                    let prompt =
+                        Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx);
+                    cx.prompt(
+                        PromptLevel::Warning,
+                        &prompt,
+                        &["Save all", "Discard all", "Cancel"],
+                    )
+                })?;
+                match answer.next().await {
+                    Some(0) => save_intent = SaveIntent::SaveAll,
+                    Some(1) => save_intent = SaveIntent::Skip,
+                    _ => {}
+                }
+            }
             let mut saved_project_items_ids = HashSet::default();
             for item in items_to_close.clone() {
                 // Find the item's current index and its set of project item models. Avoid
@@ -900,7 +966,7 @@ impl Pane {
                         &pane,
                         item_ix,
                         &*item,
-                        save_behavior,
+                        save_intent,
                         &mut cx,
                     )
                     .await?
@@ -998,18 +1064,17 @@ impl Pane {
         pane: &WeakViewHandle<Pane>,
         item_ix: usize,
         item: &dyn ItemHandle,
-        save_behavior: SaveBehavior,
+        save_intent: SaveIntent,
         cx: &mut AsyncAppContext,
     ) -> Result<bool> {
         const CONFLICT_MESSAGE: &str =
             "This file has changed on disk since you started editing it. Do you want to overwrite it?";
-        const DIRTY_MESSAGE: &str = "This file contains unsaved edits. Do you want to save it?";
 
-        if save_behavior == SaveBehavior::DontSave {
+        if save_intent == SaveIntent::Skip {
             return Ok(true);
         }
 
-        let (has_conflict, is_dirty, can_save, is_singleton) = cx.read(|cx| {
+        let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.read(|cx| {
             (
                 item.has_conflict(cx),
                 item.is_dirty(cx),
@@ -1018,67 +1083,77 @@ impl Pane {
             )
         });
 
+        // when saving a single buffer, we ignore whether or not it's dirty.
+        if save_intent == SaveIntent::Save {
+            is_dirty = true;
+        }
+
+        if save_intent == SaveIntent::SaveAs {
+            is_dirty = true;
+            has_conflict = false;
+            can_save = false;
+        }
+
+        if save_intent == SaveIntent::Overwrite {
+            has_conflict = false;
+        }
+
         if has_conflict && can_save {
-            if save_behavior == SaveBehavior::SilentlyOverwrite {
-                pane.update(cx, |_, cx| item.save(project, cx))?.await?;
-            } else {
-                let mut answer = pane.update(cx, |pane, cx| {
-                    pane.activate_item(item_ix, true, true, cx);
-                    cx.prompt(
-                        PromptLevel::Warning,
-                        CONFLICT_MESSAGE,
-                        &["Overwrite", "Discard", "Cancel"],
-                    )
-                })?;
-                match answer.next().await {
-                    Some(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?,
-                    Some(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
-                    _ => return Ok(false),
-                }
+            let mut answer = pane.update(cx, |pane, cx| {
+                pane.activate_item(item_ix, true, true, cx);
+                cx.prompt(
+                    PromptLevel::Warning,
+                    CONFLICT_MESSAGE,
+                    &["Overwrite", "Discard", "Cancel"],
+                )
+            })?;
+            match answer.next().await {
+                Some(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?,
+                Some(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
+                _ => return Ok(false),
             }
-        } else if is_dirty && (can_save || is_singleton) {
-            let will_autosave = cx.read(|cx| {
-                matches!(
-                    settings::get::<WorkspaceSettings>(cx).autosave,
-                    AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
-                ) && Self::can_autosave_item(&*item, cx)
-            });
-            let should_save = if save_behavior == SaveBehavior::PromptOnWrite && !will_autosave {
-                let mut answer = pane.update(cx, |pane, cx| {
-                    pane.activate_item(item_ix, true, true, cx);
-                    cx.prompt(
-                        PromptLevel::Warning,
-                        DIRTY_MESSAGE,
-                        &["Save", "Don't Save", "Cancel"],
-                    )
-                })?;
-                match answer.next().await {
-                    Some(0) => true,
-                    Some(1) => false,
-                    _ => return Ok(false),
+        } else if is_dirty && (can_save || can_save_as) {
+            if save_intent == SaveIntent::Close {
+                let will_autosave = cx.read(|cx| {
+                    matches!(
+                        settings::get::<WorkspaceSettings>(cx).autosave,
+                        AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
+                    ) && Self::can_autosave_item(&*item, cx)
+                });
+                if !will_autosave {
+                    let mut answer = pane.update(cx, |pane, cx| {
+                        pane.activate_item(item_ix, true, true, cx);
+                        let prompt = dirty_message_for(item.project_path(cx));
+                        cx.prompt(
+                            PromptLevel::Warning,
+                            &prompt,
+                            &["Save", "Don't Save", "Cancel"],
+                        )
+                    })?;
+                    match answer.next().await {
+                        Some(0) => {}
+                        Some(1) => return Ok(true), // Don't save his file
+                        _ => return Ok(false),      // Cancel
+                    }
                 }
-            } else {
-                true
-            };
+            }
 
-            if should_save {
-                if can_save {
-                    pane.update(cx, |_, cx| item.save(project, cx))?.await?;
-                } else if is_singleton {
-                    let start_abs_path = project
-                        .read_with(cx, |project, cx| {
-                            let worktree = project.visible_worktrees(cx).next()?;
-                            Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
-                        })
-                        .unwrap_or_else(|| Path::new("").into());
+            if can_save {
+                pane.update(cx, |_, cx| item.save(project, cx))?.await?;
+            } else if can_save_as {
+                let start_abs_path = project
+                    .read_with(cx, |project, cx| {
+                        let worktree = project.visible_worktrees(cx).next()?;
+                        Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
+                    })
+                    .unwrap_or_else(|| Path::new("").into());
 
-                    let mut abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path));
-                    if let Some(abs_path) = abs_path.next().await.flatten() {
-                        pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))?
-                            .await?;
-                    } else {
-                        return Ok(false);
-                    }
+                let mut abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path));
+                if let Some(abs_path) = abs_path.next().await.flatten() {
+                    pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))?
+                        .await?;
+                } else {
+                    return Ok(false);
                 }
             }
         }
@@ -1167,15 +1242,16 @@ impl Pane {
                     vec![
                         ContextMenuItem::action(
                             "Close Active Item",
-                            CloseActiveItem {
-                                save_behavior: None,
-                            },
+                            CloseActiveItem { save_intent: None },
                         ),
                         ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
                         ContextMenuItem::action("Close Clean Items", CloseCleanItems),
                         ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft),
                         ContextMenuItem::action("Close Items To The Right", CloseItemsToTheRight),
-                        ContextMenuItem::action("Close All Items", CloseAllItems),
+                        ContextMenuItem::action(
+                            "Close All Items",
+                            CloseAllItems { save_intent: None },
+                        ),
                     ]
                 } else {
                     // In the case of the user right clicking on a non-active tab, for some item-closing commands, we need to provide the id of the tab, for the others, we can reuse the existing command.
@@ -1187,7 +1263,7 @@ impl Pane {
                                     pane.update(cx, |pane, cx| {
                                         pane.close_item_by_id(
                                             target_item_id,
-                                            SaveBehavior::PromptOnWrite,
+                                            SaveIntent::Close,
                                             cx,
                                         )
                                         .detach_and_log_err(cx);
@@ -1219,7 +1295,10 @@ impl Pane {
                                 }
                             }
                         }),
-                        ContextMenuItem::action("Close All Items", CloseAllItems),
+                        ContextMenuItem::action(
+                            "Close All Items",
+                            CloseAllItems { save_intent: None },
+                        ),
                     ]
                 },
                 cx,
@@ -1339,12 +1418,8 @@ impl Pane {
                                 .on_click(MouseButton::Middle, {
                                     let item_id = item.id();
                                     move |_, pane, cx| {
-                                        pane.close_item_by_id(
-                                            item_id,
-                                            SaveBehavior::PromptOnWrite,
-                                            cx,
-                                        )
-                                        .detach_and_log_err(cx);
+                                        pane.close_item_by_id(item_id, SaveIntent::Close, cx)
+                                            .detach_and_log_err(cx);
                                     }
                                 })
                                 .on_down(
@@ -1552,7 +1627,7 @@ impl Pane {
                     cx.window_context().defer(move |cx| {
                         if let Some(pane) = pane.upgrade(cx) {
                             pane.update(cx, |pane, cx| {
-                                pane.close_item_by_id(item_id, SaveBehavior::PromptOnWrite, cx)
+                                pane.close_item_by_id(item_id, SaveIntent::Close, cx)
                                     .detach_and_log_err(cx);
                             });
                         }
@@ -2135,6 +2210,15 @@ impl<V: 'static> Element<V> for PaneBackdrop<V> {
     }
 }
 
+fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
+    let path = buffer_path
+        .as_ref()
+        .and_then(|p| p.path.to_str())
+        .unwrap_or(&"This buffer");
+    let path = truncate_and_remove_front(path, 80);
+    format!("{path} contains unsaved edits. Do you want to save it?")
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -2155,12 +2239,7 @@ mod tests {
 
         pane.update(cx, |pane, cx| {
             assert!(pane
-                .close_active_item(
-                    &CloseActiveItem {
-                        save_behavior: None
-                    },
-                    cx
-                )
+                .close_active_item(&CloseActiveItem { save_intent: None }, cx)
                 .is_none())
         });
     }
@@ -2412,12 +2491,7 @@ mod tests {
         assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
 
         pane.update(cx, |pane, cx| {
-            pane.close_active_item(
-                &CloseActiveItem {
-                    save_behavior: None,
-                },
-                cx,
-            )
+            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
         })
         .unwrap()
         .await
@@ -2428,12 +2502,7 @@ mod tests {
         assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
 
         pane.update(cx, |pane, cx| {
-            pane.close_active_item(
-                &CloseActiveItem {
-                    save_behavior: None,
-                },
-                cx,
-            )
+            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
         })
         .unwrap()
         .await
@@ -2441,12 +2510,7 @@ mod tests {
         assert_item_labels(&pane, ["A", "B*", "C"], cx);
 
         pane.update(cx, |pane, cx| {
-            pane.close_active_item(
-                &CloseActiveItem {
-                    save_behavior: None,
-                },
-                cx,
-            )
+            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
         })
         .unwrap()
         .await
@@ -2454,12 +2518,7 @@ mod tests {
         assert_item_labels(&pane, ["A", "C*"], cx);
 
         pane.update(cx, |pane, cx| {
-            pane.close_active_item(
-                &CloseActiveItem {
-                    save_behavior: None,
-                },
-                cx,
-            )
+            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
         })
         .unwrap()
         .await
@@ -2569,10 +2628,28 @@ mod tests {
         add_labeled_item(&pane, "C", false, cx);
         assert_item_labels(&pane, ["A", "B", "C*"], cx);
 
-        pane.update(cx, |pane, cx| pane.close_all_items(&CloseAllItems, cx))
-            .unwrap()
-            .await
+        pane.update(cx, |pane, cx| {
+            pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
+        })
+        .unwrap()
+        .await
+        .unwrap();
+        assert_item_labels(&pane, [], cx);
+
+        add_labeled_item(&pane, "A", true, cx);
+        add_labeled_item(&pane, "B", true, cx);
+        add_labeled_item(&pane, "C", true, cx);
+        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
+
+        let save = pane
+            .update(cx, |pane, cx| {
+                pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
+            })
             .unwrap();
+
+        cx.foreground().run_until_parked();
+        window.simulate_prompt_answer(2, cx);
+        save.await.unwrap();
         assert_item_labels(&pane, [], cx);
     }
 

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

@@ -84,6 +84,13 @@ impl PaneGroup {
         }
     }
 
+    pub fn swap(&mut self, from: &ViewHandle<Pane>, to: &ViewHandle<Pane>) {
+        match &mut self.root {
+            Member::Pane(_) => {}
+            Member::Axis(axis) => axis.swap(from, to),
+        };
+    }
+
     pub(crate) fn render(
         &self,
         project: &ModelHandle<Project>,
@@ -421,6 +428,21 @@ impl PaneAxis {
         }
     }
 
+    fn swap(&mut self, from: &ViewHandle<Pane>, to: &ViewHandle<Pane>) {
+        for member in self.members.iter_mut() {
+            match member {
+                Member::Axis(axis) => axis.swap(from, to),
+                Member::Pane(pane) => {
+                    if pane == from {
+                        *member = Member::Pane(to.clone());
+                    } else if pane == to {
+                        *member = Member::Pane(from.clone())
+                    }
+                }
+            }
+        }
+    }
+
     fn bounding_box_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<RectF> {
         debug_assert!(self.members.len() == self.bounding_boxes.borrow().len());
 

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

@@ -126,9 +126,8 @@ actions!(
         CloseInactiveTabsAndPanes,
         AddFolderToProject,
         Unfollow,
-        Save,
         SaveAs,
-        SaveAll,
+        ReloadActiveItem,
         ActivatePreviousPane,
         ActivateNextPane,
         FollowNextCollaborator,
@@ -158,6 +157,30 @@ pub struct ActivatePane(pub usize);
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct ActivatePaneInDirection(pub SplitDirection);
 
+#[derive(Clone, Deserialize, PartialEq)]
+pub struct SwapPaneInDirection(pub SplitDirection);
+
+#[derive(Clone, Deserialize, PartialEq)]
+pub struct NewFileInDirection(pub SplitDirection);
+
+#[derive(Clone, PartialEq, Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SaveAll {
+    pub save_intent: Option<SaveIntent>,
+}
+
+#[derive(Clone, PartialEq, Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Save {
+    pub save_intent: Option<SaveIntent>,
+}
+
+#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[serde(rename_all = "camelCase")]
+pub struct CloseAllItemsAndPanes {
+    pub save_intent: Option<SaveIntent>,
+}
+
 #[derive(Deserialize)]
 pub struct Toast {
     id: usize,
@@ -210,7 +233,17 @@ pub struct OpenTerminal {
 
 impl_actions!(
     workspace,
-    [ActivatePane, ActivatePaneInDirection, Toast, OpenTerminal]
+    [
+        ActivatePane,
+        ActivatePaneInDirection,
+        SwapPaneInDirection,
+        NewFileInDirection,
+        Toast,
+        OpenTerminal,
+        SaveAll,
+        Save,
+        CloseAllItemsAndPanes,
+    ]
 );
 
 pub type WorkspaceId = i64;
@@ -251,6 +284,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
     cx.add_async_action(Workspace::follow_next_collaborator);
     cx.add_async_action(Workspace::close);
     cx.add_async_action(Workspace::close_inactive_items_and_panes);
+    cx.add_async_action(Workspace::close_all_items_and_panes);
     cx.add_global_action(Workspace::close_global);
     cx.add_global_action(restart);
     cx.add_async_action(Workspace::save_all);
@@ -262,13 +296,17 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
         },
     );
     cx.add_action(
-        |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext<Workspace>| {
-            workspace.save_active_item(false, cx).detach_and_log_err(cx);
+        |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext<Workspace>| {
+            workspace
+                .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
+                .detach_and_log_err(cx);
         },
     );
     cx.add_action(
         |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
-            workspace.save_active_item(true, cx).detach_and_log_err(cx);
+            workspace
+                .save_active_item(SaveIntent::SaveAs, cx)
+                .detach_and_log_err(cx);
         },
     );
     cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
@@ -284,6 +322,12 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
         },
     );
 
+    cx.add_action(
+        |workspace: &mut Workspace, action: &SwapPaneInDirection, cx| {
+            workspace.swap_pane_in_direction(action.0, cx)
+        },
+    );
+
     cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| {
         workspace.toggle_dock(DockPosition::Left, cx);
     });
@@ -1303,14 +1347,19 @@ impl Workspace {
 
             Ok(this
                 .update(&mut cx, |this, cx| {
-                    this.save_all_internal(SaveBehavior::PromptOnWrite, cx)
+                    this.save_all_internal(SaveIntent::Close, cx)
                 })?
                 .await?)
         })
     }
 
-    fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
-        let save_all = self.save_all_internal(SaveBehavior::PromptOnConflict, cx);
+    fn save_all(
+        &mut self,
+        action: &SaveAll,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let save_all =
+            self.save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx);
         Some(cx.foreground().spawn(async move {
             save_all.await?;
             Ok(())
@@ -1319,13 +1368,12 @@ impl Workspace {
 
     fn save_all_internal(
         &mut self,
-        save_behaviour: SaveBehavior,
+        mut save_intent: SaveIntent,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<bool>> {
         if self.project.read(cx).is_read_only() {
             return Task::ready(Ok(true));
         }
-
         let dirty_items = self
             .panes
             .iter()
@@ -1341,7 +1389,27 @@ impl Workspace {
             .collect::<Vec<_>>();
 
         let project = self.project.clone();
-        cx.spawn(|_, mut cx| async move {
+        cx.spawn(|workspace, mut cx| async move {
+            // Override save mode and display "Save all files" prompt
+            if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
+                let mut answer = workspace.update(&mut cx, |_, cx| {
+                    let prompt = Pane::file_names_for_prompt(
+                        &mut dirty_items.iter().map(|(_, handle)| handle),
+                        dirty_items.len(),
+                        cx,
+                    );
+                    cx.prompt(
+                        PromptLevel::Warning,
+                        &prompt,
+                        &["Save all", "Discard all", "Cancel"],
+                    )
+                })?;
+                match answer.next().await {
+                    Some(0) => save_intent = SaveIntent::SaveAll,
+                    Some(1) => save_intent = SaveIntent::Skip,
+                    _ => {}
+                }
+            }
             for (pane, item) in dirty_items {
                 let (singleton, project_entry_ids) =
                     cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
@@ -1354,7 +1422,7 @@ impl Workspace {
                             &pane,
                             ix,
                             &*item,
-                            save_behaviour,
+                            save_intent,
                             &mut cx,
                         )
                         .await?
@@ -1626,75 +1694,72 @@ impl Workspace {
 
     pub fn save_active_item(
         &mut self,
-        force_name_change: bool,
+        save_intent: SaveIntent,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>> {
         let project = self.project.clone();
-        if let Some(item) = self.active_item(cx) {
-            if !force_name_change && item.can_save(cx) {
-                if item.has_conflict(cx) {
-                    const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
+        let pane = self.active_pane();
+        let item_ix = pane.read(cx).active_item_index();
+        let item = pane.read(cx).active_item();
+        let pane = pane.downgrade();
 
-                    let mut answer = cx.prompt(
-                        PromptLevel::Warning,
-                        CONFLICT_MESSAGE,
-                        &["Overwrite", "Cancel"],
-                    );
-                    cx.spawn(|this, mut cx| async move {
-                        let answer = answer.recv().await;
-                        if answer == Some(0) {
-                            this.update(&mut cx, |this, cx| item.save(this.project.clone(), cx))?
-                                .await?;
-                        }
-                        Ok(())
-                    })
-                } else {
-                    item.save(self.project.clone(), cx)
-                }
-            } else if item.is_singleton(cx) {
-                let worktree = self.worktrees(cx).next();
-                let start_abs_path = worktree
-                    .and_then(|w| w.read(cx).as_local())
-                    .map_or(Path::new(""), |w| w.abs_path())
-                    .to_path_buf();
-                let mut abs_path = cx.prompt_for_new_path(&start_abs_path);
-                cx.spawn(|this, mut cx| async move {
-                    if let Some(abs_path) = abs_path.recv().await.flatten() {
-                        this.update(&mut cx, |_, cx| item.save_as(project, abs_path, cx))?
-                            .await?;
-                    }
-                    Ok(())
-                })
+        cx.spawn(|_, mut cx| async move {
+            if let Some(item) = item {
+                Pane::save_item(project, &pane, item_ix, item.as_ref(), save_intent, &mut cx)
+                    .await
+                    .map(|_| ())
             } else {
-                Task::ready(Ok(()))
+                Ok(())
             }
-        } else {
-            Task::ready(Ok(()))
-        }
+        })
     }
 
     pub fn close_inactive_items_and_panes(
         &mut self,
         _: &CloseInactiveTabsAndPanes,
         cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        self.close_all_internal(true, SaveIntent::Close, cx)
+    }
+
+    pub fn close_all_items_and_panes(
+        &mut self,
+        action: &CloseAllItemsAndPanes,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx)
+    }
+
+    fn close_all_internal(
+        &mut self,
+        retain_active_pane: bool,
+        save_intent: SaveIntent,
+        cx: &mut ViewContext<Self>,
     ) -> Option<Task<Result<()>>> {
         let current_pane = self.active_pane();
 
         let mut tasks = Vec::new();
 
-        if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
-            pane.close_inactive_items(&CloseInactiveItems, cx)
-        }) {
-            tasks.push(current_pane_close);
-        };
+        if retain_active_pane {
+            if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
+                pane.close_inactive_items(&CloseInactiveItems, cx)
+            }) {
+                tasks.push(current_pane_close);
+            };
+        }
 
         for pane in self.panes() {
-            if pane.id() == current_pane.id() {
+            if retain_active_pane && pane.id() == current_pane.id() {
                 continue;
             }
 
             if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
-                pane.close_all_items(&CloseAllItems, cx)
+                pane.close_all_items(
+                    &CloseAllItems {
+                        save_intent: Some(save_intent),
+                    },
+                    cx,
+                )
             }) {
                 tasks.push(close_pane_items)
             }
@@ -1925,8 +1990,13 @@ impl Workspace {
             .update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
     }
 
-    pub fn split_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
-        let new_pane = self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
+    pub fn split_item(
+        &mut self,
+        split_direction: SplitDirection,
+        item: Box<dyn ItemHandle>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx);
         new_pane.update(cx, move |new_pane, cx| {
             new_pane.add_item(item, true, true, None, cx)
         })
@@ -2104,7 +2174,7 @@ impl Workspace {
         }
 
         let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
-        self.split_item(Box::new(item.clone()), cx);
+        self.split_item(SplitDirection::Right, Box::new(item.clone()), cx);
         item
     }
 
@@ -2162,11 +2232,32 @@ impl Workspace {
         direction: SplitDirection,
         cx: &mut ViewContext<Self>,
     ) {
-        let bounding_box = match self.center.bounding_box_for_pane(&self.active_pane) {
-            Some(coordinates) => coordinates,
-            None => {
-                return;
-            }
+        if let Some(pane) = self.find_pane_in_direction(direction, cx) {
+            cx.focus(pane);
+        }
+    }
+
+    pub fn swap_pane_in_direction(
+        &mut self,
+        direction: SplitDirection,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if let Some(to) = self
+            .find_pane_in_direction(direction, cx)
+            .map(|pane| pane.clone())
+        {
+            self.center.swap(&self.active_pane.clone(), &to);
+            cx.notify();
+        }
+    }
+
+    fn find_pane_in_direction(
+        &mut self,
+        direction: SplitDirection,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<&ViewHandle<Pane>> {
+        let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else {
+            return None;
         };
         let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
         let center = match cursor {
@@ -2182,10 +2273,7 @@ impl Workspace {
             SplitDirection::Up => vec2f(center.x(), bounding_box.origin_y() - distance_to_next),
             SplitDirection::Down => vec2f(center.x(), bounding_box.max_y() + distance_to_next),
         };
-
-        if let Some(pane) = self.center.pane_at_pixel_position(target) {
-            cx.focus(pane);
-        }
+        self.center.pane_at_pixel_position(target)
     }
 
     fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
@@ -4272,7 +4360,9 @@ mod tests {
         });
         let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
         cx.foreground().run_until_parked();
-        window.simulate_prompt_answer(2, cx); // cancel
+        window.simulate_prompt_answer(2, cx); // cancel save all
+        cx.foreground().run_until_parked();
+        window.simulate_prompt_answer(2, cx); // cancel save all
         cx.foreground().run_until_parked();
         assert!(!window.has_pending_prompt(cx));
         assert!(!task.await.unwrap());
@@ -4324,19 +4414,21 @@ mod tests {
             let item1_id = item1.id();
             let item3_id = item3.id();
             let item4_id = item4.id();
-            pane.close_items(cx, SaveBehavior::PromptOnWrite, move |id| {
+            pane.close_items(cx, SaveIntent::Close, move |id| {
                 [item1_id, item3_id, item4_id].contains(&id)
             })
         });
         cx.foreground().run_until_parked();
 
+        assert!(window.has_pending_prompt(cx));
+        // Ignore "Save all" prompt
+        window.simulate_prompt_answer(2, cx);
+        cx.foreground().run_until_parked();
         // There's a prompt to save item 1.
         pane.read_with(cx, |pane, _| {
             assert_eq!(pane.items_len(), 4);
             assert_eq!(pane.active_item().unwrap().id(), item1.id());
         });
-        assert!(window.has_pending_prompt(cx));
-
         // Confirm saving item 1.
         window.simulate_prompt_answer(0, cx);
         cx.foreground().run_until_parked();
@@ -4462,8 +4554,12 @@ mod tests {
         // prompts, the task should complete.
 
         let close = left_pane.update(cx, |pane, cx| {
-            pane.close_items(cx, SaveBehavior::PromptOnWrite, move |_| true)
+            pane.close_items(cx, SaveIntent::Close, move |_| true)
         });
+        cx.foreground().run_until_parked();
+        // Discard "Save all" prompt
+        window.simulate_prompt_answer(2, cx);
+
         cx.foreground().run_until_parked();
         left_pane.read_with(cx, |pane, cx| {
             assert_eq!(
@@ -4580,7 +4676,7 @@ mod tests {
         });
 
         pane.update(cx, |pane, cx| {
-            pane.close_items(cx, SaveBehavior::PromptOnWrite, move |id| id == item_id)
+            pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
         })
         .await
         .unwrap();
@@ -4603,7 +4699,7 @@ mod tests {
 
         // Ensure autosave is prevented for deleted files also when closing the buffer.
         let _close_items = pane.update(cx, |pane, cx| {
-            pane.close_items(cx, SaveBehavior::PromptOnWrite, move |id| id == item_id)
+            pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
         });
         deterministic.run_until_parked();
         assert!(window.has_pending_prompt(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.106.0"
+version = "0.107.0"
 publish = false
 
 [lib]
@@ -50,7 +50,7 @@ language_selector = { path = "../language_selector" }
 lsp = { path = "../lsp" }
 language_tools = { path = "../language_tools" }
 node_runtime = { path = "../node_runtime" }
-ai = { path = "../ai" }
+assistant = { path = "../assistant" }
 outline = { path = "../outline" }
 plugin_runtime = { path = "../plugin_runtime",optional = true }
 project = { path = "../project" }
@@ -62,6 +62,7 @@ rpc = { path = "../rpc" }
 settings = { path = "../settings" }
 feature_flags = { path = "../feature_flags" }
 sum_tree = { path = "../sum_tree" }
+shellexpand = "2.1.0"
 text = { path = "../text" }
 terminal_view = { path = "../terminal_view" }
 theme = { path = "../theme" }
@@ -99,6 +100,7 @@ rust-embed.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
+schemars.workspace = true
 simplelog = "0.9"
 smallvec.workspace = true
 smol.workspace = true

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

@@ -1,13 +1,17 @@
 use anyhow::Context;
+use gpui::AppContext;
 pub use language::*;
 use node_runtime::NodeRuntime;
 use rust_embed::RustEmbed;
 use std::{borrow::Cow, str, sync::Arc};
 use util::asset_str;
 
+use self::elixir_next::ElixirSettings;
+
 mod c;
 mod css;
 mod elixir;
+mod elixir_next;
 mod go;
 mod html;
 mod json;
@@ -37,7 +41,13 @@ mod yaml;
 #[exclude = "*.rs"]
 struct LanguageDir;
 
-pub fn init(languages: Arc<LanguageRegistry>, node_runtime: Arc<dyn NodeRuntime>) {
+pub fn init(
+    languages: Arc<LanguageRegistry>,
+    node_runtime: Arc<dyn NodeRuntime>,
+    cx: &mut AppContext,
+) {
+    settings::register::<elixir_next::ElixirSettings>(cx);
+
     let language = |name, grammar, adapters| {
         languages.register(name, load_config(name), grammar, adapters, load_queries)
     };
@@ -61,11 +71,28 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: Arc<dyn NodeRuntime>
             Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
         ],
     );
-    language(
-        "elixir",
-        tree_sitter_elixir::language(),
-        vec![Arc::new(elixir::ElixirLspAdapter)],
-    );
+
+    match &settings::get::<ElixirSettings>(cx).next {
+        elixir_next::ElixirNextSetting::Off => language(
+            "elixir",
+            tree_sitter_elixir::language(),
+            vec![Arc::new(elixir::ElixirLspAdapter)],
+        ),
+        elixir_next::ElixirNextSetting::On => language(
+            "elixir",
+            tree_sitter_elixir::language(),
+            vec![Arc::new(elixir_next::NextLspAdapter)],
+        ),
+        elixir_next::ElixirNextSetting::Local { path, arguments } => language(
+            "elixir",
+            tree_sitter_elixir::language(),
+            vec![Arc::new(elixir_next::LocalNextLspAdapter {
+                path: path.clone(),
+                arguments: arguments.clone(),
+            })],
+        ),
+    }
+
     language(
         "go",
         tree_sitter_go::language(),

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

@@ -0,0 +1,266 @@
+use anyhow::{anyhow, bail, Result};
+
+use async_trait::async_trait;
+pub use language::*;
+use lsp::{LanguageServerBinary, SymbolKind};
+use schemars::JsonSchema;
+use serde_derive::{Deserialize, Serialize};
+use settings::Setting;
+use smol::{fs, stream::StreamExt};
+use std::{any::Any, env::consts, ops::Deref, path::PathBuf, sync::Arc};
+use util::{
+    async_iife,
+    github::{latest_github_release, GitHubLspBinaryVersion},
+    ResultExt,
+};
+
+#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+pub struct ElixirSettings {
+    pub next: ElixirNextSetting,
+}
+
+#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ElixirNextSetting {
+    Off,
+    On,
+    Local {
+        path: String,
+        arguments: Vec<String>,
+    },
+}
+
+#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)]
+pub struct ElixirSettingsContent {
+    next: Option<ElixirNextSetting>,
+}
+
+impl Setting for ElixirSettings {
+    const KEY: Option<&'static str> = Some("elixir");
+
+    type FileContent = ElixirSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &gpui::AppContext,
+    ) -> Result<Self>
+    where
+        Self: Sized,
+    {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}
+
+pub struct NextLspAdapter;
+
+#[async_trait]
+impl LspAdapter for NextLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("next-ls".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "next-ls"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        let release =
+            latest_github_release("elixir-tools/next-ls", false, delegate.http_client()).await?;
+        let version = release.name.clone();
+        let platform = match consts::ARCH {
+            "x86_64" => "darwin_arm64",
+            "aarch64" => "darwin_amd64",
+            other => bail!("Running on unsupported platform: {other}"),
+        };
+        let asset_name = format!("next_ls_{}", platform);
+        let asset = release
+            .assets
+            .iter()
+            .find(|asset| asset.name == asset_name)
+            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
+        let version = GitHubLspBinaryVersion {
+            name: version,
+            url: asset.browser_download_url.clone(),
+        };
+        Ok(Box::new(version) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
+
+        let binary_path = container_dir.join("next-ls");
+
+        if fs::metadata(&binary_path).await.is_err() {
+            let mut response = delegate
+                .http_client()
+                .get(&version.url, Default::default(), true)
+                .await
+                .map_err(|err| anyhow!("error downloading release: {}", err))?;
+
+            let mut file = smol::fs::File::create(&binary_path).await?;
+            if !response.status().is_success() {
+                Err(anyhow!(
+                    "download failed with status {}",
+                    response.status().to_string()
+                ))?;
+            }
+            futures::io::copy(response.body_mut(), &mut file).await?;
+
+            fs::set_permissions(
+                &binary_path,
+                <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
+            )
+            .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: binary_path,
+            arguments: vec!["--stdio".into()],
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir)
+            .await
+            .map(|mut binary| {
+                binary.arguments = vec!["--stdio".into()];
+                binary
+            })
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir)
+            .await
+            .map(|mut binary| {
+                binary.arguments = vec!["--help".into()];
+                binary
+            })
+    }
+
+    async fn label_for_symbol(
+        &self,
+        name: &str,
+        symbol_kind: SymbolKind,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        label_for_symbol_next(name, symbol_kind, language)
+    }
+}
+
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    async_iife!({
+        let mut last_binary_path = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            let entry = entry?;
+            if entry.file_type().await?.is_file()
+                && entry
+                    .file_name()
+                    .to_str()
+                    .map_or(false, |name| name == "next-ls")
+            {
+                last_binary_path = Some(entry.path());
+            }
+        }
+
+        if let Some(path) = last_binary_path {
+            Ok(LanguageServerBinary {
+                path,
+                arguments: Vec::new(),
+            })
+        } else {
+            Err(anyhow!("no cached binary"))
+        }
+    })
+    .await
+    .log_err()
+}
+
+pub struct LocalNextLspAdapter {
+    pub path: String,
+    pub arguments: Vec<String>,
+}
+
+#[async_trait]
+impl LspAdapter for LocalNextLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("local-next-ls".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "next-ls"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        Ok(Box::new(()) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        _: Box<dyn 'static + Send + Any>,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let path = shellexpand::full(&self.path)?;
+        Ok(LanguageServerBinary {
+            path: PathBuf::from(path.deref()),
+            arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        let path = shellexpand::full(&self.path).ok()?;
+        Some(LanguageServerBinary {
+            path: PathBuf::from(path.deref()),
+            arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
+        })
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+        let path = shellexpand::full(&self.path).ok()?;
+        Some(LanguageServerBinary {
+            path: PathBuf::from(path.deref()),
+            arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
+        })
+    }
+
+    async fn label_for_symbol(
+        &self,
+        name: &str,
+        symbol: SymbolKind,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        label_for_symbol_next(name, symbol, language)
+    }
+}
+
+fn label_for_symbol_next(name: &str, _: SymbolKind, language: &Arc<Language>) -> Option<CodeLabel> {
+    Some(CodeLabel {
+        runs: language.highlight_text(&name.into(), 0..name.len()),
+        text: name.to_string(),
+        filter_range: 0..name.len(),
+    })
+}

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

@@ -135,7 +135,7 @@ fn main() {
         let languages = Arc::new(languages);
         let node_runtime = RealNodeRuntime::new(http.clone());
 
-        languages::init(languages.clone(), node_runtime.clone());
+        languages::init(languages.clone(), node_runtime.clone(), cx);
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
         let channel_store =
             cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
@@ -161,7 +161,7 @@ fn main() {
         vim::init(cx);
         terminal_view::init(cx);
         copilot::init(copilot_language_server_id, http.clone(), node_runtime, cx);
-        ai::init(cx);
+        assistant::init(cx);
         component_test::init(cx);
 
         cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach();

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

@@ -38,14 +38,12 @@ pub fn menus() -> Vec<Menu<'static>> {
                 MenuItem::action("Open Recent...", recent_projects::OpenRecent),
                 MenuItem::separator(),
                 MenuItem::action("Add Folder to Project…", workspace::AddFolderToProject),
-                MenuItem::action("Save", workspace::Save),
+                MenuItem::action("Save", workspace::Save { save_intent: None }),
                 MenuItem::action("Save As…", workspace::SaveAs),
-                MenuItem::action("Save All", workspace::SaveAll),
+                MenuItem::action("Save All", workspace::SaveAll { save_intent: None }),
                 MenuItem::action(
                     "Close Editor",
-                    workspace::CloseActiveItem {
-                        save_behavior: None,
-                    },
+                    workspace::CloseActiveItem { save_intent: None },
                 ),
                 MenuItem::action("Close Window", workspace::CloseWindow),
             ],

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

@@ -5,9 +5,9 @@ pub mod only_instance;
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
 
-use ai::AssistantPanel;
 use anyhow::Context;
 use assets::Assets;
+use assistant::AssistantPanel;
 use breadcrumbs::Breadcrumbs;
 pub use client;
 use collab_ui::CollabTitlebarItem; // TODO: Add back toggle collab ui shortcut
@@ -744,7 +744,7 @@ mod tests {
     use theme::{ThemeRegistry, ThemeSettings};
     use workspace::{
         item::{Item, ItemHandle},
-        open_new, open_paths, pane, NewFile, SaveBehavior, SplitDirection, WorkspaceHandle,
+        open_new, open_paths, pane, NewFile, SaveIntent, SplitDirection, WorkspaceHandle,
     };
 
     #[gpui::test]
@@ -945,10 +945,14 @@ mod tests {
 
         editor.update(cx, |editor, cx| {
             assert!(editor.text(cx).is_empty());
+            assert!(!editor.is_dirty(cx));
         });
 
-        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
+        let save_task = workspace.update(cx, |workspace, cx| {
+            workspace.save_active_item(SaveIntent::Save, cx)
+        });
         app_state.fs.create_dir(Path::new("/root")).await.unwrap();
+        cx.foreground().run_until_parked();
         cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
         save_task.await.unwrap();
         editor.read_with(cx, |editor, cx| {
@@ -1311,7 +1315,10 @@ mod tests {
             .await;
         cx.read(|cx| assert!(editor.is_dirty(cx)));
 
-        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
+        let save_task = workspace.update(cx, |workspace, cx| {
+            workspace.save_active_item(SaveIntent::Save, cx)
+        });
+        cx.foreground().run_until_parked();
         window.simulate_prompt_answer(0, cx);
         save_task.await.unwrap();
         editor.read_with(cx, |editor, cx| {
@@ -1353,7 +1360,10 @@ mod tests {
         });
 
         // Save the buffer. This prompts for a filename.
-        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
+        let save_task = workspace.update(cx, |workspace, cx| {
+            workspace.save_active_item(SaveIntent::Save, cx)
+        });
+        cx.foreground().run_until_parked();
         cx.simulate_new_path_selection(|parent_dir| {
             assert_eq!(parent_dir, Path::new("/root"));
             Some(parent_dir.join("the-new-name.rs"))
@@ -1377,7 +1387,9 @@ mod tests {
             editor.handle_input(" there", cx);
             assert!(editor.is_dirty(cx));
         });
-        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
+        let save_task = workspace.update(cx, |workspace, cx| {
+            workspace.save_active_item(SaveIntent::Save, cx)
+        });
         save_task.await.unwrap();
         assert!(!cx.did_prompt_for_new_path());
         editor.read_with(cx, |editor, cx| {
@@ -1444,7 +1456,10 @@ mod tests {
         });
 
         // Save the buffer. This prompts for a filename.
-        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
+        let save_task = workspace.update(cx, |workspace, cx| {
+            workspace.save_active_item(SaveIntent::Save, cx)
+        });
+        cx.foreground().run_until_parked();
         cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
         save_task.await.unwrap();
         // The buffer is not dirty anymore and the language is assigned based on the path.
@@ -1508,9 +1523,7 @@ mod tests {
         });
         cx.dispatch_action(
             window.into(),
-            workspace::CloseActiveItem {
-                save_behavior: None,
-            },
+            workspace::CloseActiveItem { save_intent: None },
         );
 
         cx.foreground().run_until_parked();
@@ -1521,9 +1534,7 @@ mod tests {
 
         cx.dispatch_action(
             window.into(),
-            workspace::CloseActiveItem {
-                save_behavior: None,
-            },
+            workspace::CloseActiveItem { save_intent: None },
         );
         cx.foreground().run_until_parked();
         window.simulate_prompt_answer(1, cx);
@@ -1682,7 +1693,7 @@ mod tests {
         pane.update(cx, |pane, cx| {
             let editor3_id = editor3.id();
             drop(editor3);
-            pane.close_item_by_id(editor3_id, SaveBehavior::PromptOnWrite, cx)
+            pane.close_item_by_id(editor3_id, SaveIntent::Close, cx)
         })
         .await
         .unwrap();
@@ -1717,7 +1728,7 @@ mod tests {
         pane.update(cx, |pane, cx| {
             let editor2_id = editor2.id();
             drop(editor2);
-            pane.close_item_by_id(editor2_id, SaveBehavior::PromptOnWrite, cx)
+            pane.close_item_by_id(editor2_id, SaveIntent::Close, cx)
         })
         .await
         .unwrap();
@@ -1874,28 +1885,28 @@ mod tests {
 
         // Close all the pane items in some arbitrary order.
         pane.update(cx, |pane, cx| {
-            pane.close_item_by_id(file1_item_id, SaveBehavior::PromptOnWrite, cx)
+            pane.close_item_by_id(file1_item_id, SaveIntent::Close, cx)
         })
         .await
         .unwrap();
         assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
 
         pane.update(cx, |pane, cx| {
-            pane.close_item_by_id(file4_item_id, SaveBehavior::PromptOnWrite, cx)
+            pane.close_item_by_id(file4_item_id, SaveIntent::Close, cx)
         })
         .await
         .unwrap();
         assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
 
         pane.update(cx, |pane, cx| {
-            pane.close_item_by_id(file2_item_id, SaveBehavior::PromptOnWrite, cx)
+            pane.close_item_by_id(file2_item_id, SaveIntent::Close, cx)
         })
         .await
         .unwrap();
         assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
 
         pane.update(cx, |pane, cx| {
-            pane.close_item_by_id(file3_item_id, SaveBehavior::PromptOnWrite, cx)
+            pane.close_item_by_id(file3_item_id, SaveIntent::Close, cx)
         })
         .await
         .unwrap();
@@ -2388,11 +2399,12 @@ mod tests {
 
     #[gpui::test]
     fn test_bundled_languages(cx: &mut AppContext) {
+        cx.set_global(SettingsStore::test(cx));
         let mut languages = LanguageRegistry::test();
         languages.set_executor(cx.background().clone());
         let languages = Arc::new(languages);
         let node_runtime = node_runtime::FakeNodeRuntime::new();
-        languages::init(languages.clone(), node_runtime);
+        languages::init(languages.clone(), node_runtime, cx);
         for name in languages.language_names() {
             languages.language_for_name(&name);
         }
@@ -2418,7 +2430,7 @@ mod tests {
             pane::init(cx);
             project_panel::init((), cx);
             terminal_view::init(cx);
-            ai::init(cx);
+            assistant::init(cx);
             app_state
         })
     }

docs/tools.md πŸ”—

@@ -32,7 +32,7 @@ Have a team member add you to the [Zed Industries](https://zed-industries.slack.
 
 ### Discord
 
-We have a discord community. You can use [this link](https://discord.gg/SSD9eJrn6s) to join. **!Don't share this link, this is specifically for team memebers!**
+We have a Discord community. You can use [this link](https://discord.gg/SSD9eJrn6s) to join. **!Don't share this link, this is specifically for team members!**
 
 Once you have joined the community, let a team member know and we can add your correct role.
 
@@ -56,7 +56,7 @@ We use Vercel for all of our web deployments and some backend things. If you sig
 
 ### Environment Variables
 
-You can get access to many of our shared enviroment variables through 1Password and Vercel. For one password search the value you are looking for, or sort by passwords or API credentials.
+You can get access to many of our shared enviroment variables through 1Password and Vercel. For 1Password search the value you are looking for, or sort by passwords or API credentials.
 
 For Vercel, go to `settings` -> `Environment Variables` (either on the entire org, or on a specific project depending on where it is shared.) For a given Vercel project if you have their CLI installed you can use `vercel pull` or `vercel env` to pull values down directly. More on those in their [CLI docs](https://vercel.com/docs/cli/env).
 

docs/ui/states.md πŸ”—

@@ -1,43 +0,0 @@
-## Interaction State
-
-**Enabled**
-
-An enabled state communicates an interactive component or element.
-
-**Disabled**
-
-A disabled state communicates a inoperable component or element.
-
-**Hover**
-
-A hover state communicates when a user has placed a cursor above an interactive element.
-
-**Focused**
-
-A focused state communicates when a user has highlighted an element, using an input method such as a keyboard or voice.
-
-**Activated**
-
-An activated state communicates a highlighted destination, whether initiated by the user or by default.
-
-**Pressed**
-
-A pressed state communicates a user tap.
-
-**Dragged**
-
-A dragged state communicates when a user presses and moves an element.
-
-## Selected State
-
-**Unselected**
-
-dfa
-
-**Partially Selected**
-
-daf
-
-**Selected**
-
-dfa

script/evaluate_semantic_index πŸ”—

@@ -1,3 +1,3 @@
 #!/bin/bash
 
-cargo run -p semantic_index --example eval
+RUST_LOG=semantic_index=trace cargo run -p semantic_index --example eval --release

styles/src/style_tree/update_notification.ts πŸ”—

@@ -26,10 +26,10 @@ export default function update_notification(): any {
         dismiss_button: interactive({
             base: {
                 color: foreground(theme.middle),
-                icon_width: 8,
-                icon_height: 8,
-                button_width: 8,
-                button_height: 8,
+                icon_width: 14,
+                icon_height: 14,
+                button_width: 14,
+                button_height: 14,
             },
             state: {
                 hovered: {